Create optimized Go image files and stepped pits

Keywords: Linux Docker Kubernetes github

It is not difficult to create a Go image file on Docker, but the created file is very large, close to 1G, which is not convenient to use. One of the main problems of Docker image is how to optimize and create a small image. We can create Docker image files with multi-level construction method, which is not complicated. But because of the need to use a simple version of Linux (Alpine) when using this method, it brings a series of problems. This article describes how to solve these problems and successfully create an optimized Go image file, which is only 14M after optimization.

Single level build:

We use a Go program as an example to show how to create a Go image. Here is the directory structure of this program.

The specific content of the Go program is not important, as long as it can be run. We will focus on the "docker" subdirectory (the files in the "kubernetes" subdirectory have other uses, which will be explained in another article). There are three documents in it. "docker-backend.sh" is the command file for creating images, "Dockerfile-k8sdemo-backend" is a multi-level build file, "Dockerfile-k8sdemo-backend-full" is a single level build file.

FROM golang:latest # Get standard golang image from Docker Library
WORKDIR /app # Set the current working directory in the image
COPY go.mod go.sum ./ # Copy the package management file of Go
RUN go mod download # Download the dependency Library in the dependency package
COPY . . #Copy files from host to image
WORKDIR /app/cmd # Set the current working directory in the new image
RUN GOOS=linux go build -o main.exe #Compile Go program and generate executable
CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait" # Keep the image running and keep the container running

Above is the "Dockerfile-k8sdemo-backend-full" image file. Please read the notes in the document for explanation.

Build image container

cd /home/vagrant/jfeng45/k8sdemo/
docker build -f ./script/kubernetes/backend/docker/Dockerfile-k8sdemo-backend-full -t k8sdemo-backend-full .

Run the image container, "-- name k8sdemo backend full" is to give the container a name (k8sdemo backend full). The last "k8sdemo backend full" is the name of the image.

docker run -td --name k8sdemo-backend-full k8sdemo-backend-full

Log in to the image container, where "a95c" is the first four digits of the container ID.

docker exec -it a95c /bin/bash

There is a statement in the file that needs to explain "COPY.". It copies the file from the host computer to the image. In the image, the current working directory has been set with "WORKDIR". Which directory is the host computer's "." (current directory)? It is not the directory where the Dockerfile file is located, but the directory where you run the "Docker build" command.

We want to copy the whole program to the image. When we run the docker command, it must be in the root directory of the program, which is the "k8sdemo" directory. But the files related to the container are all in the subdirectory of the "script" directory, so when you run the "Docker build" command, how does it find the Docekrfile? An important concept here is "build cotext" (build context), which determines the default directory of the Dockerfile. When you run "Docker build - t k8sdemo backend." to create an image, it will look for the Dockerfile file from the root directory of "build cotext". The default value is the directory where you run the docker command. But because our Dockerfile is in another directory, we need to add a "- f" option in the command to specify the location of the Dockerfile. The command is as follows. Where "- t k8sdemo backend full" indicates the image name, and the format is "name:tag". We have no tag here, only the image name.

docker build -f ./script/kubernetes/backend/docker/Dockerfile-k8sdemo-backend-full -t k8sdemo-backend-full .

For details, see Dockerfile reference

The image created in this way uses the full version of Linux system, so it is relatively large, about 1G. If you want to optimize, you need to build with multiple levels.

Multi stage builds:

In a single level build, there is only one "From" statement, while in a multi-level build, there are multiple "From", each "From" constitutes a level. For example, the following file has two "From", which is a secondary build. Each level can choose its own base image to construct its own image according to its needs. After each level of image is completed, the next level of image can choose to keep only the final files that are useful to itself in the previous level of construction, and delete all the intermediate products, which saves a lot of space. For details, see Use multi-stage builds

The following is the multi-level construction of dockerfile("Dockerfile-k8sdemo-backend").

FROM golang:latest as builder # This image is marked with "builder"
# Set the Current Working Directory inside the container
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
WORKDIR /app/cmd
# Build the Go app
#RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main.exe
RUN go build -o main.exe

######## Start a new stage from scratch #######
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
# Copy the Pre-built binary file from the previous stage
COPY --from=builder /app/cmd/main.exe . #Copy the file "/ app/cmd/main.exe" from "builder" to the current directory of this level
# Command to run the executable
CMD exec /bin/sh -c "trap : TERM INT; (while true; do sleep 1000; done) & wait"

To create a mirror:

cd /home/vagrant/jfeng45/k8sdemo/
docker build -f ./script/kubernetes/backend/docker/Dockerfile-k8sdemo-backend -t k8sdemo-backend .

Login image:

docker run -it --name k8sdemo-backend k8sdemo-backend /bin/sh

The above file divides the construction process into two parts. The first part compiles and generates the Go executable file, which uses the full version of Linux. The second part copies the executable file to a suitable directory and keeps the container running, and uses the simplified version of Linux. The commands in the first part are basically the same as the single level build instructions, and the commands in the second part will be explained later.

With this method, the space occupation is greatly reduced. The Docker image created is only 14M, but because of its simplified version of Linux (Alpine), I stepped on many pits. Let's see how these pits were filled.

Stepped pits:

1. File not found

After the image is created successfully, log in to the image:

docker run -it --name k8sdemo-backend k8sdemo-backend /bin/sh

Run the compiled Go executable "main.exe", with the following error information:

~ # ./main.exe
./main.exe not found

Go is a statically compiled language, that is to say, when compiling, the required inventory is put in the compiled program, so that no other libraries need to be dynamically linked during execution, making it very convenient to run. But this is not always the case, for example, when you use cgo (let go program call C program), you usually need to dynamically link libc Library (glibc in Linux). cgo is used in the net and os/user libraries in go. However, since the Linux version of Apline does not have a libc library, the dynamic link cannot be found at runtime, so an error is reported. It has two solutions:

  • CGO_ENABLED=0: when you add this parameter when compiling Go, cgo will not be used when compiling, which means that all libraries using cgo cannot be used. This is the easiest way, but it limits your program.
  • Using musl: musl is a lightweight libc library. The Linux version of Apline comes with its own musl library. You just need to add the following command.
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

For more information on musl, see Statically compiled Go programs, always, even with cgo, using musl

For a discussion of this error, see Installed Go binary not found in path on Alpine Linux Docker

2. Zap report wrong

Zap is a popular Go log library, which I use to output logs in my program. When the above sentence is added, the original error disappears, but there is a new one. It's produced by zap.

~ # ./main.exe
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6a37ab]

goroutine 1 [running]:
github.com/jfeng45/k8sdemo/config.initLog(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        /app/config/zap.go:94 +0x1fb
github.com/jfeng45/k8sdemo/config.RegisterLog(0x0, 0x0)
        /app/config/zap.go:42 +0x42
github.com/jfeng45/k8sdemo/config.BuildRegistrationInterface(0x751137, 0x5, 0x43ab77, 0x984940, 0xc00002c750, 0xc000074f50)
        /app/config/appConfig.go:23 +0x26
main.testRegistration()
        /app/cmd/main.go:18 +0x3a
main.main()
        /app/cmd/main.go:11 +0x20

I don't know why I made a mistake. It should be related to Musl library. It is estimated that one of the libraries used by zap is not compatible with Musl. The Logrus problem doesn't exist if I change the log to another library. It's a little bit of a shame. Zap is the best Go log library I've found so far. If you insist on using zap, you can only use full version Linux and endure large image files; or use Logrus log library instead, so you can enjoy small image files.

3. k8s deployment failed

After changing to Logrus, no more errors are reported. The program in Docker runs normally. But if you use this image to create k8s deployment, there is a problem.

Here is the command for k8s to create a deployment:

vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/backend$ kubectl get pod k8sdemo-backend-deployment-6b99dc6b8c-2fwnm
NAME                                          READY   STATUS             RESTARTS   AGE
k8sdemo-backend-deployment-6b99dc6b8c-2fwnm   0/1     CrashLoopBackOff   42         3h10m

The error message is "CrashLoopBackOff". The reason for this is that the container requires the program to run all the time. Once the operation is finished, the container will stop. k8s finds that after the container is stopped, it will redeploy the container, and then it will be stopped again, thus falling into a dead cycle.
The solution is to add the following commands to the image file:

CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait"

For details, see How can I keep a container running on Kubernetes? and My kubernetes pods keep crashing with "CrashLoopBackOff" but I can't find any log

4. Pod error

After adding the command and regenerating the image, the problem of the dead cycle was solved as expected. No Error was reported in the k8s deployment, but a new Error occurred in the Pod as follows: "the STATUS of k8sdemo-backend-deployment-6b99dc6b8c-n6bnt" is "Error".

vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/backend$ kubectl get pod
NAME                                           READY   STATUS    RESTARTS   AGE
envar-demo                                     1/1     Running   8          16d
k8sdemo-backend-deployment-6b99dc6b8c-n6bnt    0/1     Error     1          6s
k8sdemo-database-deployment-578fc88c88-mm6x8   1/1     Running   2          4d21h
nginx-deployment-77fff558d7-84z9z              1/1     Running   3          10d
nginx-deployment-77fff558d7-dh2ms              1/1     Running   3          10d

The reason is that the following command is run in the Docker file:

CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait"

But there is no '/ bin/bash' in Alpine. You need to change it to '/ bin/sh'. You need to change it to the following command:

CMD exec /bin/sh -c "trap : TERM INT; (while true; do sleep 1000; done) & wait"

After modification, k8s is deployed successfully and the program runs normally.

Source code:

github link of complete source code

Indexes

  1. Dockerfile reference
  2. Use multi-stage builds
  3. Statically compiled Go programs, always, even with cgo, using musl
  4. Installed Go binary not found in path on Alpine Linux Docker
  5. How can I keep a container running on Kubernetes?
  6. My kubernetes pods keep crashing with "CrashLoopBackOff" but I can't find any log
  7. Building Docker Containers for Go Applications

This article is based on the platform of blog one article multiple sending OpenWrite Release!

Posted by DavidP123 on Wed, 23 Oct 2019 18:04:13 -0700