Containerization of Go Applications: Docker from Scratch to Production

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Go and Docker are the Perfect Duo?

\n\n

Go (or Golang) is a language that was designed from the ground up for modern cloud architectures. Even before Docker became the de facto standard, Go could already compile into a static binary that requires no dependencies. And Docker is a technology that allows you to package an application with all its environment into a lightweight container. When you combine these two technologies, you get a nearly perfect solution: minimal image size, lightning-fast startup, and a complete absence of dependency conflicts.

\n\n

In this article, we won't lecture you on the theory of containerization. Instead, we'll write a simple Go application, build it into a Docker image, optimize it using multi-stage builds, and ensure our container is ready for production. All of this — with real code examples you can run right now.

\n\n

Step 1: Write a Simple Go Application

\n\n

First, let's create a simple HTTP server in Go. This will be our "test subject" program. Create a file main.go with the following content:

\n\n
package main\n\nimport (\n    "fmt"\n    "log"\n    "net/http"\n    "os"\n)\n\nfunc main() {\n    port := os.Getenv("PORT")\n    if port == "" {\n        port = "8080"\n    }\n\n    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {\n        fmt.Fprintf(w, "Hello from Go container! 🐹")\n    })\n\n    log.Printf("Server started on port %s", port)\n    log.Fatal(http.ListenAndServe(":"+port, nil))\n}\n
\n\n

This code is classic: it listens on a port (which can be set via the PORT environment variable) and responds to any request with a simple greeting. Now we want this application to run in Docker, regardless of the operating system the container is running on.

\n\n

Step 2: First Dockerfile — Naive Approach

\n\n

The simplest way to "containerize" a Go application is to take the full Go image, copy the code into it, and run it. Create a file Dockerfile:

\n\n
# Use the official Go image\nFROM golang:1.22\n\n# Set the working directory\nWORKDIR /app\n\n# Copy project files\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\n\n# Build the application\nRUN go build -o main .\n\n# Run\nCMD ["./main"]\n
\n\n

Let's build the image and run it:

\n\n
docker build -t go-app:v1 .\ndocker run -p 8080:8080 go-app:v1\n
\n\n

Open your browser and go to http://localhost:8080. It works? Great. But there's one problem: the image size. Let's check:

\n\n
docker images go-app:v1\n
\n\n

You'll see something like 1.2 GB. This is a disaster! The image contains a full Go compiler, package manager, system utilities — everything that is not needed to run the finished binary. In production, such an image would take forever to load and take up a lot of space.

\n\n

Step 3: Multi-stage Build — The Secret to Lightweight Images

\n\n

Docker supports multi-stage builds. This is when you use multiple FROM instructions in one Dockerfile, and only the last stage goes into the final image. The first stage — the "build" stage — contains the compiler and all tools. The second stage is minimal, for example, scratch (empty image) or alpine.

\n\n

Let's rewrite our Dockerfile:

\n\n
# Stage 1: Build\nFROM golang:1.22 AS builder\n\nWORKDIR /app\n\n# Cache dependencies\nCOPY go.mod go.sum ./\nRUN go mod download\n\n# Copy source code and build\nCOPY . .\nRUN CGO_ENABLED=0 GOOS=linux go build -o main .\n\n# Stage 2: Final image\nFROM alpine:latest\n\nRUN apk --no-cache add ca-certificates\n\nWORKDIR /root/\n\n# Copy only the binary from the first stage\nCOPY --from=builder /app/main .\n\nEXPOSE 8080\n\nCMD ["./main"]\n
\n\n

What happened here? We created two stages:

Blogs

Book Recommendations