Introduction
Creating a Dockerfile for Golang to containerize your application is a good practice, but sometimes, you can optimize it even more by using various techniques that we will be discussing in this article.
Generic Dockerfile
Generally, you will find this common Dockerfile for Golang:
It’s always recommended to use the latest version of Golang, (but don’t use the latest tag, keep the version hardcoded)
FROM golang:1.23.1-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
CMD ["./main"]
Build the Dockerfile
docker build -t golang-app .
It’s absolutely terrible image, 490MB, even with alpine. We will gradually optimize it.
Run the Dockerfile
docker run -d -p 8080:8080 golang-app
It’s running nicely, at port 8080.
ping the healthcheck endpoint:
curl http://localhost:8080/health
we get a success response, so it’s working.
{ "status": "healthy" }
Optimizing the Dockerfile
To optimize your Dockerfile, you can use multi-stage builds, which help reduce the final image size and improve build times. Here is an optimized version of the Dockerfile:
FROM golang:1.23.1-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o main .
# Final stage
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]
- Build the Dockerfile
docker build -t golang-app .
It’s just 20.39 MB, what more can you ask for? From 490MB to 20.39MB, that’s a 95% reduction in size, not only that, there is also caching of the dependencies, so the subsequent builds are faster.
Explanation on the Dockerfile
- Multi-stage builds: By using multi-stage builds, we can separate the build environment from the runtime environment. This helps in keeping the final image lightweight.
- Alpine base image: Using
alpine:latest
as the base image for the final stage ensures a minimal footprint. - Environment variables: Setting
CGO_ENABLED=0
andGOOS=linux
ensures that the binary is statically linked and compatible with Linux. - Caching: Using multi stage builds, we can use the Docker multi stage build cache to cache the dependencies and intermediate layers, which speeds up the build process.
- LD Flags: Using
-ldflags="-w -s"
to strip the binary of debug information and symbols, which helps in reducing the size of the binary.
Additional Optimization Tips
Use .dockerignore
Create a .dockerignore
file to exclude unnecessary files from the build context, which can speed up the build process and reduce the image size.
# .dockerignore
.git
build
random_stuff
bin
dist
Make sure only the files needed for the build are included in the build context. and the assets that are needed.
Cache Dependencies
Leverage Docker’s layer caching by copying go.mod
and go.sum
files first and running go mod download
before copying the rest of the application files. This way, dependencies are only downloaded if they change.
Minimize Layers
Combine multiple RUN
commands into a single layer to reduce the number of layers in the final image.
RUN apk add --no-cache git && \
go mod download && \
go build -o main .
It does not matter much in our case if we combine RUN commands or not, I’ve just given it as an example.
Use Scratch Base Image
For the smallest possible image, you can use the scratch
base image. This is useful for simple applications that do not require any additional libraries.
# syntax=docker/dockerfile:1
# Build stage
FROM golang:1.23.1-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o main .
# Final stage
FROM scratch
COPY --from=builder /app/main .
CMD ["./main"]
Smallest is not always best, it’s important to consider the trade-offs and choose the best base image for your application.
Best Optimization for Production
The Alpine image is quite nice, and works well, but, it can start causing some problems because of how it’s DNS is configured. Please read this blog post for more information: Alpine Linux DNS Issues by Martin Heinz.
Preferably, you should use the debian-slim image, which is a minimal version of Debian, and has no such problems + the rock solid base of Debian.
# Stage 1: Install dependencies
FROM golang:1.23.1-bookworm AS deps
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
# Stage 2: Build the application
FROM golang:1.23.1-bookworm AS builder
WORKDIR /app
COPY --from=deps /go/pkg /go/pkg
COPY . .
# Enable them if you need them
# ENV CGO_ENABLED=0
# ENV GOOS=linux
RUN go build -ldflags="-w -s" -o main .
# Final stage: Run the application
FROM debian:bookworm-slim
WORKDIR /app
# Create a non-root user and group
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy the built application
COPY --from=builder /app/main .
# Change ownership of the application binary
RUN chown appuser:appuser /app/main
# Switch to the non-root user
USER appuser
CMD ["./main"]
Using the debian-slim image, we get the image of size 131 MB, which isn’t that much, and is super stable. Always prefer stability over the smallest size.
Still want to use Alpine?
If you still want to use Alpine, update the final stage of dockerfile to this
# Final stage: Run the application
FROM alpine:latest
WORKDIR /app
# Create a non-root user and group
RUN addgroup -S appuser && adduser -S appuser -G appuser
# Copy the built application
COPY --from=builder /app/main .
# Change ownership of the application binary
RUN chown appuser:appuser /app/main
# Switch to the non-root user
USER appuser
CMD ["./main"]
Using github actions?
In case you are using github actions, build the docker binary there, so that you can leverage the existing cache using the setup-go action.
name: Run CICD
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
- name: Build
run: go build -ldflags="-w -s" -o main
- name: Build Docker Image
run: |
docker build -t golang-app .
and set your Dockerfile to just the final stage, and rather than copying the binary from the builder stage, we will just copy the built binary from the github actions cache.
# Final stage: Run the application
FROM debian:bookworm-slim
WORKDIR /app
# Create a non-root user and group
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy the built application
COPY ./main /app/main
# Change ownership of the application binary
RUN chown appuser:appuser /app/main
# Switch to the non-root user
USER appuser
CMD ["/main"]
Conclusion
By following these optimization techniques, you can create a more efficient and faster Dockerfile for your Golang applications. This not only improves the performance but also reduces the size of your Docker images, making them easier to distribute and deploy.
Remember, optimizing your Dockerfile is an ongoing process. Continuously monitor and refine your Dockerfile to ensure it meets the needs of your application and deployment environment.
Never add secrets or sensitive information directly in the Dockerfile. Use environment variables or Docker secrets for sensitive information.
I’m looking for a job, if you are looking for a senior backend developer, please consider me, I’m available on my email [email protected]