Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Thao Nguyen
Thao Nguyen

Posted on • Edited on

     

[Go Tour ] 2. Optimize Dockerfile

Simple Dockerfile

FROM golang:1.21.3-alpineWORKDIR /appCOPY . .RUNgo build-v-o /usr/local/bin/myappCMD ["myapp"]
Enter fullscreen modeExit fullscreen mode

1. Identified Issues

1.1. Image size:
Thegolang:1.21.3-alpine base image carries a lot of stuff that make the image unnecessarily big. Since Golang is a compiled language, having the compiler and its dependencies in the final image is redundant.
Taking a simple Hello World sample for a instance, the image size amounts to 327MB.

1.2. Security Concerns:
We cannot not entirely ensure what is up in thegolang:1.21.3-alpine image.

1.3. Inefficiency:
Despite Docker's layer caching, theCOPY . . instruction invalidates the cache for all the subsequent changes. Consequently, any modification will result in downloading all the dependencies and full recompilation. the simple Hello World sample image would approximately take 10 seconds to build.

```#9 [5/5] RUN go build -v -o /usr/local/bin/myapp#9 0.503 go: downloading gorm.io/gorm v1.25.5#9 0.505 go: downloading gorm.io/driver/sqlite v1.5.4#9 0.723 go: downloading github.com/mattn/go-sqlite3 v1.14.17#9 0.913 go: downloading github.com/jinzhu/now v1.1.5#9 0.913 go: downloading github.com/jinzhu/inflection v1.0.0#9 1.182 internal/goos#9 1.182 internal/itoa#9 1.182 internal/godebugs...#9 8.840 gotour/example#9 DONE 9.6s```
Enter fullscreen modeExit fullscreen mode

2. Optimizations

2.1. Use a multi-stage build to reduce the image size and improve security.

#----- Builder stage -----#FROMgolang:1.21.3-alpineasbuilderWORKDIR /app# download dependenciesRUN--mount=type=bind,source=go.mod,target=go.mod\--mount=type=bind,source=go.sum,target=go.sum\    go mod download-x# compile go binaryRUN--mount=type=bind,source=.,target=.\CGO_ENABLED=0\    go build-o /usr/local/bin/myapp ./#----- Runtime stage -----#FROMgcr.io/distroless/static-debian11asrunnerCOPY --from=builder /usr/local/bin/myapp /usr/local/bin/myappEXPOSE 8080CMD ["myapp"]#----- Dev stage -----#FROMrunnerasdev#----- Prod stage -----#FROMrunnerasprod
Enter fullscreen modeExit fullscreen mode

First, we split the Dockerfile into two main stages:builder andrunner. Thebuilder is responsible for compiling the Go binary and therunner is responsible for running the binary. Therunner uses thedistroless/static-debian11 image as its base image which is a minimal image that contains only the necessary dependencies to run a statically compiled binary.
The image size is now reduced to 13.64MB.

Second, we seperate the download of dependencies and the compilation of the binary into two separate steps. Most of the time, the dependencies do not change as often as the source code. Therefore, we can only invalidate the docker layer cache when the dependencies change. This will save us a lot of time when building the image.

...# download dependenciesRUN --mount=type=bind,source=go.mod,target=go.mod \    --mount=type=bind,source=go.sum,target=go.sum \    go mod download -x# compile go binaryRUN --mount=type=bind,source=.,target=.\    CGO_ENABLED=0 \    go build -o /usr/local/bin/myapp ./...
Enter fullscreen modeExit fullscreen mode

2.2. Use docker dedicated RUN cache

However, a drawback arises when changing dependencies (go.mod), as Docker tends to redownload all dependencies and recompile the binary, leading to inefficiency.
In prior article:



I introduced how go buildkit caches the downloaded dependencies and compiled binaries. To capitalize on this feature, we can utilize Docker's dedicated RUN cache. The Dockerfile is as follows:

# download dependencies-RUN --mount=type=bind,source=go.mod,target=go.mod \+RUN \+   --mount=type=cache,target=/go/pkg/mod \+   --mount=type=bind,source=go.mod,target=go.mod \    --mount=type=bind,source=go.sum,target=go.sum \    go mod download -x# compile go binary-RUN --mount=type=bind,source=.,target=.\+RUN \+   --mount=type=cache,target=/go/pkg/mod \+   --mount=type=cache,target=/root/.cache/go-build \+   --mount=type=bind,source=.,target=.\    CGO_ENABLED=0 \    go build -v -o /usr/local/bin/myapp ./
Enter fullscreen modeExit fullscreen mode

By adding--mount=type=cache,target=/go/pkg/mod and--mount=type=cache,target=/root/.cache/go-build to theRUN command, we can use docker dedicated RUN cache to cache the dependencies and the compiled binary. Everytimes dockerBuilderKit run theRUN command, it will mount the cache to the specified target. This will leverage the artifacts from the previous build and save us a lot of time.
For instance, I made a change intogo.mod andrepository package and re-build the image. The output is as follows:

#9[builder 3/4] RUN--mount=type=cache,target=/go/pkg/mod--mount=type=bind,source=go.mod,target=go.mod--mount=type=bind,source=go.sum,target=go.sum     go mod download-x#9 DONE 0.3s#10[builder 4/4] RUN--mount=type=cache,target=/go/pkg/mod--mount=type=cache,target=/root/.cache/go-build--mount=type=bind,source=.,target=.CGO_ENABLED=0     go build-v-o /usr/local/bin/myapp ./#10 0.556 gotour/example/internal/repository#10 0.587 gotour/example/internal/service#10 0.597 gotour/example#10 DONE 1.0s
Enter fullscreen modeExit fullscreen mode

This log discloses that the dockerBuilderKit re-executedgo mod download command but it was exceptionally fast as the go mod reused the downloaded dependencies from the cache. Similarly, thego build command was also re-executed and remarkably swift, as it only recompiled three packages (repository,service andmain) instead of the whole project along with all Go standard libraries.

2.3. Multi platforms build (Bonus)

We can also use docker multi platform build to build the image for different platforms. For instance, we can build the image forlinux/amd64 andlinux/arm64 platforms.

#----- Builder stage -----#-FROM golang:1.21.3-alpine as builder+FROM --platform=${BUILDPLATFORM} golang:1.21.3-alpine as builder+ARG TARGETOS+ARG TARGETARCHRUN \   --mount=type=cache,target=/go/pkg/mod \   --mount=type=cache,target=/root/.cache/go-build \   --mount=type=bind,source=.,target=.\    CGO_ENABLED=0 \+   GOOS=${TARGETOS} \+   GOARCH=${TARGETARCH} \    go build -v -o /app/bin/main .
Enter fullscreen modeExit fullscreen mode

3. Conclusion

In this section, we have explored how to optimize the Dockerfile for a golang application. Key takeaways include leveraging Docker's multi-stage build to diminish image size (from >300Mb to 13Mb) and enhance security. Additionally, We delved into utilizing Docker's dedicated RUN cache to speed up the build process. However, this technique is primarily effective on the Local Development Environment. For the CI/CD pipeline, we need a more advanced technique to enable the cache across separate builds.

In the upcoming section, we will delve into configuring hot-reload in local development environment for a Golang application.

Thank you for reading.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Joined

More fromThao Nguyen

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp