avatarPetr Jahoda

Summary

The web content provides a comprehensive guide on running a Go service within a Docker container, including methods to significantly reduce the executable size.

Abstract

The article is an installment in a series detailing the creation and enhancement of a Go service. It emphasizes the use of Docker to streamline deployment across various systems, ensuring consistency and ease of management. The author outlines two methods for building the Go executable: using Goland's built-in functionality and employing a custom bash script. A key focus is on minimizing the size of the Go executable by approximately 73% through the use of specific compiler flags and an executable packer like UPX. The article also covers the creation of a Docker image, pushing it to Docker Hub, and running the Go service as a Docker container, including running it in the background. Additionally, the author provides a bonus feature on automated versioning to ensure the docker image is always up-to-date with the latest software version.

Opinions

  • The author advocates for the use of Docker due to its ability to create a uniform deployment system regardless of the underlying hardware, simplifying the management of multiple services and databases.
  • Docker is recommended for its flexibility in changing service configurations, such as web server ports, without altering the Go code.
  • The author suggests Bret Fisher's "Docker Mastery" course for those unfamiliar with Docker, indicating its effectiveness in building foundational Docker knowledge.
  • The author expresses a preference for using a bash script to automate the build and deployment process, highlighting its efficiency in handling multiple tasks, including building the executable, packing it, and managing Docker images and containers.
  • The use of UPX for executable compression is endorsed by the author, who reports no issues with its use in production environments.
  • The article conveys the importance of maintaining accurate versioning, with the author providing a method for automating this process to ensure consistency between the software version and the Docker image.

Run Go service in Docker

And reduce executable size by ~73% as a bonus

Original Gopher image created by Renee French, used Gopher image created by Maria Letta
  1. part: creating the service
  2. part: improving the service
  3. part: upgrade for web
  4. part: adding SSE
  5. part: simple javascript frontend functionality
  6. part: frontend — backend communication
  7. part: server-side logging
  8. part: you are reading it right now
  9. part: adding database container
  10. part: database — service communication

Addition 1: functions, methods, pointers and interfaces Addition 2: websocket communication Addition 3: socket communication

Why use Docker at all?

Of course, you can create Go executable for every system and you can run it where-ever you want. That’s true.

By using Docker, you can create a neat deploy system for yourself. And you do not care, what system actually runs on the final machine. You only want the machine to run Docker. You can easily stop and start multiple programs (multiple services if you like this terminology). You can easily change the port for Go web server without changing the Go code itself. You can run multiple different databases at the same time.

You can do a lot using Docker and I advise you to invest time to learn it.

For those of you not familiar with Docker, I recommend Bret Fisher’s Docker Mastery. This helped me to move from “what the hell is Docker” to “great, what else can I use Docker for”.

And for those of you already familiar with Docker, but want to know more, this talk can give you much more understanding about containers at all.

Prepare the project

Open Goland and create new file called Dockerfile in your project directory. Insert code below. Those command will create empty image using FROM scratch and then copy content from css, html, js and linux directory.

But wait, there is no linux directory yet. That reason is, we did not build our software. We did not make any executable.

Create Go executable

There are numerous ways, how to build our software… how to create this executable. One way is to use Goland’s built-in functionality.

Create a new configuration by copying the first one. And in this new configuration change four things:

  1. Name… this name will be used for the name of the executable and has to match with CMD [“/medium_service”] , so we use medium_service, Goland will automatically add _linux to filename
  2. Select output directory (linux as we will use linux containers)
  3. Remove tick on Run after build
  4. Set environment to GOOS=linux

Side-note: by defining GOOS you can build executable for any system and any architecture that is available.

Shrinking the executable by 73%

When you run this configuration, you immediately see that a new executable appears in this linux directory. This executable is about 9.6MB and we will do two tricks to make it much more smaller. First add two flags-ldflags=”-s -w” to Go tool arguments, as on screenshot below.

Run this configuration again and you can see, that the executable is about 6.9MB, reduction about 28%. Second trick is to use any executable packers, that are available. I use UPX and I use it also in production with no impact at all. With commandupx medium_service_linux the size of the executable will shrink to about 2.6MB, reduction of 73%.

Create Go executable with script

Another way, I prefer to use, is to use a script, be it a bash or powershell script. As I am using apple device, so below you can see my bash script. This script is called create.sh and is placed in the project directory. It does numerous things.

At first it will get name of the current working directory by using name=${PWD##*/}, then it will update all modules (not necessary, but I like to have latest versions of all modules), then it will build our executable using GOOS=linux go build -ldflags=”-s -w” -o linux/”$name”, then move into that linux folder, pack the executable with UPX and move out.

After all this is done, it will finally do something with docker. By using docker rmi -f petrjahoda/”$name”:latest it will remove any previous image already created before, by using docker build -t petrjahoda/”$name”:latest . it will create image. This dot means it will search for Dockerfile in a directory, where this command is executed. And finally it will push this image to docker hub using docker push petrjahoda/”$name”:latest. To make this final command working you need to have your own repository working and you have to be logged in to Docker on your machine.

Side-note: Don’t forget to chmod +x create.sh and remove that previous executable with _linux at the end.

Running the script

When you run this script you see numerous things going on (see below). After everything is done, you can find your image in your docker hub repository. In my case it is here.

go: golang.org/x/sys upgrade => v0.0.0202012230745330d417f636930
 Ultimate Packer for eXecutables
 Copyright © 19962020
UPX 3.96 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020
File size Ratio Format Name
 — — — — — — — — — — — — — — — — — — — — — — — — -
 6901760 -> 2630120 38.11% linux/amd64 medium_service
Packed 1 file.
Error: No such image: petrjahoda/medium_service:latest
[+] Building 0.4s (8/8) FINISHED 
 => [internal] load build definition from Dockerfile 0.0s
 => => transferring dockerfile: 135B 0.0s
 => [internal] load .dockerignore 0.0s
 => => transferring context: 2B 0.0s
 => [internal] load build context 0.2s
 => => transferring context: 5.26MB 0.1s
 => [1/4] COPY /css /css 0.0s
 => [2/4] COPY /html html 0.0s
 => [3/4] COPY /js js 0.0s
 => [4/4] COPY /linux / 0.0s
 => exporting to image 0.0s
 => => exporting layers 0.0s
 => => writing image sha256:1df9b9adbc1167cb1fcb1fc66d74cf33964c35530dc04fa4f5434e917bc50a53 0.0s
 => => naming to docker.io/petrjahoda/medium_service:latest 0.0s
The push refers to repository [docker.io/petrjahoda/medium_service]
bc5cf960a3a7: Pushed 
6eec7c2c219c: Pushed 
2038e7a8e302: Pushed 
95541ba72fa1: Pushed 
latest: digest: sha256:4584ba804f99d001a57d84bc85b6624eb8f9ab2b8edc9e226e43e670a0ce51b3 size: 1149

Test running Go service as Docker container

Looks like we did everything right. Time to test it.

Open up your terminal, or console, or use Goland’s built-in terminal and run docker run — name medium_service -p 90:81 petrjahoda/medium_service:latest. This will run a new container with name medium_service, mapping internal port 81 to external port 90 and using already created image with latest tag. When you run the command (in my example I had to use that port 82, because I used it in my application) you see logs from your application.

Open up your browser and navigate to http://localhost:90. Your web page has to be running. If you try to click on Ask for data button, you immediately see, that it was successfully logged.

Make Docker container run in the background

To make this docker container running all the time, end it for now using CTRL+C and remove it using docker rm medium_service. Now run it again, this time with added -d parameter: docker run — name medium_service -p 90:81 -d petrjahoda/medium_service:latest. This will run the container and detach from it. So now your console is free to use or close and you have your web server working in the background.

You can check your container working by using docker ps -a that will show all your not removed containers, or by using docker stats (see image below) that will monitor your running containers in realtime. In my case I have more running containers, medium_service is on the top.

CONGRATULATIONS. You have your own web service running in a Docker container. Every part of the software is running as it should and as a bonus, by using FROM scratch we make our container very secure (but on the other side you cannot from example ping FROM INSIDE the container).

By using docker image ls you can see our image is about 5.26MB whole (Go executable and all other files).

Bonus feature

Below is my create.sh file I use in production for every software. There are two different things. At first you see, it runs ./update and at the end there are three more docker commands.

The reason for this is, that I like to have a correct version of my software and do not have to think about it. So in every software in main.go I have a constant const version = “2020.4.3.14” and when the service is starting, i like to use something like logInfo(“MAIN”, serviceName+” [“+version+”] starting…”).

This ./update does two things. Update this constant and update those last three lines of code in create.sh, containing version.

The result is, that the latest docker image is always really the latest and there are more images as my software is built with proper version number. For example, version 2020.4.3.13 is combined like this:

  • 2020 is year
  • 4 is fourth quarter of the year
  • 3 is third month in this quarter
  • 13 is the day of the month

You can see using this approach for example here. Also that update executable is in the project directory.

Summary

This article helped you to know, how to …

  • create Go executable using two different methods
  • create Docker image and push it into Docker Hub
  • run Docker image with different parameters
  • run Docker image in the background (and check logs)
  • shrink Go executable by ~ 73%
  • add some automated versioning as bonus feature
Go
Golang
Programming
Tutorial
Docker
Recommended from ReadMedium