AI 摘要
AI
正在生成摘要...

Docker Compose 实战:搭建 Nginx + Node.js + PostgreSQL + Redis 完整开发环境

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

为什么需要这篇文章?

每次搭建新项目都要手动装 Nginx、配 Node.js、启 PostgreSQL、起 Redis……换台电脑又得重来一遍。有没有一种方式,一条命令就能把整个开发环境拉起来?

答案就是 Docker Compose。本文将手把手带你用 Docker Compose 搭建一个较完整的的 Web 开发环境,涵盖反向代理、应用服务、数据库和缓存四大组件。跟着做,完成配置后可在自己的环境中验证服务是否正常。

环境准备

前置要求:

  • Linux 服务器或本地开发机(macOS/Windows 均可)
  • Docker 20.10+
  • Docker Compose V2(docker compose 命令)

验证安装:

BASH
docker --version
# Docker version 24.x.x 或更高

docker compose version
# Docker Compose version v2.x.x

如果还没安装 Docker,可以用官方脚本一键安装:

BASH
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# 重新登录终端生效

项目结构

我们搭建的项目结构如下:

TEXT
my-web-app/
├── docker-compose.yml      # 核心编排文件
├── nginx/
│   └── nginx.conf          # Nginx 配置
├── app/
│   ├── Dockerfile          # Node.js 应用镜像
│   ├── package.json
│   └── server.js           # 简单的 Express 应用
└── db/
    └── init.sql            # 数据库初始化脚本

第一步:编写 Docker Compose 文件

这是整个项目的核心,docker-compose.yml

YAML
# version 字段在 Docker Compose v2 中已废弃,可省略

services:
  # ==================== Nginx 反向代理 ====================
  nginx:
    image: nginx:1.25-alpine
    container_name: web-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - nginx-logs:/var/log/nginx
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - webnet

  # ==================== Node.js 应用 ====================
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    container_name: web-app
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://appuser:apppass@postgres:5432/webapp
      - REDIS_URL=redis://redis:6379
      - PORT=3000
    expose:
      - "3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 15s
    restart: unless-stopped
    networks:
      - webnet

  # ==================== PostgreSQL 数据库 ====================
  postgres:
    image: postgres:16-alpine
    container_name: web-postgres
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppass
      POSTGRES_DB: webapp
    volumes:
      - pg-data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d webapp"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped
    networks:
      - webnet

  # ==================== Redis 缓存 ====================
  redis:
    image: redis:7-alpine
    container_name: web-redis
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped
    networks:
      - webnet

# ==================== 数据卷 ====================
volumes:
  pg-data:
    driver: local
  redis-data:
    driver: local
  nginx-logs:
    driver: local

# ==================== 网络 ====================
networks:
  webnet:
    driver: bridge

关键设计解析

1. depends_on + condition: service_healthy

很多人只知道 depends_on 控制启动顺序,但不知道 condition 参数。默认情况下 depends_on 只等容器启动,不等服务就绪。加上 condition: service_healthy 后,会等健康检查通过才启动下游服务:

YAML
# ❌ 错误:容器启动了但 PostgreSQL 还没 ready
depends_on:
  - postgres

# ✅ 正确:等 PostgreSQL 健康检查通过
depends_on:
  postgres:
    condition: service_healthy

2. expose vs ports

  • ports: "80:80" — 映射到宿主机,外部可访问
  • expose: "3000" — 仅容器间通信,不暴露到宿主机

Node.js 应用不需要直接对外,通过 Nginx 转发即可,所以用 expose

3. 命名卷 vs Bind Mount

YAML
# 命名卷(生产推荐)— 数据由 Docker 管理,性能好
volumes:
  - pg-data:/var/lib/postgresql/data

# Bind Mount(开发推荐)— 直接挂载本地目录,方便调试
volumes:
  - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro

第二步:编写 Nginx 配置

nginx/nginx.conf

NGINX
events {
    worker_connections 1024;
}

http {
    upstream app_backend {
        server app:3000;
    }

    server {
        listen 80;
        server_name localhost;

        # 访问日志
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        # Gzip 压缩
        gzip on;
        gzip_types text/plain text/css application/json application/javascript;

        # 反向代理到 Node.js
        location / {
            proxy_pass http://app_backend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            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 /static/ {
            alias /app/public/;
            expires 30d;
        }
    }
}

注意 upstream 中的 app — 这就是 Docker Compose 网络中的服务名。Docker 内置 DNS 会自动将 app 解析为容器 IP。

第三步:编写 Node.js 应用

app/Dockerfile

DOCKERFILE
FROM node:20-alpine

WORKDIR /app

# 先复制依赖文件(利用 Docker 缓存层)
COPY package*.json ./
RUN npm ci --only=production

# 再复制源码
COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

app/package.json

JSON
{
  "name": "web-app",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.0",
    "pg": "^8.11.0",
    "redis": "^4.6.0"
  }
}

app/server.js

JAVASCRIPT
const express = require("express");
const { Pool } = require("pg");
const { createClient } = require("redis");

const app = express();
app.use(express.json());

// PostgreSQL 连接池
const pgPool = new Pool({ connectionString: process.env.DATABASE_URL });

// Redis 客户端
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.on("error", (err) => console.error("Redis error:", err));

// ==================== 路由 ====================

// 健康检查
app.get("/health", async (req, res) => {
  try {
    await pgPool.query("SELECT 1");
    await redisClient.ping();
    res.json({ status: "ok", db: "connected", cache: "connected" });
  } catch (err) {
    res.status(503).json({ status: "error", message: err.message });
  }
});

// 写入数据(同时写库 + 缓存)
app.post("/api/items", async (req, res) => {
  const { name, value } = req.body;
  try {
    const result = await pgPool.query(
      "INSERT INTO items (name, value) VALUES ($1, $2) RETURNING *",
      [name, value]
    );
    // 写入缓存
    await redisClient.set(`item:${result.rows[0].id}`, JSON.stringify(result.rows[0]), { EX: 3600 });
    res.json(result.rows[0]);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// 读取数据(优先走缓存)
app.get("/api/items/:id", async (req, res) => {
  try {
    // 先查缓存
    const cached = await redisClient.get(`item:${req.params.id}`);
    if (cached) {
      return res.json({ source: "cache", data: JSON.parse(cached) });
    }
    // 缓存未命中,查数据库
    const result = await pgPool.query("SELECT * FROM items WHERE id = $1", [req.params.id]);
    if (result.rows.length === 0) {
      return res.status(404).json({ error: "not found" });
    }
    // 回填缓存
    await redisClient.set(`item:${req.params.id}`, JSON.stringify(result.rows[0]), { EX: 3600 });
    res.json({ source: "database", data: result.rows[0] });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ==================== 启动 ====================
async function start() {
  await redisClient.connect();
  console.log("Redis connected");

  const port = process.env.PORT || 3000;
  app.listen(port, () => console.log(`App running on port ${port}`));
}

start().catch(console.error);

第四步:数据库初始化

db/init.sql

SQL
CREATE TABLE IF NOT EXISTS items (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    value TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入测试数据
INSERT INTO items (name, value) VALUES
    ('hello', 'world'),
    ('docker', 'compose'),
    ('nginx', 'reverse-proxy');

这个文件放在 /docker-entrypoint-initdb.d/ 目录下,PostgreSQL 容器首次启动时会自动执行。

第五步:启动!

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

# 查看运行状态
docker compose ps

# 查看日志(实时跟踪)
docker compose logs -f app

预期输出:

TEXT
NAME             IMAGE                     STATUS          PORTS
web-nginx        nginx:1.25-alpine         Up (healthy)    0.0.0.0:80->80/tcp
web-app          my-web-app-app            Up (healthy)
web-postgres     postgres:16-alpine        Up (healthy)    0.0.0.0:5432->5432/tcp
web-redis        redis:7-alpine            Up (healthy)    0.0.0.0:6379->6379/tcp

验证服务:

BASH
# 健康检查
curl http://localhost/health
# {"status":"ok","db":"connected","cache":"connected"}

# 写入数据
curl -X POST http://localhost/api/items \
  -H "Content-Type: application/json" \
  -d '{"name":"test","value":"hello from compose"}'

# 读取数据(第一次走数据库)
curl http://localhost/api/items/1
# {"source":"database","data":{"id":1,"name":"hello","value":"world",...}}

# 读取数据(第二次走缓存)
curl http://localhost/api/items/1
# {"source":"cache","data":{"id":1,"name":"hello","value":"world",...}}

常用运维命令

BASH
# 停止所有服务(保留数据卷)
docker compose down

# 停止并删除数据卷(⚠️ 会丢数据)
docker compose down -v

# 重建单个服务
docker compose up -d --build app

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

# 查看资源占用
docker compose stats

# 一键更新镜像并重启
docker compose pull && docker compose up -d

5 个避坑指南

1. 容器间通信用服务名,不用 localhost

Docker Compose 创建了独立的 bridge 网络,容器之间通过服务名互通:

YAML
# ✅ 正确:用服务名
DATABASE_URL=postgresql://appuser:apppass@postgres:5432/webapp

# ❌ 错误:localhost 指向容器自己
DATABASE_URL=postgresql://appuser:apppass@localhost:5432/webapp

2. 端口冲突排查

BASH
# 查看谁占用了 80 端口
sudo lsof -i :80
# 或
sudo ss -tlnp | grep :80

3. 数据库连接被拒:等服务就绪

如果应用先于 PostgreSQL 启动,会报 ECONNREFUSED。确保用 healthcheck + condition: service_healthy

4. 修改配置后忘记重建

Docker 缓存了旧的配置。改了 Dockerfile 或 nginx.conf 后,必须加 --build

BASH
docker compose up -d --build nginx  # 重建 Nginx
docker compose up -d --build app    # 重建 App

5. 查不到日志?用 docker compose logs

BASH
# ❌ 容器名太长记不住
docker logs web-postgres

# ✅ 用 compose 命令,自动带上项目前缀
docker compose logs postgres
docker compose logs --tail=50 -f  # 实时跟踪最近 50 行

总结

通过这个实战项目,你学到了:

技术点 说明
服务编排 docker-compose.yml 定义多容器应用
健康检查 healthcheck + condition 确保服务按序就绪
网络隔离 自定义 bridge 网络,服务名互通
数据持久化 命名卷保存 PostgreSQL 和 Redis 数据
反向代理 Nginx 转发请求到后端应用
缓存策略 Redis 缓存 + 数据库兜底的读写模式

一条 docker compose up -d,整个环境就绪。换电脑、换服务器,复制这个目录就能跑——这就是 Docker Compose 的魅力。


本文为 FlecBlog 技术分享系列文章,代码示例基于 Docker Compose V2 和 Alpine 镜像,适用于开发和测试环境。生产部署请根据实际需求调整资源限制、安全策略和备份方案。

评论