Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
80: Containerizing a Go Application
I've seen this exact scenario play out in countless code reviews. A developer builds a Go service, it works perfectly on their MacBook, they write a Dockerfile, push it to a registry, and then the deployment fails with a cryptic error that makes absolutely no sense given the code.
The mysterious "no such file or directory" error
Imagine you've written a simple API to handle user profiles. You've created a Dockerfile like this:
FROM golang:1.21-alpine
WORKDIR /app
COPY . .
RUN go build -o main .
CMD ["./main"]
You build the image and run it. But instead of your API starting up, you get this:
standard_init_linux.go:211: exec user process caused "no such file or directory"
Wait, what? The file is right there. You literally just ran go build -o main .. You check the container logs, you check the Dockerfile, and everything looks correct. This is one of those Go-specific gotchas that drives people crazy because the error message is lying to you.
The problem isn't that the main binary is missing. The problem is that the binary is dynamically linked to the C library (glibc) on the build machine, but the alpine image uses a different, smaller C library called musl. When the OS tries to run your binary, it looks for the glibc loader, can't find it, and reports that the loader is missing—but it tells you the binary is missing.
Stripping dependencies and disabling CGO
To fix this, we need to tell Go to build a statically linked binary. This means the binary contains everything it needs to run, without relying on the host OS's C libraries. I always do this for containerized Go apps.
Change your build command to this:
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
Here is what's happening:
CGO_ENABLED=0: This is the magic switch. It tells Go not to use any C code, forcing it to use the pure Go implementation of things like network sockets and DNS.GOOS=linux: This ensures that even if you're building on a Mac or Windows machine, the binary is targeted for Linux.-a: This forces rebuilding of all packages to ensure everything is statically linked.
Now, the binary is a self-contained unit. It doesn't care if the OS is Alpine, Ubuntu, or even a completely empty image.
Shrinking the image with multi-stage builds
While the fix above works, your image is still huge. You're shipping the entire Go compiler and build toolchain inside your production image. That's like shipping a whole carpentry workshop just to deliver a single chair. It's a security risk (more tools for an attacker) and a waste of bandwidth.
The industry standard is the multi-stage build. We use one image to build the binary, and a second, tiny image to actually run it.
# Stage 1: The Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
# Stage 2: The Runtime
FROM scratch
COPY --from=builder /app/server /server
# If your app needs SSL/TLS (HTTPS), you'll need these certificates
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
I used scratch for the runtime stage. scratch is an empty image. It has nothing—no shell, no package manager, no libraries. Your final image will be just a few megabytes because it contains exactly one thing: your statically compiled Go binary.
Handling environment variables and ports
One final tip: don't hardcode your ports or database strings in your Go code. When you move to containers, you'll be orchestrating these via Kubernetes or Docker Compose, and you'll want to inject configuration via environment variables.
In your Go code, use os.Getenv("PORT"). In your Dockerfile, you can set a default, but it's better to leave it to the environment:
ENV PORT=8080
Remember that EXPOSE in the Dockerfile is mostly documentation. It tells the person running the container which port the app is listening on, but it doesn't actually open the port to the outside world—you still have to map it when you run the container (e.g., docker run -p 8080:8080).
📋 Practical Task
Exercise: Containerizing a Statically Linked Health-Check API
Your goal is to create a production-ready container for a Go application that provides a system health check. This exercise will test your ability to implement a multi-stage build and ensure the binary is statically linked.
Requirements:
- Create a simple Go program that starts an HTTP server on a port defined by an environment variable (default to 8080).
- The server should have one endpoint,
/health, which returns a200 OKstatus and a JSON body:{"status": "up"}. - Write a
Dockerfileusing a multi-stage build:- The first stage must use
golang:alpineto compile the binary. - The second stage must use the
scratchimage to run the binary. - You must use
CGO_ENABLED=0to ensure the binary runs onscratch.
- The first stage must use
- Ensure the final image size is under 20MB.
Verification:
Build the image and run it. Use curl localhost:8080/health to verify the response. Run docker images to check that your final image is significantly smaller than the build image.
There are no comments for now.