Docker Containerization — A Practical Guide for Go Projects
I. Docker Core Concepts
1.1 What is Docker
Docker is an open-source containerization platform that packages applications and all their dependencies into a standardized unit (container), achieving "build once, run anywhere". For Go developers, Docker addresses the following pain points:
- Inconsistent development and production environments
- Complex dependency management (databases, caches, message queues, and other middleware)
- Non-standardized deployment process
- Difficulty in multi-service collaboration in a microservices architecture
1.2 Three Core Concepts
Image
An image is a read-only template that contains all the file system layers required to run an application. It can be compared to a "Class":
- An image is composed of multiple layers, each representing an instruction in the Dockerfile
- Layers are read-only and reusable; multiple images can share underlying layers
- Images are versioned using
tag, e.g.,golang:1.22-alpine
+---------------------------+
| 应用代码层 (COPY .) | <-- 最上层,变化最频繁
+---------------------------+
| 依赖安装层 (go mod) |
+---------------------------+
| 基础工具层 (RUN apk) |
+---------------------------+
| 基础镜像层 (alpine) | <-- 最底层,最稳定
+---------------------------+
Container
A container is a running instance of an image, comparable to an "Object":
- A container adds a writable layer on top of an image
- Each container has its own independent file system, network, and process space
- Containers are temporary and stateless; data in the writable layer is lost after destruction
- Data requiring persistence should use volumes
Registry
A registry is a service for storing and distributing images:
- Docker Hub: The official public registry, similar to GitHub
- Private Registries: Such as Harbor, AWS ECR, Alibaba Cloud ACR
- Images are identified by the format
registry/repository:tag, e.g.,docker.io/library/golang:1.22
II. Dockerfile Writing (Go Project Example)
2.1 Basic Dockerfile
Suppose we have a simple Go Web service:
// main.go
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go Docker Service!")
})
log.Printf("Server starting on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
The simplest but not recommended Dockerfile:
# 不推荐:镜像体积大,包含编译工具链
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
EXPOSE 8080
CMD ["./server"]
Images generated this way typically exceed 800MB because they include the complete Go toolchain.
2.2 Multi-stage Build
Multi-stage build is a core technique for Dockerizing Go projects, which can reduce image size from 800MB+ to 10-20MB:
# ============ 第一阶段:编译 ============
FROM golang:1.22-alpine AS builder
# 设置 Go 模块代理(国内加速)
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /app
# 先复制依赖文件,利用缓存层(详见 2.3 节)
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码并编译
COPY . .
# CGO_ENABLED=0 生成静态链接的二进制文件
# -ldflags="-s -w" 去除调试信息,减小体积
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-o /app/server .
# ============ 第二阶段:运行 ============
FROM alpine:3.19
# 安装必要的 CA 证书(HTTPS 请求需要)和时区数据
RUN apk --no-cache add ca-certificates tzdata
# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# 从 builder 阶段复制编译好的二进制文件
COPY --from=builder /app/server .
# 复制配置文件(如果有)
# COPY --from=builder /app/config ./config
# 切换到非 root 用户
USER appuser
EXPOSE 8080
# 使用 ENTRYPOINT 而非 CMD,防止被意外覆盖
ENTRYPOINT ["./server"]
More extreme solution -- using a scratch empty image:
# 编译阶段同上...
# 使用空镜像,最终镜像仅包含二进制文件
FROM scratch
# 从 builder 复制 CA 证书
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 从 builder 复制时区信息
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Image size comparison:
| Base Image | Final Size | Applicable Scenarios |
|---|---|---|
golang:1.22 |
~850MB | Not recommended for production |
alpine:3.19 |
~15MB | Recommended, supports shell debugging |
scratch |
~8MB | Extremely minimal, no shell |
distroless |
~12MB | Google recommended, good security |
2.3 Build Cache Optimization
When Docker builds, each layer is cached. As long as the input for a layer hasn't changed, the cache will be reused. The key principle is: place layers with low frequency of change first, and layers with high frequency of change later.
# 好的做法:分离依赖下载和代码编译
COPY go.mod go.sum ./ # 依赖文件变化少,缓存命中率高
RUN go mod download # 只有依赖变化时才重新下载
COPY . . # 代码频繁变化
RUN go build -o server . # 每次代码变化都重新编译
# 坏的做法:一次性复制所有文件
COPY . . # 任何文件变化都导致后续所有层缓存失效
RUN go mod download
RUN go build -o server .
2.4 Best Practices Summary
.dockerignore File
Create a .dockerignore file in the project root directory to reduce build context size:
# .dockerignore
.git
.gitignore
.idea
.vscode
*.md
README*
LICENSE
Makefile
docker-compose*.yml
Dockerfile*
tmp/
vendor/
bin/
*.test
*.prof
Security Best Practices
# 1. Use specific version tags, avoid 'latest'
FROM alpine:3.19 # 好
FROM alpine:latest # 坏 -- 不可复现
# 2. Run as a non-root user
RUN addgroup -S app && adduser -S app -G app
USER app
# 3. Read-only file system (specified at runtime)
# docker run --read-only --tmpfs /tmp myapp
# 4. Do not store secrets in the image
# Bad practice
ENV DB_PASSWORD=secret123
# Good practice: Inject at runtime via environment variables or Secret management tools
III. docker-compose Orchestration
3.1 Why docker-compose is Needed
In a microservices architecture, a Go project often depends on multiple external services. docker-compose allows defining and managing the orchestration of multiple containers using a single YAML file.
3.2 Complete Example: Go Service + MySQL + Redis + Consul
# docker-compose.yml
version: "3.9"
services:
# ============ Go Application Service ============
app:
build:
context: .
dockerfile: Dockerfile
args:
- GOPROXY=https://goproxy.cn,direct
container_name: go-app
ports:
- "8080:8080" # Host Port:Container Port
environment:
- PORT=8080
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=rootpassword
- DB_NAME=myapp
- REDIS_ADDR=redis:6379
- CONSUL_ADDR=consul:8500
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
consul:
condition: service_started
networks:
- app-network
restart: unless-stopped
# 资源限制
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
# ============ MySQL Database ============
mysql:
image: mysql:8.0
container_name: go-mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: myapp
MYSQL_CHARSET: utf8mb4
volumes:
- mysql-data:/var/lib/mysql # Data persistence
- ./deploy/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql # Initialization script
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
restart: unless-stopped
# ============ Redis Cache ============
redis:
image: redis:7-alpine
container_name: go-redis
ports:
- "6379:6379"
command: redis-server --requirepass redispassword --appendonly yes
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "redispassword", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
restart: unless-stopped
# ============ Consul Service Registration and Discovery ============
consul:
image: hashicorp/consul:1.17
container_name: go-consul
ports:
- "8500:8500" # HTTP API and Web UI
- "8600:8600/udp" # DNS
command: agent -server -bootstrap-expect=1 -ui -client=0.0.0.0
volumes:
- consul-data:/consul/data
networks:
- app-network
restart: unless-stopped
# ============ Volume Definitions ============
volumes:
mysql-data:
driver: local
redis-data:
driver: local
consul-data:
driver: local
# ============ Network Definitions ============
networks:
app-network:
driver: bridge
3.3 Common docker-compose Commands
# Start all services (run in background)
docker-compose up -d
# Start and rebuild images
docker-compose up -d --build
# View service status
docker-compose ps
# View logs for a specific service
docker-compose logs -f app
# Stop all services
docker-compose down
# Stop and remove volumes (use with caution, data will be lost)
docker-compose down -v
# Start only a specific service and its dependencies
docker-compose up -d app
# Enter a container for a specific service
docker-compose exec app sh
# Scale service instances
docker-compose up -d --scale app=3
IV. Quick Reference for Common Docker Commands
4.1 Image Management
# Build image
docker build -t myapp:v1.0 .
docker build -t myapp:v1.0 -f deploy/Dockerfile . # Specify Dockerfile path
# View local images
docker images
docker image ls
# Delete image
docker rmi myapp:v1.0
docker image prune # Clean up dangling images
docker image prune -a # Clean up all unused images
# Image tag
docker tag myapp:v1.0 registry.example.com/myapp:v1.0
# Import/Export (offline scenarios)
docker save -o myapp.tar myapp:v1.0
docker load -i myapp.tar
# View image build history / layer information
docker history myapp:v1.0
docker inspect myapp:v1.0
4.2 Container Management
# Run container
docker run -d --name myapp -p 8080:8080 myapp:v1.0
docker run -d --name myapp \
-p 8080:8080 \
-e DB_HOST=localhost \
-v /host/data:/app/data \
--restart unless-stopped \
myapp:v1.0
# View running containers
docker ps
docker ps -a # Include stopped containers
# Container lifecycle
docker start myapp
docker stop myapp
docker restart myapp
docker rm myapp # Delete stopped container
docker rm -f myapp # Force delete (including running ones)
# Enter container internal
docker exec -it myapp sh # For alpine use sh
docker exec -it myapp bash # For images with bash use bash
# View logs
docker logs myapp
docker logs -f myapp # Real-time follow
docker logs --tail 100 myapp # Last 100 lines
# View container resource usage
docker stats
docker stats myapp
# Copy files
docker cp myapp:/app/logs/app.log ./app.log # Container to host
docker cp ./config.yaml myapp:/app/config.yaml # Host to container
4.3 System Cleanup
# One-click cleanup: stopped containers, dangling images, unused networks
docker system prune
# Include unused volumes (use with caution)
docker system prune --volumes
# View Docker disk usage
docker system df
V. Docker Network Modes
5.1 Bridge Mode (Default)
Each container has its own independent network namespace, communicating via the virtual bridge docker0:
# Create custom bridge network
docker network create my-network
# Connect containers to custom network
docker run -d --name app --network my-network myapp:v1.0
docker run -d --name db --network my-network mysql:8.0
# Containers within the same network can access each other by name
# Inside the app container, "db:3306" can be used to connect to the database
Advantages of custom bridge networks:
- Containers discover each other by name (DNS)
- Better network isolation
- Ability to dynamically connect/disconnect containers
5.2 Host Mode
Containers directly use the host's network, with no network isolation:
docker run -d --network host myapp:v1.0
# Container listening on port 8080 = Host port 8080
# No -p port mapping needed
- Pros: Best performance, no NAT overhead
- Cons: Port conflict risk, lower security
- Use cases: High-performance network requirements, network debugging
5.3 None Mode
Containers have no network connectivity, completely isolated:
docker run -d --network none myapp:v1.0
- Use cases: Tasks that only require computation and no network
5.4 Network Mode Comparison
| Mode | Isolation | Performance | Use Cases |
|---|---|---|---|
| bridge | High | Medium | Default choice, most scenarios |
| host | None | High | High-performance requirements |
| none | Complete | - | Security-sensitive computational tasks |
| overlay | High | Medium | Cross-host Swarm/K8s |
VI. Volumes and Persistence
6.1 Why Volumes are Needed
A container's file system is temporary, and data is lost when the container is deleted. Volumes provide a persistent storage mechanism.
6.2 Three Mount Types
# 1. Named Volume -- Recommended for data persistence
docker volume create mydata
docker run -v mydata:/app/data myapp:v1.0
# 2. Bind Mount -- Suitable for mounting source code during development
docker run -v /host/path:/container/path myapp:v1.0
docker run -v $(pwd)/config:/app/config:ro myapp:v1.0 # Read-only mount
# 3. tmpfs Mount -- Temporary file system in memory
docker run --tmpfs /app/tmp myapp:v1.0
6.3 Volume Management
# View all volumes
docker volume ls
# View volume details
docker volume inspect mydata
# Delete volume
docker volume rm mydata
# Clean up unused volumes
docker volume prune
6.4 Typical Usage in Go Projects
# Volume usage in docker-compose.yml
services:
app:
volumes:
- ./config:/app/config:ro # Configuration files (read-only bind)
- app-logs:/app/logs # Log directory (named volume)
- /app/tmp # Anonymous volume (temporary files)
mysql:
volumes:
- mysql-data:/var/lib/mysql # Database data (named volume, most important)
volumes:
app-logs:
mysql-data:
VII. Docker's Application in Microservices Development
7.1 Local Development Workflow
In a microservices architecture, a Go service may depend on multiple other services. Docker makes local development easy:
+-----------------------------------------------+
| docker-compose orchestration |
| |
| +----------+ +----------+ +----------+ |
| | User Service | | Order Service | | Product Service | |
| | Go :8081 | | Go :8082 | | Go :8083 | |
| +----+-----+ +----+-----+ +----+-----+ |
| | | | |
| +----+--------------+--------------+----+ |
| | app-network (bridge) | |
| +----+----------+----------+-----------+ |
| | | | |
| +----+---+ +---+----+ +--+------+ |
| | MySQL | | Redis | | Consul | |
| | :3306 | | :6379 | | :8500 | |
| +--------+ +--------+ +---------+ |
+-----------------------------------------------+
7.2 Hot Reload Development Mode
During development, tools like air can be combined to achieve code hot reloading:
# docker-compose.dev.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app # Mount source code
command: air # Use air for hot reload
ports:
- "8080:8080"
# Dockerfile.dev
FROM golang:1.22-alpine
RUN go install github.com/air-verse/air@latest
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
# No need for COPY . . because source code is mounted via volume
CMD ["air"]
7.3 Integration Testing
Docker makes it easy to set up an integration testing environment:
# Start test dependencies
docker-compose -f docker-compose.test.yml up -d
# Run integration tests
go test ./... -tags=integration
# Cleanup
docker-compose -f docker-compose.test.yml down -v
You can also dynamically create test containers in Go code using the testcontainers-go library:
package repository_test
import (
"context"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/mysql"
)
func TestUserRepository(t *testing.T) {
ctx := context.Background()
// Dynamically create MySQL container for testing
mysqlC, err := mysql.Run(ctx,
"mysql:8.0",
mysql.WithDatabase("testdb"),
mysql.WithUsername("test"),
mysql.WithPassword("test"),
)
if err != nil {
t.Fatal(err)
}
defer mysqlC.Terminate(ctx)
// Get connection address
host, _ := mysqlC.Host(ctx)
port, _ := mysqlC.MappedPort(ctx, "3306")
// Use a real database for testing...
t.Logf("MySQL running at %s:%s", host, port.Port())
}
VIII. Pushing Images to Registry
8.1 Pushing to Docker Hub
# 1. Login
docker login
# 2. Tag the image (format: username/repository:tag)
docker tag myapp:v1.0 username/myapp:v1.0
docker tag myapp:v1.0 username/myapp:latest
# 3. Push
docker push username/myapp:v1.0
docker push username/myapp:latest
8.2 Pushing to a Private Registry
# Taking Alibaba Cloud ACR as an example
# 1. Login to private registry
docker login registry.cn-hangzhou.aliyuncs.com
# 2. Tag
docker tag myapp:v1.0 registry.cn-hangzhou.aliyuncs.com/myns/myapp:v1.0
# 3. Push
docker push registry.cn-hangzhou.aliyuncs.com/myns/myapp:v1.0
8.3 Setting up a Local Private Registry (Harbor)
Harbor is an enterprise-grade Docker image registry that supports features like access control and image scanning:
# Quickly deploy Harbor using Docker Compose
# After downloading the offline installation package:
./install.sh --with-chartmuseum --with-trivy
# Push to Harbor
docker tag myapp:v1.0 harbor.example.com/myproject/myapp:v1.0
docker push harbor.example.com/myproject/myapp:v1.0
8.4 Automated Build and Push in CI/CD
Automatically build and push images in GitHub Actions:
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
username/myapp:${{ github.ref_name }}
username/myapp:latest
IX. Complete Practical Workflow for Dockerizing Go Projects
9.1 Project Structure
myapp/
├── cmd/
│ └── server/
│ └── main.go # Entrypoint
├── internal/
│ ├── handler/ # HTTP Handlers
│ ├── service/ # Business Logic
│ ├── repository/ # Data Access
│ └── config/ # Configuration Management
├── deploy/
│ ├── mysql/
│ │ └── init.sql # Database Initialization
│ └── nginx/
│ └── nginx.conf # Reverse Proxy Configuration
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── docker-compose.dev.yml
├── .dockerignore
└── Makefile
9.2 Makefile Automation
# Makefile
APP_NAME=myapp
VERSION=$(shell git describe --tags --always --dirty)
REGISTRY=registry.example.com/myns
.PHONY: build run test docker-build docker-push deploy clean
# Local build
build:
CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(VERSION)" \
-o bin/$(APP_NAME) ./cmd/server
# Local run
run:
go run ./cmd/server
# Run tests
test:
go test ./... -v -cover
# Build Docker image
docker-build:
docker build -t $(APP_NAME):$(VERSION) .
docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest
# Push image
docker-push: docker-build
docker tag $(APP_NAME):$(VERSION) $(REGISTRY)/$(APP_NAME):$(VERSION)
docker push $(REGISTRY)/$(APP_NAME):$(VERSION)
# Start development environment
dev:
docker-compose -f docker-compose.dev.yml up -d --build
# Start production environment
deploy:
docker-compose up -d --build
# View logs
logs:
docker-compose logs -f app
# Cleanup
clean:
docker-compose down -v
docker image prune -f
rm -rf bin/
9.3 Complete Production-Grade Dockerfile
# ============ Stage 1: Build ============
FROM golang:1.22-alpine AS builder
# Install necessary build tools
RUN apk add --no-cache git ca-certificates tzdata
# Set up Go environment
ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /build
# Dependency cache layer
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# Compile
COPY . .
ARG VERSION=dev
RUN go build \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o /build/app \
./cmd/server
# ============ Stage 2: Run ============
FROM alpine:3.19
LABEL maintainer="your-email@example.com"
LABEL version="${VERSION}"
LABEL description="My Go Application"
# Install runtime dependencies
RUN apk --no-cache add ca-certificates tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
# Create non-root user and necessary directories
RUN addgroup -S app && adduser -S app -G app && \
mkdir -p /app/logs /app/config && \
chown -R app:app /app
WORKDIR /app
# Copy binary file
COPY --from=builder /build/app .
# Copy configuration file template (optional)
# COPY --from=builder /build/config ./config
# Switch to non-root user
USER app
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Start
ENTRYPOINT ["./app"]
9.4 Complete Workflow Summary
# 1. Write code and Dockerfile
# 2. Local development and testing
make dev # Start development environment
make test # Run tests
# 3. Build image
make docker-build # Build production image
# 4. Local verification
make deploy # Start full environment
make logs # View logs
curl http://localhost:8080/health # Test API
# 5. Push image
make docker-push # Push to registry
# 6. Deploy to server
ssh your-server
docker pull registry.example.com/myns/myapp:v1.0
docker-compose up -d
# 7. Cleanup
make clean
X. Common Issues and Troubleshooting
10.1 Common Issues
| Issue | Reason | Solution |
|---|---|---|
| Image size too large | Multi-stage build not used | Use multi-stage build |
| Slow build | Cache miss | COPY go.mod first, then COPY . |
| Incorrect timezone in container | Missing timezone data | Install tzdata, set TZ |
| HTTPS request failed | Missing CA certificates | apk add ca-certificates |
| Cannot connect to other containers | Network issue | Ensure they are in the same docker network |
| Data loss | Volume not used | Use named volume for important data |
10.2 Debugging Tips
# View container's internal file system
docker exec -it myapp sh
# View container network configuration
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myapp
# View container environment variables
docker exec myapp env
# Temporarily run a debug container (sharing network with target container)
docker run -it --rm --network container:myapp alpine sh
# View Docker build process (detailed output)
docker build --progress=plain -t myapp . 2>&1 | tee build.log
Next Article: 016 - Introduction to Kubernetes, learn how to deploy Docker containers to a K8s cluster for production-grade orchestration and management.
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6781