目录

Docker-12 实战项目与生产发布: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
    restart: unless-stopped
    mem_limit: 512m
    cpus: 1.0
    pids_limit: 256
    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
    restart: unless-stopped
    mem_limit: 256m
    cpus: 0.5
    pids_limit: 128
    networks: [data]

  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
    restart: unless-stopped
    mem_limit: 128m
    cpus: 0.5
    pids_limit: 128
    networks: [edge, app]

volumes:
  pgdata:
  redisdata:

networks:
  edge: {}
  app: {}
  data:
    internal: true

网络设计说明:Nginx 只在 edgeapp 网络,可以转发到 API,但不能直接访问 Redis 和 PostgreSQL;API 在 appdata 网络,可以访问 Redis 和 DB;Redis 和 DB 都在 data 网络(internal: true),无法直接出网。每个服务都配置了 restart、内存、CPU 和 PIDs 限制,符合前面 09 篇的上线检查表。

如果 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 -vsystem 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、健康检查、优雅退出、迁移策略、镜像供应链、日志指标、备份恢复和回滚路径。把这些内容放进仓库,换一台机器才能真正复现整个系统。


上一篇 下一篇
11-安全性能与可观测性 13-面试题:基础与命令