7 min read
Updated at -

Best Dockerfile for Golang, Optimize Your Dockerfile

Table of Contents
Best Dockerfile for Golang, Optimize Your Dockerfile

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 .
Docker Build

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
Docker Run

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 .
Optimized Build

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 and GOOS=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"]
Debian Build

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]

  • docker
  • golang
  • dockerfile
  • optimization
  • best-practices