← Blog archive

Best Dockerfile for Golang, Optimize Your Dockerfile

Create best Dockerfile for Golang, optimize your Dockerfile for Golang and make it blazingly fast! 🔥

Best Dockerfile for Golang, Optimize Your Dockerfile

Introduction

The usual Go Dockerfile works, but it often ships the compiler, build cache, and debug symbols with the application. We can remove most of that with a multi-stage build, dependency caching, and a stripped binary.

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)

DOCKERFILECode
FROM golang:1.24-alpine

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY . .

RUN go build -o main .

CMD ["./main"]

Build the Dockerfile

ShellCode
docker build -t golang-app .
Docker Build

It's absolutely terrible image, 490MB, even with alpine. We will gradually optimize it.

Run the Dockerfile

ShellCode
docker run -d -p 8080:8080 golang-app
Docker Run

It's running nicely, at port 8080.

ping the healthcheck endpoint:

ShellCode
curl http://localhost:8080/health

we get a success response, so it's working.

JSONCode
{ "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:

DOCKERFILECode
FROM golang:1.24-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
ShellCode
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: The build environment stays separate from the runtime image, so the final image only needs the compiled binary.
  • 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.

PLAINTEXTCode
# .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.

DOCKERFILECode
RUN apk add --no-cache git && \
    go mod download && \
    go build -o main .

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.

DOCKERFILECode
# syntax=docker/dockerfile:1
# Build stage
FROM golang:1.24-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"]

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.

DOCKERFILECode
# Stage 1: Install dependencies
FROM golang:1.24-bookworm AS deps

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

# Stage 2: Build the application
FROM golang:1.24-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

DOCKERFILECode
# 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.

YAMLCode
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.24"
      - 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.

DOCKERFILECode
# 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

The final image is smaller, faster to pull, and no longer ships the Go toolchain. Pin the base images, keep dependency layers cacheable, and run the binary as a non-root user.

Discussion