这篇文档从 0 到 1 讲清楚 Docker 的日常使用方式,并用一个完整项目示例演示如何用 docker-compose 管理多服务。
示例架构:
nginx: 反向代理(统一入口)
web: 前端服务(Next.js)
api: 后端服务(Hono)
postgres: 数据库
redis: 缓存
1. Docker 核心概念(先建立直觉)
Image(镜像):应用和运行环境的打包结果,类似“模板”
Container(容器):镜像运行出来的实例,类似“进程”
Dockerfile:定义如何构建镜像
Volume:持久化数据(例如 PostgreSQL 数据目录)
Network:容器之间通信网络(compose 默认会创建)
docker-compose.yml:多容器编排文件
一句话理解:Dockerfile 负责造镜像,compose 负责把多个容器组织起来运行。
2. 安装与环境检查 安装 Docker Desktop(Windows/macOS)或 Linux Docker Engine 后,先验证:
1 2 3 docker --version docker compose version docker info
如果都正常输出版本信息,说明环境可用。
3. 从构建一个 Docker 开始 先看一个最小 Node 服务(可替换为你的 Hono API)。
3.1 准备项目结构 1 2 3 4 5 6 demo-api/ package.json pnpm-lock.yaml src/index.ts Dockerfile .dockerignore
3.2 示例 Dockerfile(生产可用基础版) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 FROM node:22 -alpine AS baseWORKDIR /app RUN corepack enable FROM base AS depsCOPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile FROM deps AS buildCOPY . . RUN pnpm build FROM base AS runtimeENV NODE_ENV=productionCOPY package.json pnpm-lock.yaml ./ RUN pnpm install --prod --frozen-lockfile COPY --from=build /app/dist ./dist EXPOSE 3000 CMD ["node" , "dist/index.js" ]
3.3 .dockerignore(非常重要) 1 2 3 4 5 6 node_modules dist .git .next *.log .env
避免把无关内容打进镜像,加快构建、减小体积。
3.4 构建与运行 1 2 3 4 5 6 7 8 9 10 11 12 docker build -t demo-api:1.0.0 . docker run -d --name demo-api -p 3000:3000 demo-api:1.0.0 docker logs -f demo-api docker stop demo-api docker rm demo-api
4. Docker 常见命令速查 4.1 镜像相关 1 2 3 4 docker images docker build -t app:latest . docker rmi app:latest docker image prune -f
docker image prune -f 说明:
docker image prune:清理悬空镜像(dangling images),常见于多次构建后出现的 <none>:<none>
-f:跳过确认提示,直接执行
默认不会删除你正常打了标签并在使用的镜像
若使用 docker image prune -a -f,会删除所有“未被容器使用”的镜像,风险更高,需谨慎
4.2 容器相关 1 2 3 4 5 6 7 8 docker ps docker ps -a docker start <container> docker stop <container> docker restart <container> docker rm <container> docker logs -f <container> docker exec -it <container> sh
4.3 资源排查 1 2 3 4 docker stats docker inspect <container> docker network ls docker volume ls
4.4 清理(谨慎) 1 2 3 docker system df docker system prune -f docker system prune -a -f
5. 多服务项目示例(nginx + nextjs + hono + postgres + redis) 下面给一个可直接改造到真实项目的目录结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 my-app/ docker-compose.yml .env nginx/ default.conf web/ Dockerfile package.json ... api/ Dockerfile package.json ...
5.1 docker-compose.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 services: nginx: image: nginx:1.27-alpine container_name: app-nginx ports: - "80:80" volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - web - api restart: unless-stopped networks: - app-net web: build: context: ./web dockerfile: Dockerfile container_name: app-web environment: - NODE_ENV=production - NEXT_PUBLIC_API_BASE=/api expose: - "3000" depends_on: - api restart: unless-stopped networks: - app-net api: build: context: ./api dockerfile: Dockerfile container_name: app-api environment: - NODE_ENV=production - PORT=3001 - DATABASE_URL=postgresql://app:app123@postgres:5432/appdb - REDIS_URL=redis://redis:6379 expose: - "3001" depends_on: postgres: condition: service_healthy redis: condition: service_started restart: unless-stopped networks: - app-net postgres: image: postgres:16-alpine container_name: app-postgres environment: - POSTGRES_USER=app - POSTGRES_PASSWORD=app123 - POSTGRES_DB=appdb volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL" , "pg_isready -U app -d appdb" ] interval: 5s timeout: 3s retries: 10 restart: unless-stopped networks: - app-net redis: image: redis:7-alpine container_name: app-redis command: ["redis-server" , "--appendonly" , "yes" ] volumes: - redis_data:/data restart: unless-stopped networks: - app-net volumes: pg_data: redis_data: networks: app-net: driver: bridge
5.2 nginx 配置(nginx/default.conf) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 server { listen 80 ; server_name _; location / { proxy_pass http://web:3000; 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 ; } location /api/ { proxy_pass http://api:3001/; 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 ; } }
5.3 启动整套服务 1 2 3 4 5 6 7 8 9 docker compose up -d --build docker compose ps docker compose logs -f api docker compose logs -f web
6. 日常开发与更新流程(重点) 6.1 代码更新后重建并发布 1 2 3 4 5 6 docker compose build web api docker compose up -d web api docker compose up -d --build web api
Nginx、PostgreSQL、Redis 不会被重建,数据卷也会保留。
6.2 同步数据库(迁移) 推荐把迁移命令放在 api 容器里执行,保证环境一致:
1 2 3 4 5 docker compose exec api pnpm db:migrate docker compose run --rm api pnpm db:migrate
如果是 Prisma/Drizzle/Knex,只要把命令替换成你的实际脚本即可。
6.3 服务重启与回滚思路 1 2 3 4 5 6 7 docker compose restart api docker compose pull docker compose build --no-cache web api docker compose up -d web api
回滚常见做法:
镜像使用明确版本标签(如 api:2026.02.28-1)
compose 改回旧标签
docker compose up -d
6.4 配置变更(环境变量) 改了 .env 或 compose 中环境变量后:
若应用未自动读取新配置,重建相关服务:
1 docker compose up -d --build api web
7. 数据持久化与备份建议
PostgreSQL 数据放在 pg_data volume,不随容器销毁而丢失
Redis 如果开启 AOF(示例已开启),可提高数据恢复能力
定期备份数据库(建议至少每日)
示例备份命令:
1 docker compose exec postgres pg_dump -U app appdb > backup.sql
恢复示例:
1 docker compose exec -T postgres psql -U app -d appdb < backup.sql
8. 常见问题排查 8.1 端口冲突 报错 bind: address already in use 时,修改宿主机端口映射,例如:
8.2 服务已启动但不可访问 按顺序检查:
docker compose ps 看服务是否 Up
docker compose logs -f nginx 看代理错误
docker compose logs -f web/api 看应用报错
docker compose exec nginx sh 后 wget/curl 内网地址测试连通
8.3 数据库连接失败
检查 DATABASE_URL 中主机是否写 postgres(服务名),而不是 localhost
查看 postgres healthcheck 是否通过
确认迁移是否执行、用户权限是否正确
9. 生产环境建议(简版)
镜像固定版本,不要长期用 latest
前后端镜像分离构建,缓存依赖层提高 CI 速度
数据库和 Redis 配置合理的备份/监控/告警
使用反向代理统一做 HTTPS(可加 certbot 或外部网关)
在 CI 中执行:测试 -> 构建镜像 -> 推镜像 -> 服务器 docker compose pull && up -d
10. 生产级配置模板(可直接改造) 下面给一套偏生产实践的模板:web、api、nginx 与部署脚本。 你可以先按原样跑通,再替换为自己的镜像名、域名、迁移命令。
10.1 web(Next.js)生产 Dockerfile 建议开启 Next.js standalone 输出(next.config.js 里设置 output: "standalone")。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 FROM node:22 -alpine AS baseWORKDIR /app RUN apk add --no-cache libc6-compat && corepack enable FROM base AS depsCOPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile FROM deps AS builderCOPY . . RUN pnpm build FROM node:22 -alpine AS runnerWORKDIR /app ENV NODE_ENV=productionENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup -S nextjs && adduser -S nextjs -G nextjs COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public USER nextjsEXPOSE 3000 CMD ["node" , "server.js" ]
10.2 api(Hono)生产 Dockerfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 FROM node:22 -alpine AS baseWORKDIR /app RUN corepack enable FROM base AS depsCOPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile FROM deps AS builderCOPY . . RUN pnpm build FROM node:22 -alpine AS runnerWORKDIR /app ENV NODE_ENV=productionRUN addgroup -S app && adduser -S app -G app && corepack enable COPY package.json pnpm-lock.yaml ./ RUN pnpm install --prod --frozen-lockfile COPY --from=builder /app/dist ./dist USER appEXPOSE 3001 CMD ["node" , "dist/index.js" ]
10.3 生产级 Nginx 配置(nginx/default.conf) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 upstream web_upstream { server web:3000 ; keepalive 64 ; } upstream api_upstream { server api:3001 ; keepalive 64 ; } map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80 ; server_name _; client_max_body_size 20m ; sendfile on ; tcp_nopush on ; tcp_nodelay on ; keepalive_timeout 65 ; gzip on ; gzip_types text/plain text/css application/json application/javascript application/xml+rss image/svg+xml; gzip_min_length 1024 ; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Referrer-Policy strict-origin-when-cross-origin always; location /_next/static/ { proxy_pass http://web_upstream; proxy_http_version 1 .1 ; proxy_set_header Host $host ; expires 30d ; add_header Cache-Control "public, immutable" ; } location / { proxy_pass http://web_upstream; 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_set_header Upgrade $http_upgrade ; proxy_set_header Connection $connection_upgrade ; proxy_read_timeout 60s ; } location /api/ { proxy_pass http://api_upstream/; 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_set_header Upgrade $http_upgrade ; proxy_set_header Connection $connection_upgrade ; proxy_read_timeout 60s ; } location /healthz { return 200 "ok\n" ; add_header Content-Type text/plain; } }
10.4 推荐生产编排(docker-compose.prod.yml)
生产环境建议使用已构建并推送到仓库的镜像,不在服务器上 build。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 services: nginx: image: nginx:1.27-alpine container_name: app-nginx ports: - "80:80" volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: web: condition: service_started api: condition: service_started restart: always healthcheck: test: ["CMD-SHELL" , "wget -q -O - http://localhost/healthz || exit 1" ] interval: 10s timeout: 3s retries: 10 networks: - app-net web: image: ghcr.io/your-org/your-web:${APP_TAG} container_name: app-web env_file: - .env.production expose: - "3000" restart: always healthcheck: test: ["CMD-SHELL" , "wget -q -O - http://localhost:3000 || exit 1" ] interval: 15s timeout: 3s retries: 10 networks: - app-net api: image: ghcr.io/your-org/your-api:${APP_TAG} container_name: app-api env_file: - .env.production environment: - DATABASE_URL=postgresql://app:${POSTGRES_PASSWORD}@postgres:5432/appdb - REDIS_URL=redis://redis:6379 expose: - "3001" depends_on: postgres: condition: service_healthy redis: condition: service_started restart: always healthcheck: test: ["CMD-SHELL" , "wget -q -O - http://localhost:3001/healthz || exit 1" ] interval: 10s timeout: 3s retries: 10 networks: - app-net postgres: image: postgres:16-alpine container_name: app-postgres environment: - POSTGRES_USER=app - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=appdb volumes: - pg_data:/var/lib/postgresql/data restart: always healthcheck: test: ["CMD-SHELL" , "pg_isready -U app -d appdb" ] interval: 5s timeout: 3s retries: 20 networks: - app-net redis: image: redis:7-alpine container_name: app-redis command: ["redis-server" , "--appendonly" , "yes" ] volumes: - redis_data:/data restart: always networks: - app-net volumes: pg_data: redis_data: networks: app-net: driver: bridge
11. deploy.sh 发布脚本(生产可用基础版) 这个脚本做了几件事:
校验环境与参数
拉取最新镜像
确保数据库与缓存先起来
执行数据库迁移
更新 web/api/nginx
做健康检查,不通过则退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #!/usr/bin/env bash set -Eeuo pipefailAPP_DIR="/opt/my-app" COMPOSE_FILE="docker-compose.prod.yml" ENV_FILE=".env.production" TAG="${1:-} " if [[ -z "${TAG} " ]]; then echo "Usage: ./deploy.sh <APP_TAG>" echo "Example: ./deploy.sh 2026.02.28-1" exit 1 fi cd "${APP_DIR} " if ! command -v docker >/dev/null 2>&1; then echo "docker not found" exit 1 fi if ! docker compose version >/dev/null 2>&1; then echo "docker compose not available" exit 1 fi if [[ ! -f "${COMPOSE_FILE} " ]]; then echo "compose file not found: ${COMPOSE_FILE} " exit 1 fi if [[ ! -f "${ENV_FILE} " ]]; then echo "env file not found: ${ENV_FILE} " exit 1 fi export APP_TAG="${TAG} " echo "[1/6] Pull latest images" docker compose --env-file "${ENV_FILE} " -f "${COMPOSE_FILE} " pull web api nginx echo "[2/6] Ensure postgres/redis are up" docker compose --env-file "${ENV_FILE} " -f "${COMPOSE_FILE} " up -d postgres redis echo "[3/6] Run DB migration" docker compose --env-file "${ENV_FILE} " -f "${COMPOSE_FILE} " run --rm api pnpm db:migrate echo "[4/6] Update app services" docker compose --env-file "${ENV_FILE} " -f "${COMPOSE_FILE} " up -d web api nginx echo "[5/6] Wait for health checks" for i in {1..30}; do if curl -fsS "http://127.0.0.1/healthz" >/dev/null && \ curl -fsS "http://127.0.0.1/api/healthz" >/dev/null; then echo "Health check passed" break fi if [[ "${i} " == "30" ]]; then echo "Health check failed after retries" exit 1 fi sleep 2 done echo "[6/6] Done" docker compose --env-file "${ENV_FILE} " -f "${COMPOSE_FILE} " ps
建议把脚本权限加上:
执行方式:
1 ./deploy.sh 2026.02.28-1