Why Go and Docker are the Perfect Duo?
\n\nGo (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\nIn 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\nStep 1: Write a Simple Go Application
\n\nFirst, 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:
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\nThis 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.
Step 2: First Dockerfile — Naive Approach
\n\nThe 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:
# 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\nLet's build the image and run it:
\n\ndocker build -t go-app:v1 .\ndocker run -p 8080:8080 go-app:v1\n\n\nOpen your browser and go to http://localhost:8080. It works? Great. But there's one problem: the image size. Let's check:
docker images go-app:v1\n\n\nYou'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.
Step 3: Multi-stage Build — The Secret to Lightweight Images
\n\nDocker 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.
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\nWhat happened here? We created two stages: