Docker Compose V2 实战指南:从零搭建较完整的 Web 应用栈

说明: 本文为配置思路与示例整理,不代表作者已在自己的服务器上逐项验证全部命令。执行涉及公网暴露、账户权限、数据删除或服务重启的操作前,请先备份,并结合官方文档与实际环境核验。

本文通过一个完整的实战案例,带你从零开始使用 Docker Compose V2 部署一个包含 Nginx、Node.js、PostgreSQL 和 Redis 的较完整的 Web 应用栈。包含配置详解、健康检查、数据持久化和常用运维命令。

前言

在实际项目中,一个完整的 Web 应用通常需要多个服务协同工作:Web 服务器、应用服务器、数据库、缓存……手动逐个启动这些服务不仅繁琐,而且容易出错。Docker Compose 正是解决这一痛点的利器。

本文将带你从零搭建一个较完整的的多容器应用栈,不是 Hello World,而是你可以直接用在真实项目中的配置。

最终效果

完成本文后,你将拥有:

  • Nginx 反向代理 + 静态文件服务
  • Node.js 应用服务(带健康检查)
  • PostgreSQL 数据库(带持久化和初始化脚本)
  • Redis 缓存(带密码认证和持久化)

项目结构

TEXT
myapp/
├── docker-compose.yml
├── nginx/
│   └── nginx.conf
├── app/
│   ├── Dockerfile
│   └── src/
│       └── index.js
├── db/
│   └── init/
│       └── 01-schema.sql
└── .env

第一步:环境变量配置

创建 .env 文件管理所有敏感配置:

BASH
# .env
POSTGRES_USER=appuser
POSTGRES_PASSWORD=changeme_strong_password_here
POSTGRES_DB=myapp_db
REDIS_PASSWORD=redis_secret_here
APP_PORT=3000
NODE_ENV=production

⚠️ 安全提示.env 文件不要提交到 Git 仓库,务必在 .gitignore 中添加 .env

第二步:编写 Docker Compose 配置

这是核心配置,每个细节都有注释说明:

YAML
# docker-compose.yml
services:
  # ─── Nginx 反向代理 ───
  nginx:
    image: nginx:1.27-alpine
    container_name: myapp-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - app_static:/app/static:ro
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - frontend

  # ─── Node.js 应用 ───
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    container_name: myapp-node
    environment:
      - NODE_ENV=${NODE_ENV}
      - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
      - PORT=3000
    volumes:
      - app_static:/app/public
      - app_logs:/app/logs
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - frontend
      - backend

  # ─── PostgreSQL 数据库 ───
  postgres:
    image: postgres:16-alpine
    container_name: myapp-postgres
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped
    networks:
      - backend

  # ─── Redis 缓存 ───
  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    command: >
      redis-server
      --requirepass ${REDIS_PASSWORD}
      --appendonly yes
      --maxmemory 256mb
      --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - backend

# ─── 持久化卷 ───
volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local
  app_static:
    driver: local
  app_logs:
    driver: local

# ─── 网络隔离 ───
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

关键设计要点

1. 健康检查(healthcheck)

没有健康检查的 depends_on 只保证容器启动顺序,不保证服务就绪。加上 condition: service_healthy 后,Nginx 会等 Node.js 真正能响应请求后才启动,避免 502 错误。

2. 网络隔离

将网络分为 frontendbackend 两层。Nginx 只在 frontend,Redis 和 PostgreSQL 只在 backend,Node.js 横跨两层。这样即使缓存被攻破,攻击者也无法直接访问数据库。

3. 数据持久化

使用 Docker Named Volumes 而不是 bind mount,数据存储在 Docker 管理的目录中,不会因容器删除而丢失。

第三步:Nginx 配置

NGINX
# nginx/nginx.conf
worker_processes auto;
error_log /var/log/nginx/error.log warn;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    'rt=$request_time';

    access_log /var/log/nginx/access.log main;

    sendfile        on;
    tcp_nopush      on;
    keepalive_timeout 65;
    gzip            on;
    gzip_types      text/plain application/json application/javascript text/css;

    # ─── 限流配置 ───
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    upstream app_server {
        server app:3000;
        keepalive 32;
    }

    server {
        listen 80;
        server_name _;

        # 静态文件直接由 Nginx 提供,不经过 Node.js
        location /static/ {
            alias /app/static/;
            expires 30d;
            add_header Cache-Control "public, immutable";
        }

        # API 限流
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://app_server;
            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_http_version 1.1;
            proxy_set_header Connection "";
        }

        location / {
            proxy_pass http://app_server;
            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;
        }
    }
}

第四步:应用 Dockerfile

DOCKERFILE
# app/Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY src/ ./src/

FROM node:22-alpine
RUN apk add --no-cache wget
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/src ./src
COPY package.json ./

# 非 root 用户运行
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN mkdir -p /app/logs /app/public && chown -R appuser:appgroup /app
USER appuser

EXPOSE 3000
CMD ["node", "src/index.js"]

🔒 安全要点:始终以非 root 用户运行容器化进程,可以大幅降低容器逃逸的风险。

第五步:数据库初始化脚本

SQL
-- db/init/01-schema.sql
-- 此文件在 PostgreSQL 首次启动时自动执行

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE posts (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    title VARCHAR(200) NOT NULL,
    content TEXT,
    published BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);

第六步:启动和验证

BASH
# 构建并启动所有服务
docker compose up -d --build

# 查看服务状态
docker compose ps

# 输出示例:
# NAME              IMAGE                    STATUS          PORTS
# myapp-nginx       nginx:1.27-alpine        Up (healthy)   0.0.0.0:80->80/tcp
# myapp-node        myapp-app                Up (healthy)
# myapp-postgres    postgres:16-alpine       Up (healthy)   5432/tcp
# myapp-redis       redis:7-alpine           Up (healthy)   6379/tcp

# 查看日志
docker compose logs -f app

# 测试 API
curl -s http://localhost/health

常用运维命令速查

BASH
# 查看实时日志(所有服务)
docker compose logs -f

# 重启单个服务(修改 Nginx 配置后)
docker compose restart nginx

# 进入容器调试
docker compose exec app sh
docker compose exec postgres psql -U appuser -d myapp_db

# 执行一次性任务(如数据库迁移)
docker compose run --rm app npx prisma migrate deploy

# 停止并清理(保留数据卷)
docker compose down

# 停止并彻底清理(⚠️ 删除所有数据!)
docker compose down -v

# 查看资源占用
docker compose stats

数据备份

BASH
# PostgreSQL 备份
docker compose exec postgres pg_dump -U appuser myapp_db > backup_$(date +%Y%m%d).sql

# Redis 备份
docker compose exec redis redis-cli -a $REDIS_PASSWORD BGSAVE
docker cp myapp-redis:/data/dump.rdb ./redis_backup_$(date +%Y%m%d).rdb

常见问题排查

问题 原因 解决方案
容器反复重启 健康检查失败 docker compose logs <服务名> 查看具体错误
502 Bad Gateway 后端未就绪 确保 depends_on 配置了 condition: service_healthy
数据库连接被拒 密码/用户名不匹配 检查 .env 和 docker-compose.yml 的环境变量一致性
磁盘空间不足 日志或数据卷过大 定期 docker system prune,配置日志轮转
容器间通信失败 网络隔离问题 确保服务在同一个 Docker network 中

总结

本文展示了一个较完整的 Docker Compose 部署方案的核心要素:

  1. 健康检查 + 条件依赖:确保服务启动顺序正确
  2. 网络分层隔离:提升安全性
  3. 数据卷持久化:保障数据安全
  4. 多阶段构建:减小镜像体积
  5. 非 root 运行:降低安全风险
  6. 限流配置:保护后端服务

这些实践可以直接应用到你自己的项目中。建议从这个模板开始,根据实际需求增减服务。

评论