Docker-10 实战项目与生产发布:Go + PostgreSQL + Redis + Nginx
1. 项目目标和边界
我们构建一个典型的 Web 服务:
浏览器 → Nginx(入口/TLS/静态文件)
↓
Go API(无状态)
↙ ↘
PostgreSQL(持久数据) Redis(缓存/队列)
Docker Compose 负责本地和单机测试环境;生产多副本、自动调度和故障迁移可以把同样的镜像交给 Kubernetes 或云平台。
2. 推荐目录
demo/
├── cmd/server/main.go
├── internal/
├── migrations/
│ └── 001_init.sql
├── web/
│ └── static/
├── nginx/
│ └── default.conf
├── Dockerfile
├── .dockerignore
├── compose.yaml
├── compose.dev.yaml
├── compose.prod.yaml
├── .env.example
└── Makefile
应用至少提供:
GET /healthz:进程和基本依赖可用性。GET /readyz:可以接收流量,数据库迁移和关键依赖已就绪。GET /metrics:Prometheus 指标(生产可放在内部网络)。- 收到 SIGTERM 后停止接收新请求、等待在途请求、关闭数据库和 Redis 连接。
3. 生产风格 Dockerfile
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
RUN apk add --no-cache ca-certificates git
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot AS runtime
COPY --from=build /out/server /server
COPY --from=build /src/migrations /migrations
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
.dockerignore:
.git
.env
*.log
tmp/
coverage/
node_modules/
Dockerfile*
compose*.yaml
构建和本地验证:
docker buildx build --load -t demo/api:dev .
docker run --rm --entrypoint /server demo/api:dev --help
docker image inspect demo/api:dev
4. Compose 完整环境
name: demo
services:
api:
image: demo/api:${APP_VERSION:-dev}
build:
context: .
target: runtime
environment:
APP_ENV: ${APP_ENV:-dev}
HTTP_ADDR: :8080
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app?sslmode=disable
REDIS_ADDR: redis:6379
expose:
- "8080"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
restart: unless-stopped
networks: [app, data]
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
networks: [data]
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
networks: [app]
nginx:
image: nginx:1.27-alpine
ports:
- "127.0.0.1:8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./web/static:/usr/share/nginx/html:ro
depends_on:
api:
condition: service_started
networks: [edge, app]
volumes:
pgdata:
redisdata:
networks:
edge: {}
app: {}
data:
internal: true
如果 Docker Compose 版本不接受某个资源字段,以 docker compose config 输出和实际启动结果为准;生产资源限制应在真正的编排平台再次声明。
5. Nginx 反向代理
server {
listen 80;
server_name _;
location /static/ {
alias /usr/share/nginx/html/;
add_header Cache-Control "public, max-age=3600";
}
location / {
proxy_pass http://api:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
}
}
api 是 Compose 服务名,Nginx 通过内部网络 DNS 解析它。公网部署时还要在入口处理 TLS、请求体大小、超时、限流和真实客户端 IP 信任边界。
6. 启动、迁移和验收
cp .env.example .env
# 编辑 .env,至少设置 POSTGRES_PASSWORD
docker compose config
docker compose build --pull
docker compose up -d db redis
# 迁移容器应使用与 API 相同的镜像和版本
docker compose run --rm api /server migrate up
docker compose up -d api nginx
docker compose ps
curl -fsS http://127.0.0.1:8080/healthz
curl -fsS http://127.0.0.1:8080/readyz
docker compose logs --since 10m api
迁移要可重复、可审计、可回滚。不要让每个 API 副本在启动时同时执行不可幂等的 DDL;使用单独的迁移 job 或分布式锁。
7. CI/CD 示例
name: image
on:
push:
tags: ["v*.*.*"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/acme/demo-api:${{ github.ref_name }}
labels: org.opencontainers.image.revision=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
实际流水线还应加入测试、镜像扫描、SBOM、签名和部署审批。发布记录至少包含源码 commit、镜像 digest、配置版本、数据库迁移版本和操作者。
8. 单机生产发布策略
# 1. 拉取指定版本
docker compose pull api nginx
# 2. 先运行迁移(必要时在维护窗口执行)
docker compose run --rm api /server migrate up
# 3. 重建 API,保持数据库和缓存在线
docker compose up -d --no-deps api
# 4. 验证健康、错误率和延迟
docker compose ps
curl -fsS http://127.0.0.1:8080/readyz
# 5. 异常时回滚镜像版本并重启 API
APP_VERSION=previous docker compose up -d --no-deps api
单机 Compose 的更新通常是重建/替换,不是真正的无损滚动发布。需要多个副本、连接排空和自动回滚时,应把镜像和健康检查交给 Kubernetes、Nomad 或云编排平台。
9. 备份、恢复和灾难演练
# PostgreSQL 逻辑备份
docker compose exec -T db pg_dump -U app -Fc app > "backup/app-$(date +%F).dump"
# Redis 持久化数据不是数据库备份的替代品;按业务决定是否备份
docker compose exec db pg_isready -U app
备份要加密、异地保存、限制访问并定期恢复到隔离环境。验证 RPO(最多丢多少数据)和 RTO(多久恢复)是否达到目标;只检查“备份文件存在”不算演练。
10. 故障演练清单
API 反复重启
docker compose ps
docker compose logs --tail 200 api
docker inspect demo-api-1 --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
检查环境变量、entrypoint、依赖 DNS、端口、OOM 和迁移状态。
API 连不上数据库
docker compose exec api getent hosts db
docker compose exec api sh -c 'nc -vz db 5432'
docker compose exec db pg_isready -U app
检查是否错误使用 localhost、数据库网络是否与 API 共享、健康检查是否只表达进程存活。
磁盘占满
docker system df -v
df -h
docker events --since 1h
分别统计镜像、BuildKit、volume 和日志;不要未经确认执行 down -v 或 system prune --volumes。
11. 配置、版本和秘密的发布契约
生产发布时要把三类东西分开:
| 内容 | 进入哪里 | 是否进入镜像 |
|---|---|---|
| 应用二进制和静态资源 | 镜像 | 是 |
| 非秘密默认配置 | 配置文件/环境变量 | 通常否,或只保留安全默认值 |
| 密码、token、证书私钥 | secret manager/受限挂载 | 否 |
同一个镜像应该能在 dev、staging、prod 运行,只由配置决定连接地址、日志级别、资源和入口。不要为每个环境重新编译一份“生产镜像”,否则漏洞修复和回滚会变成多条分叉链路。
发布记录至少保存:
application version
image digest(每个架构)
source revision
Dockerfile/Compose 配置 revision
database migration version
配置/secret 版本
审批人、操作者、开始/结束时间
健康检查和回滚结果
12. 数据库迁移与兼容发布
镜像切换和数据库 schema 变更不能互相假设“同时完成”。推荐 expand/contract:
旧应用 A
↓
先添加向后兼容的列/表/索引(expand)
↓
发布同时支持新旧 schema 的应用 B
↓
回填、观察、切换读写
↓
确认无旧版本后删除旧字段(contract)
迁移 job 要求:
- 使用和目标镜像一致的迁移二进制和数据库客户端。
- 获取单实例锁,避免多个副本同时执行不可幂等 DDL。
- 每一步有超时、日志、版本记录和失败处理。
- 大表迁移分批、可暂停,避免锁住线上请求。
- 回滚应用前确认 schema 对旧版本仍兼容;不能只回滚镜像 tag。
13. 单机 Compose 的发布脚本骨架
#!/usr/bin/env bash
set -Eeuo pipefail
project=demo
version="${APP_VERSION:?APP_VERSION is required}"
export APP_VERSION="$version"
docker context show
docker compose -p "$project" -f compose.yaml config >/tmp/demo.compose.rendered.yaml
docker compose -p "$project" -f compose.yaml pull api nginx
# 迁移前先验证数据库健康和备份状态
docker compose -p "$project" -f compose.yaml ps db
docker compose -p "$project" -f compose.yaml run --rm --no-deps api /server migrate up
# 只替换无状态 API,保留 db/redis volume
docker compose -p "$project" -f compose.yaml up -d --no-deps api nginx
for i in $(seq 1 30); do
curl -fsS http://127.0.0.1:8080/readyz >/dev/null && exit 0
sleep 2
done
echo "readiness failed; inspect logs and roll back" >&2
docker compose -p "$project" -f compose.yaml logs --tail 200 api nginx
exit 1
脚本还应在真实环境加入锁(避免并发发布)、超时、通知、审计输出、旧版本保留和明确回滚命令。不要用 latest 或当前目录中的未提交源码作为发布输入。
14. 备份与恢复 runbook
发现异常/数据损坏
↓
停止写入或摘除入口
↓
确认最后一个可验证备份和目标 RPO
↓
恢复到隔离 volume/实例
↓
运行 schema、行数、校验和和关键业务查询
↓
切换连接/入口,观察错误率和延迟
↓
保留原现场,记录 RTO、丢失数据和后续修复
不要边恢复边覆盖唯一备份。备份文件、secret、数据库日志和恢复主机都可能包含敏感数据,需要单独的权限、加密和销毁策略。
15. 生产上线前审阅
- 镜像 digest 与源码 revision 可追溯,镜像已扫描并生成 SBOM。
- API、Nginx、数据库和 Redis 的网络边界已画清;只有入口发布端口。
- API 非 root、只读根、临时目录和写入 volume 都有明确原因。
- restart policy、healthcheck、readiness、SIGTERM 和超时经过实验。
- 数据库迁移能前向兼容,回滚不依赖破坏性 DDL。
- 日志/指标/告警已验证,磁盘、OOM、DNS、证书过期有 runbook。
- 备份恢复和单机/节点故障演练完成,RPO/RTO 达标。
- 发布脚本明确 context、项目名、锁、超时、回滚和审批。
16. 本篇小结
一个可上线的容器项目不止是“写好 Dockerfile”:还要有清晰的服务边界、可复现的 Compose、健康检查、优雅退出、迁移策略、镜像供应链、日志指标、备份恢复和回滚路径。把这些内容放进仓库,换一台机器才能真正复现整个系统。