Master Dockerizing Go Apps: From Local Development to Production‑Ready Deployment
This step‑by‑step guide shows how to containerize a Go web project, covering project layout, Dockerfile creation (including multi‑stage builds), Docker Compose orchestration, Makefile automation, CI/CD with GitHub Actions, performance tuning, security checks, and production deployment best practices.
Project layout
The repository follows a conventional Go module layout:
go-web-demo/
├── cmd/server/main.go # entry point
├── internal/handler/ # HTTP handlers
├── internal/service/ # business logic
├── internal/model/ # data models
├── internal/repository/ # data access
├── pkg/config/ # configuration loader
├── pkg/logger/ # structured logger
├── configs/config.yaml # default configuration file
├── scripts/build.sh # optional build helper
├── scripts/init.sql # DB initialization script
├── Dockerfile # multi‑stage build definition
├── docker-compose.yml # development compose file
├── docker-compose.prod.yml # production overrides
├── Makefile # common automation targets
├── .github/workflows/ci-cd.yml # GitHub Actions pipeline
└── README.mdMulti‑stage Dockerfile
A production‑ready Dockerfile builds the binary in a Go builder image and copies it into a minimal Alpine runtime image. It also creates a non‑root user, sets a health check, and uses CGO_ENABLED=0 for a static binary.
# ---------- Builder ----------
FROM golang:1.21-alpine AS builder
WORKDIR /build
RUN apk add --no-cache git ca-certificates tzdata
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
RUN go build -ldflags="-s -w" -trimpath -o /build/server ./cmd/server
# ---------- Runtime ----------
FROM alpine:3.19 AS runtime
RUN apk add --no-cache ca-certificates tzdata
RUN addgroup -g 1000 appgroup && \
adduser -u 1000 -G appgroup -s /bin/sh -D appuser
WORKDIR /app
COPY --from=builder /build/server /app/server
COPY configs/config.yaml /app/configs/config.yaml
COPY scripts/init.sql /app/scripts/init.sql
RUN chown -R appuser:appgroup /app && chmod +x /app/server
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
ENTRYPOINT ["/app/server"]Best‑practice checklist
Include a .dockerignore to reduce build context size.
Copy go.mod and go.sum before source files to maximise layer caching.
Combine related RUN commands to minimise the number of layers.
Consider a distroless base image for an even smaller footprint.
Pass build arguments ( VERSION, BUILD_TIME, GIT_COMMIT) to embed version information.
Docker Compose for multi‑service orchestration
The development compose file defines three core services (app, mysql, redis) and optional monitoring components (nginx, prometheus, grafana). Common environment variables and logging settings are reused via YAML anchors.
version: '3.8'
x-common-env: &common-env
TZ: Asia/Shanghai
LOG_LEVEL: info
LOG_FORMAT: json
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
VERSION: ${VERSION:-dev}
BUILD_TIME: ${BUILD_TIME:-}
GIT_COMMIT: ${GIT_COMMIT:-}
image: go-web-demo:${VERSION:-latest}
container_name: go-web-app
restart: unless-stopped
ports:
- "8080:8080"
environment:
<<: *common-env
SERVER_PORT: 8080
DATABASE_HOST: mysql
DATABASE_PORT: 3306
DATABASE_USER: root
DATABASE_PASSWORD: root123456
DATABASE_NAME: go_web_demo
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app-network
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
healthcheck:
test: ["CMD","wget","--no-verbose","--tries=1","--spider","http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
mysql:
image: mysql:8.0
container_name: go-web-mysql
restart: unless-stopped
ports:
- "3306:3306"
environment:
<<: *common-env
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: go_web_demo
volumes:
- mysql-data:/var/lib/mysql
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- app-network
healthcheck:
test: ["CMD","mysqladmin","ping","-h","localhost","-u","root","-proot123456"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: go-web-redis
restart: unless-stopped
ports:
- "6379:6379"
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
networks:
- app-network
healthcheck:
test: ["CMD","redis-cli","-a","${REDIS_PASSWORD}","ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
app-network:
driver: bridge
volumes:
mysql-data:
driver: local
redis-data:
driver: local
app-logs:
driver: localKey points:
Use depends_on with health checks to guarantee service readiness.
Expose only the application port; external traffic can be routed through an optional Nginx reverse proxy.
Define resource limits and reservations to avoid noisy‑neighbor problems.
Makefile – one‑click build, test, and deployment
.PHONY: help build run test clean docker-build docker-up docker-down deploy
APP_NAME := go-web-demo
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DOCKER_REGISTRY := your-registry.com
DOCKER_IMAGE := $(DOCKER_REGISTRY)/$(APP_NAME):$(VERSION)
help:
@echo "Available targets:"
@echo " build - compile Go binary"
@echo " run - run locally"
@echo " test - unit tests"
@echo " docker-build - build Docker image"
@echo " docker-up - start compose stack"
@echo " docker-down - stop compose stack"
@echo " deploy - build and push image"
build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)" -o bin/$(APP_NAME) ./cmd/server
run:
go run ./cmd/server
test:
go test -v -race -coverprofile=coverage.out ./...
clean:
rm -rf bin/ coverage.out
docker-build:
docker build --build-arg VERSION=$(VERSION) \
--build-arg BUILD_TIME=$(BUILD_TIME) \
--build-arg GIT_COMMIT=$(GIT_COMMIT) -t $(DOCKER_IMAGE) .
docker-up:
VERSION=$(VERSION) docker-compose up -d
docker-down:
docker-compose down
deploy: docker-build
docker push $(DOCKER_IMAGE)GitHub Actions CI/CD pipeline
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
tags: ['v*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
- uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout=5m
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
- run: go test -v -race -coverprofile=coverage.out ./...
- uses: codecov/codecov-action@v3
with:
file: ./coverage.out
build:
runs-on: ubuntu-latest
needs: test
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: docker/setup-buildx-action@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
run: |
VERSION=${{ github.ref_name }}
if [[ ${{ github.ref }} == refs/tags/* ]]; then
VERSION=${VERSION#v}
elif [[ ${{ github.ref }} == refs/heads/main ]]; then
VERSION=latest
else
VERSION=${GITHUB_SHA::8}
fi
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
VERSION=${{ steps.meta.outputs.version }}
BUILD_TIME=${{ github.event.head_commit.timestamp }}
GIT_COMMIT=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
integration-test:
runs-on: ubuntu-latest
needs: build
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: test123
MYSQL_DATABASE: test_db
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
- run: |
go test -v -tags=integration ./tests/integration/...
deploy:
runs-on: ubuntu-latest
needs: [build, integration-test]
if: startsWith(github.ref, 'refs/tags/')
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
cd /opt/go-web-demo
docker-compose pull
docker-compose up -d
docker system prune -f
- name: Health Check
run: |
sleep 10
curl -f https://your-domain.com/health || exit 1
- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v1
with:
payload: |
{"text": "Deploy ${{ github.ref_name }} ${{ job.status }}", "status": "${{ job.status }}"}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}golangci‑lint configuration
run:
timeout: 5m
tests: true
linters:
enable:
- gofmt
- govet
- errcheck
- staticcheck
- unused
- gosimple
- structcheck
- varcheck
- ineffassign
- deadcode
- typecheck
- gosec
- gocritic
- bodyclose
- whitespace
- misspell
linters-settings:
gosec:
severity: medium
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheckProduction deployment checklist
Use the multi‑stage Dockerfile with a non‑root user.
Set environment variables for timezone, log level, and GOMAXPROCS in docker-compose.prod.yml.
Define resource limits (CPU, memory) and replica count for high availability.
Expose the application only internally; front‑end traffic should go through Nginx.
Enable health checks and rolling updates via docker-compose up --no-deps --build.
Run a deployment script that backs up the current version, pulls the latest code, updates the image, and validates health before completing.
Monitoring and alerting (Prometheus & Alertmanager)
Prometheus scrapes the Go application, MySQL, and Redis. Alert rules fire on high error rates, latency spikes, or container downtime.
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'go-app'
static_configs:
- targets: ['app:8080']
metrics_path: /metrics
- job_name: 'mysql'
static_configs:
- targets: ['mysql:3306']
- job_name: 'redis'
static_configs:
- targets: ['redis:6379']
rule_files:
- "alerts.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
# monitoring/alerts.yml
groups:
- name: app_alerts
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors/sec"
- alert: HighLatency
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High latency detected"
description: "P95 latency is {{ $value }}s"
- alert: ContainerDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Container {{ $labels.instance }} is down"FAQ – common problems and solutions
Container exits immediately : The binary may have terminated or not run in foreground. Check logs with docker logs <container> and ensure the ENTRYPOINT uses exec form (e.g., ["/app/server"]).
Cannot connect to database : Verify depends_on health checks, network configuration, and that DATABASE_HOST points to the MySQL service name.
Slow image build : Optimize Dockerfile ordering, use a .dockerignore, and enable BuildKit ( DOCKER_BUILDKIT=1).
Container time is wrong : Set the TZ environment variable or mount /etc/localtime into the container.
Missing logs : Write logs to stdout/stderr and configure the Docker daemon with the json-file driver and rotation options.
Memory leak in Go app : Use pprof for profiling, adjust GOMAXPROCS, and set appropriate memory limits in Compose.
Signal not handled : Use exec‑form ENTRYPOINT so that SIGTERM is received by the Go process.
Final checklist before release
Dockerfile uses multi‑stage build and a non‑root user.
.dockerignore removes unnecessary files.
Health check is defined for the app service.
CPU and memory limits are set.
Logs are emitted to stdout in JSON format.
Graceful shutdown handling is implemented.
Image tags are immutable (e.g., v1.2.3).
Run vulnerability scanning (e.g., docker scan or Trivy).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Ray's Galactic Tech
Practice together, never alone. We cover programming languages, development tools, learning methods, and pitfall notes. We simplify complex topics, guiding you from beginner to advanced. Weekly practical content—let's grow together!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
