项目实战:从零搭建容器化部署流水线
项目目标
搭建一条最小可用的生产级部署流水线,达到:
- 代码 push 后自动构建、自动发布;
- 发布过程零停机(滚动更新);
- 支持一键回滚;
- 入口统一走 Nginx,自动 HTTPS。
整体架构
Git push
│
▼
GitHub Actions ──build──▶ 镜像仓库 (GHCR)
│ │
│ ssh deploy │ pull
▼ ▼
目标服务器 ────────▶ Docker Compose (app × 2)
│
▼
Nginx (80/443, TLS)
第 1 步:应用容器化
多阶段构建,最终镜像基于 distroless。
# build/Dockerfile
FROM golang:1.24-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app"]
配套的 .dockerignore:
.git
data
*.db
node_modules
frontend/dist
第 2 步:Compose 编排
# deploy/docker-compose.yml
services:
app:
image: ghcr.io/example/app:${TAG:-latest}
restart: unless-stopped
environment:
- APP_ENV=production
- DB_PATH=/data/app.db
volumes:
- appdata:/data
networks: [web]
deploy:
replicas: 2
update_config:
order: start-first # 先起新容器,再停旧容器 → 零停机
delay: 5s
rollback_config:
order: stop-first
healthcheck:
test: ["CMD", "/app", "-health"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
ports: ["80:80", "443:443"]
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on: [app]
networks: [web]
volumes:
appdata:
networks:
web:
第 3 步:Nginx 配置
# deploy/nginx/conf.d/site.conf
upstream app_backend {
# Docker 内置 DNS,服务名即可解析到所有副本
server app:8080;
keepalive 32;
}
server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# 安全响应头
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
client_max_body_size 20m;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
}
}
proxy_set_header Connection "" 配合 keepalive 32 可以让 Nginx 与后端保持长连接,显著降低 TIME_WAIT。
第 4 步:CI 流水线
# .github/workflows/deploy.yml
name: build-and-deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: build/Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy over SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
set -e
cd /opt/app
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
TAG=${{ github.sha }} docker compose pull
TAG=${{ github.sha }} docker compose up -d --remove-orphans
docker image prune -f
sleep 3
curl -fsS http://localhost:8080/healthz && echo "健康检查通过"
关键点:
- 用 commit sha 作为镜像 tag,回滚就是
TAG=<旧sha> docker compose up -d;
cache-from/to: type=gha 复用构建缓存,构建时间大幅下降;
- 部署后立刻做健康检查,失败则流水线标红。
第 5 步:回滚预案
# 查看历史镜像
docker images ghcr.io/example/app --format '{{.Tag}}\t{{.CreatedAt}}'
# 回滚到指定版本(10 秒内完成)
cd /opt/app && TAG=<previous-sha> docker compose up -d
回滚能成立的前提是镜像 tag 不可变且可追溯。永远不要覆盖 latest 之外的 tag。
排障检查清单
| 现象 |
排查方向 |
| 502 Bad Gateway |
docker compose ps 看 app 是否健康;upstream 名是否解析 |
| 部署后仍是旧版本 |
镜像 tag 是否用了 latest(被缓存) |
| 证书过期 |
certbot 续期容器是否在跑;renew hook 是否 reload nginx |
| 容器反复重启 |
docker compose logs --tail=100 app |
| 磁盘被镜像撑满 |
定期 docker image prune -a --filter "until=168h" |
小结
- 用 sha 做 tag 是零停机发布与快速回滚的基础。
order: start-first + healthcheck = 最小代价的滚动更新。
- Nginx 到后端保持长连接,是容易被忽略的性能细节。
- 自动化流水线的价值不在于省事,而在于每次发布都一致。