← Back to list

How to Dockerize Your Go Application the Right Way

Learn how to properly Dockerize Go applications using multistage builds, security best practices, and optimization techniques for…

Gopher in Stackademic · 2025-09-22 00:28 · 257 claps · 5.1 min read
#golang #docker #devops #microservices #containerization
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

How to Dockerize Your Go Application the Right Way

Containerizing your Go applications can feel like a superpower. It ensures your code runs the same way everywhere, simplifies deployment, and makes scaling a breeze. But there’s a right way to do it that can save you from headaches down the road.

In this post, I’ll walk you through how to properly Dockerize your Go applications based on my experience working with containers in production. We’ll cover everything from creating efficient Dockerfiles to avoiding common pitfalls that can lead to bloated images or security vulnerabilities.

Why Dockerize Go Applications?

Go’s ability to compile to a single binary makes it an ideal candidate for containerization. When done correctly, you can create incredibly small, secure, and efficient containers. This approach offers several benefits:

  • Consistency: Run anywhere Docker is installed with identical behavior
  • Isolation: Keep dependencies contained and avoid conflicts
  • Portability: Move easily between development, testing, and production
  • Scalability: Quickly spin up multiple instances when needed

Let’s dive into how to do this properly.

Creating a Simple Go Application

First, let’s create a basic Go application to containerize. Here’s a simple HTTP server:

package main

import (
 "fmt"
 "log"
 "net/http"
 "os"
)

func main() {
 // Get port from environment variable or default to 8080
 port := os.Getenv("PORT")
 if port == "" {
  port = "8080"
 }

 // Define a handler function for the root path
 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  log.Printf("Received request from %s for %s", r.RemoteAddr, r.URL.Path)
  fmt.Fprintf(w, "Hello, Gopher! 👋")
 })

 // Start the HTTP server
 serverAddr := ":" + port
 log.Printf("Server starting on %s", serverAddr)
 if err := http.ListenAndServe(serverAddr, nil); err != nil {
  log.Fatalf("Server failed to start: %v", err)
 }
}

This simple server listens on a configurable port (defaulting to 8080) and responds with a greeting message.

The Wrong Way to Dockerize

Before we get to the right approach, let’s look at a common mistake:

FROM golang:latest

WORKDIR /app

COPY . .

RUN go build -o app .

EXPOSE 8080

CMD ["./app"]

This Dockerfile works, but it has several issues:

  1. Uses the full Go image (1GB+) in production
  2. Includes all source code and potentially sensitive files
  3. Rebuilds dependencies on every code change
  4. Runs as root by default

The Right Way: Multi-Stage Builds

Here’s a better approach using multi-stage builds:

# Build stage
FROM golang:1.21 AS builder

# Create a working directory
WORKDIR /app

# Copy go.mod and go.sum files first to leverage Docker cache
COPY go.mod go.sum ./
# Download dependencies (will be cached if go.mod/go.sum don't change)
RUN go mod download

# Copy the source code
COPY . .

# Build the application with optimizations
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o app .

# Final stage: Use a minimal alpine image
FROM alpine:3.18

# Add CA certificates for HTTPS
RUN apk --no-cache add ca-certificates

# Create a non-root user to run the application
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set working directory
WORKDIR /app

# Copy only the binary from the build stage
COPY --from=builder /app/app .

# Use the non-root user
USER appuser

# Expose the application port
EXPOSE 8080

# Command to run the application
CMD ["./app"]

Let’s break down what makes this approach better:

  1. Multi-stage build: We use a builder stage to compile the code, then copy only the binary to a minimal Alpine image.
  2. Dependency caching: By copying and downloading dependencies first, Docker can cache this layer and speed up builds when only your code changes.
  3. Optimized binary: The build flags create a smaller, optimized binary.
  4. Security: We run as a non-root user to reduce attack surface.
  5. Minimal final image: The Alpine base image is tiny (about 5MB).

Creating a go.mod File

If you’re starting from scratch, you’ll need a go.mod file:

go mod init myapp
go mod tidy

Building and Running Your Docker Container

Now let’s build and run our container:

docker build -t mygoapp .
docker run -p 8080:8080 mygoapp

You should now be able to access your application at http://localhost:8080.

Advanced Techniques and Best Practices

1. Use Distroless Images for Even Better Security

For even more security, consider Google’s distroless images:

# Build stage
FROM golang:1.21 AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o app .

# Use Google's distroless image
FROM gcr.io/distroless/static-debian11

WORKDIR /app

COPY --from=builder /app/app .

EXPOSE 8080

# The distroless image doesn't have a shell, so use the binary directly
CMD ["/app/app"]

Distroless images contain only your application and its runtime dependencies, no package manager, shell, or other programs.

2. Properly Handle Signals

Go applications in Docker should handle termination signals properly:

package main

import (
 "context"
 "log"
 "net/http"
 "os"
 "os/signal"
 "syscall"
 "time"
)

func main() {
 // Create a server
 server := &http.Server{
  Addr: ":8080",
  // Handler configuration...
 }

 // Create a channel to listen for OS signals
 stop := make(chan os.Signal, 1)
 signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

 // Start the server in a goroutine
 go func() {
  log.Println("Server starting on :8080")
  if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
   log.Fatalf("Server error: %v", err)
  }
 }()

 // Wait for termination signal
 <-stop
 log.Println("Shutting down gracefully...")

 // Create a context with timeout for shutdown
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()

 // Attempt graceful shutdown
 if err := server.Shutdown(ctx); err != nil {
  log.Fatalf("Forced shutdown: %v", err)
 }

 log.Println("Server stopped gracefully")
}

This ensures your application shuts down cleanly when Docker sends a termination signal.

3. Use .dockerignore

Create a .dockerignore file to prevent unnecessary files from being copied:

.git
.gitignore
README.md
Dockerfile
docker-compose.yml
*.log
.env
.DS_Store

This speeds up builds and prevents sensitive information from being included in your image.

Common Pitfalls to Avoid

  1. Running as root: Always use a non-root user for security.
  2. Not handling signals: Ensure your app handles SIGTERM for graceful shutdowns.
  3. Hardcoding configurations: Use environment variables instead.
  4. Ignoring the build context: Use .dockerignore to keep the build context small.
  5. Using latest tag: Pin specific versions for reproducible builds.
  6. Not scanning for vulnerabilities: Use tools like Trivy to scan your images.
  7. Storing secrets in images: Use environment variables or secret management tools.

Real-World Use Cases

Microservices Architecture

Go’s small memory footprint and fast startup times make it perfect for microservices in Docker. I’ve worked on systems where dozens of small Go services communicate via gRPC, each in its own container, allowing independent scaling and deployment.

CI/CD Pipelines

Dockerized Go applications fit perfectly into modern CI/CD pipelines. You can build the image once and promote the exact same container through testing, staging, and production environments.

Serverless-Like Deployments

With platforms like Google Cloud Run or AWS Fargate, containerized Go applications can be deployed in a serverless fashion, scaling to zero when not in use and spinning up quickly when needed.

Conclusion

Dockerizing Go applications the right way gives you the best of both worlds: Go’s performance and efficiency combined with Docker’s consistency and portability. By using multi-stage builds, proper security practices, and optimizing your images, you can create containers that are small, secure, and production-ready.

Remember that the goal isn’t just to get your application running in a container — it’s to create a containerized application that follows best practices for security, efficiency, and maintainability. The extra effort pays off in the long run with more reliable deployments and fewer production issues.

Enjoyed this post?

I write everything here for free — no paywall, no ads. If it helped you or saved you time, consider buying me a coffee☕. It really helps me keep writing and sharing more content like this. Thanks for reading! 🙌


메타데이터
post_id
e3c3f4b5cae7
slug
how-to-dockerize-your-go-application-the-right-way-e3c3f4b5cae7
url
https://blog.stackademic.com/how-to-dockerize-your-go-application-the-right-way-e3c3f4b5cae7
canonical_url
https://blog.stackademic.com/how-to-dockerize-your-go-application-the-right-way-e3c3f4b5cae7
author_url
https://medium.com/@gane18
status
ok
fetched_at
2026-06-11 06:59:45