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 processes
- 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 consists of multiple layers, each representing an instruction in the Dockerfile
- Layers are read-only and reusable; multiple images can share underlying layers
- Images are version-managed 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 upon destruction
- Data requiring persistence should use data volumes (Volume)
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. Writing a Dockerfile (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 builds are a core technique for Dockerizing Go projects, capable of reducing 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 | Use Case |
|---|---|---|
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
During 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 change frequency first, and layers with high change frequency 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 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. 使用特定版本标签,避免用 latest
FROM alpine:3.19 # 好
FROM alpine:latest # 坏 -- 不可复现
# 2. 以非 root 用户运行
RUN addgroup -S app && adduser -S app -G app
USER app
# 3. 只读文件系统(运行时指定)
# docker run --read-only --tmpfs /tmp myapp
# 4. 不要在镜像中存储密钥
# 坏的做法
ENV DB_PASSWORD=secret123
# 好的做法:运行时通过环境变量或 Secret 管理工具注入
III. Docker Compose Orchestration
3.1 Why Docker Compose is Needed
In a microservices architecture, a Go project typically 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 应用服务 ============
app:
build:
context: .
dockerfile: Dockerfile
args:
- GOPROXY=https://goproxy.cn,direct
container_name: go-app
ports:
- "8080:8080" # 主机端口:容器端口
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 数据库 ============
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 # 数据持久化
- ./deploy/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化脚本
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
restart: unless-stopped
# ============ Redis 缓存 ============
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 服务注册与发现 ============
consul:
image: hashicorp/consul:1.17
container_name: go-consul
ports:
- "8500:8500" # HTTP API 和 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
# ============ 数据卷定义 ============
volumes:
mysql-data:
driver: local
redis-data:
driver: local
consul-data:
driver: local
# ============ 网络定义 ============
networks:
app-network:
driver: bridge
3.3 Common Docker Compose Commands
# 启动所有服务(后台运行)
docker-compose up -d
# 启动并重新构建镜像
docker-compose up -d --build
# 查看服务状态
docker-compose ps
# 查看某个服务的日志
docker-compose logs -f app
# 停止所有服务
docker-compose down
# 停止并删除数据卷(慎用,会丢失数据)
docker-compose down -v
# 只启动某个服务及其依赖
docker-compose up -d app
# 进入某个服务的容器
docker-compose exec app sh
# 扩展服务实例数
docker-compose up -d --scale app=3
IV. Quick Reference for Common Docker Commands
4.1 Image Management
# 构建镜像
docker build -t myapp:v1.0 .
docker build -t myapp:v1.0 -f deploy/Dockerfile . # 指定 Dockerfile 路径
# 查看本地镜像
docker images
docker image ls
# 删除镜像
docker rmi myapp:v1.0
docker image prune # 清理悬空镜像(dangling images)
docker image prune -a # 清理所有未使用的镜像
# 镜像标签
docker tag myapp:v1.0 registry.example.com/myapp:v1.0
# 导入导出(离线场景)
docker save -o myapp.tar myapp:v1.0
docker load -i myapp.tar
# 查看镜像构建历史 / 层信息
docker history myapp:v1.0
docker inspect myapp:v1.0
4.2 Container Management
# 运行容器
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
# 查看运行中的容器
docker ps
docker ps -a # 包含已停止的容器
# 容器生命周期
docker start myapp
docker stop myapp
docker restart myapp
docker rm myapp # 删除已停止的容器
docker rm -f myapp # 强制删除(包括运行中的)
# 进入容器内部
docker exec -it myapp sh # alpine 用 sh
docker exec -it myapp bash # 带 bash 的镜像用 bash
# 查看日志
docker logs myapp
docker logs -f myapp # 实时跟踪
docker logs --tail 100 myapp # 最后100行
# 查看容器资源使用
docker stats
docker stats myapp
# 复制文件
docker cp myapp:/app/logs/app.log ./app.log # 容器到主机
docker cp ./config.yaml myapp:/app/config.yaml # 主机到容器
4.3 System Cleanup
# 一键清理:停止的容器、悬空镜像、未使用的网络
docker system prune
# 包含未使用的数据卷(慎用)
docker system prune --volumes
# 查看 Docker 磁盘使用情况
docker system df
V. Docker Network Modes
5.1 Bridge Mode (Default)
Each container has its own independent network namespace and communicates via the virtual bridge docker0:
# 创建自定义 bridge 网络
docker network create my-network
# 将容器连接到自定义网络
docker run -d --name app --network my-network myapp:v1.0
docker run -d --name db --network my-network mysql:8.0
# 同一网络内的容器可以通过容器名互相访问
# app 容器内可以用 "db:3306" 连接数据库
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
# 容器中监听 8080 端口 = 宿主机 8080 端口
# 不需要 -p 端口映射
- 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 connection, completely isolated:
docker run -d --network none myapp:v1.0
- Use cases: Tasks that only require computation and no network access
5.4 Network Mode Comparison
| Mode | Isolation | Performance | Use Case |
|---|---|---|---|
| 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. Data Volumes and Persistence
6.1 Why Data Volumes are Needed
A container's file system is temporary, and data is lost when the container is deleted. Data volumes provide a persistent storage mechanism.
6.2 Three Mount Types
# 1. 命名卷(Named Volume)-- 推荐用于数据持久化
docker volume create mydata
docker run -v mydata:/app/data myapp:v1.0
# 2. 绑定挂载(Bind Mount)-- 适合开发时挂载源代码
docker run -v /host/path:/container/path myapp:v1.0
docker run -v $(pwd)/config:/app/config:ro myapp:v1.0 # 只读挂载
# 3. tmpfs 挂载 -- 内存中的临时文件系统
docker run --tmpfs /app/tmp myapp:v1.0
6.3 Data Volume Management
# 查看所有卷
docker volume ls
# 查看卷详情
docker volume inspect mydata
# 删除卷
docker volume rm mydata
# 清理未使用的卷
docker volume prune
6.4 Typical Usage in Go Projects
# docker-compose.yml 中的卷使用
services:
app:
volumes:
- ./config:/app/config:ro # 配置文件(只读绑定)
- app-logs:/app/logs # 日志目录(命名卷)
- /app/tmp # 匿名卷(临时文件)
mysql:
volumes:
- mysql-data:/var/lib/mysql # 数据库数据(命名卷,最重要)
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 # 挂载源代码
command: air # 使用 air 热重载
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
# 不需要 COPY . . 因为源代码通过 volume 挂载
CMD ["air"]
7.3 Integration Testing
Docker can easily set up an integration testing environment:
# 启动测试依赖
docker-compose -f docker-compose.test.yml up -d
# 运行集成测试
go test ./... -tags=integration
# 清理
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()
// 动态创建 MySQL 容器用于测试
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)
// 获取连接地址
host, _ := mysqlC.Host(ctx)
port, _ := mysqlC.MappedPort(ctx, "3306")
// 使用真实数据库进行测试...
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. 给镜像打标签(格式:用户名/仓库名:标签)
docker tag myapp:v1.0 username/myapp:v1.0
docker tag myapp:v1.0 username/myapp:latest
# 3. 推送
docker push username/myapp:v1.0
docker push username/myapp:latest
8.2 Pushing to a Private Registry
# 以阿里云 ACR 为例
# 1. Login私有仓库
docker login registry.cn-hangzhou.aliyuncs.com
# 2. 打标签
docker tag myapp:v1.0 registry.cn-hangzhou.aliyuncs.com/myns/myapp:v1.0
# 3. 推送
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:
# 使用 Docker Compose 快速部署 Harbor
# 下载离线安装包后:
./install.sh --with-chartmuseum --with-trivy
# 推送到 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
# 本地编译
build:
CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(VERSION)" \
-o bin/$(APP_NAME) ./cmd/server
# 本地运行
run:
go run ./cmd/server
# 运行测试
test:
go test ./... -v -cover
# 构建 Docker 镜像
docker-build:
docker build -t $(APP_NAME):$(VERSION) .
docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest
# 推送镜像
docker-push: docker-build
docker tag $(APP_NAME):$(VERSION) $(REGISTRY)/$(APP_NAME):$(VERSION)
docker push $(REGISTRY)/$(APP_NAME):$(VERSION)
# 启动开发环境
dev:
docker-compose -f docker-compose.dev.yml up -d --build
# 启动生产环境
deploy:
docker-compose up -d --build
# 查看日志
logs:
docker-compose logs -f app
# 清理
clean:
docker-compose down -v
docker image prune -f
rm -rf bin/
9.3 Complete Production-Grade Dockerfile
# ============ 第一阶段:编译 ============
FROM golang:1.22-alpine AS builder
# 安装必要的构建工具
RUN apk add --no-cache git ca-certificates tzdata
# 设置 Go 环境
ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /build
# 依赖缓存层
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# 编译
COPY . .
ARG VERSION=dev
RUN go build \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o /build/app \
./cmd/server
# ============ 第二阶段:运行 ============
FROM alpine:3.19
LABEL maintainer="your-email@example.com"
LABEL version="${VERSION}"
LABEL description="My Go Application"
# 安装运行时依赖
RUN apk --no-cache add ca-certificates tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
# 创建非 root 用户和必要目录
RUN addgroup -S app && adduser -S app -G app && \
mkdir -p /app/logs /app/config && \
chown -R app:app /app
WORKDIR /app
# 复制二进制文件
COPY --from=builder /build/app .
# 复制配置文件模板(可选)
# COPY --from=builder /build/config ./config
# 切换到非 root 用户
USER app
# 暴露端口
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"]
9.4 Complete Workflow Summary
# 1. 编写代码和 Dockerfile
# 2. 本地开发和测试
make dev # 启动开发环境
make test # 运行测试
# 3. 构建镜像
make docker-build # 构建生产镜像
# 4. 本地验证
make deploy # 启动完整环境
make logs # 查看日志
curl http://localhost:8080/health # 测试接口
# 5. 推送镜像
make docker-push # 推送到仓库
# 6. 部署到服务器
ssh your-server
docker pull registry.example.com/myns/myapp:v1.0
docker-compose up -d
# 7. 清理
make clean
X. Common Issues and Troubleshooting
10.1 Common Issues
| Issue | Cause | 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 connectivity issue | Ensure they are in the same docker network |
| Data loss | Volume not used | Use named volumes for important data |
10.2 Debugging Tips
# 查看容器内部文件系统
docker exec -it myapp sh
# 查看容器网络配置
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myapp
# 查看容器环境变量
docker exec myapp env
# 临时运行一个调试容器(和目标容器共享网络)
docker run -it --rm --network container:myapp alpine sh
# 查看 Docker 构建过程(详细输出)
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/6762