跳到主要内容
预计阅读 31 分钟

从零搭建全栈开发环境 —— 用Docker构建完整的Web应用开发与调试环境

理论学了七章,现在是检验学习成果的时候了。我们要用前面所有知识,从零搭建一个包含前端、后端、数据库、缓存的全栈开发环境。

📋 开篇自测:你已经知道多少?

  1. 你能用Docker Compose搭建一个包含至少三个服务的开发环境吗?
  2. 如何实现代码修改后容器内自动热重载?
  3. 你知道如何在容器化环境中调试应用程序吗?

一、我们要搭建什么

假设你加入了一家创业公司,要开发一个在线书店应用。技术栈如下:

  • 前端:React(开发服务器在3000端口)
  • 后端:Node.js + Express(API服务在4000端口)
  • 数据库:PostgreSQL
  • 缓存:Redis
  • 反向代理:Nginx

以前,新人入职可能要花两天时间安装这五样东西,中间还会遇到各种版本冲突和配置问题。现在,我们要把整套环境Docker化,新人入职只需要两条命令:

git clone https://github.com/your-company/bookstore.git
docker compose up -d

五分钟后,开发环境就绪。

项目目录结构

bookstore/
├── docker-compose.yml
├── docker-compose.override.yml
├── .env.example
├── .env
├── nginx/
│   └── default.conf
├── frontend/
│   ├── Dockerfile
│   ├── Dockerfile.dev
│   ├── package.json
│   └── src/
├── backend/
│   ├── Dockerfile
│   ├── Dockerfile.dev
│   ├── package.json
│   └── src/
└── scripts/
    └── init-db.sql

二、后端服务:Node.js + Express

开发用的Dockerfile

# backend/Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

# 安装nodemon用于热重载
RUN npm install -g nodemon

# 先复制依赖文件
COPY package*.json ./
RUN npm install

# 不复制源代码——开发时通过Bind Mount挂载
# 这样改代码时容器内实时同步

EXPOSE 4000

# 用nodemon启动,文件变化时自动重启
CMD ["nodemon", "--watch", "src", "src/index.js"]

生产用的Dockerfile

# backend/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src

FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/src ./src
COPY --from=builder /app/package.json ./
USER app
EXPOSE 4000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -q -O /dev/null http://localhost:4000/health || exit 1
CMD ["node", "src/index.js"]

注意两个Dockerfile的差异:

  • 开发版安装了nodemon做热重载,不复制源码(通过Bind Mount挂载)
  • 生产版使用多阶段构建,只安装生产依赖,使用非root用户运行,加了健康检查

后端入口文件示例

// backend/src/index.js
const express = require('express');
const { Pool } = require('pg');
const redis = require('redis');

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

// 数据库连接——注意主机名用的是Docker Compose中的服务名
const pool = new Pool({
  host: process.env.DB_HOST || 'postgres',
  port: process.env.DB_PORT || 5432,
  database: process.env.DB_NAME || 'bookstore',
  user: process.env.DB_USER || 'bookuser',
  password: process.env.DB_PASSWORD || 'bookpass',
});

// Redis连接
const redisClient = redis.createClient({
  url: `redis://${process.env.REDIS_HOST || 'redis'}:6379`
});

app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

app.get('/api/books', async (req, res) => {
  try {
    // 先查缓存
    const cached = await redisClient.get('books');
    if (cached) {
      return res.json({ source: 'cache', data: JSON.parse(cached) });
    }

    // 缓存没命中,查数据库
    const result = await pool.query('SELECT * FROM books ORDER BY id');

    // 存入缓存,设置60秒过期
    await redisClient.setEx('books', 60, JSON.stringify(result.rows));

    res.json({ source: 'database', data: result.rows });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

const PORT = process.env.PORT || 4000;

async function start() {
  await redisClient.connect();
  app.listen(PORT, () => {
    console.log(`Bookstore API running on port ${PORT}`);
  });
}

start().catch(console.error);

🤔 想一想 为什么在代码里我们用 process.env.DB_HOST || 'postgres' 这种写法?默认值 'postgres' 是怎么能被解析为数据库服务器地址的?


三、前端服务:React

开发用的Dockerfile

# frontend/Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

# 同样不复制源码,开发时用Bind Mount

EXPOSE 3000

# React开发服务器默认支持热重载
CMD ["npm", "start"]

React的开发服务器(通过create-react-app或Vite创建的)天生支持热模块替换(HMR),保存代码后浏览器会自动更新,开发体验非常流畅。

有一个小坑需要注意:在Docker中使用Bind Mount时,宿主机文件系统的变更通知可能无法正确传递到容器内,导致修改代码后不自动刷新。解决方法取决于你使用的构建工具:

// 如果使用 create-react-app (webpack),在 package.json 中添加:
{
  "scripts": {
    "start": "WATCHPACK_POLLING=true react-scripts start"
  }
}
// 如果使用 Vite,在 vite.config.js 中配置:
export default defineConfig({
  server: {
    watch: {
      usePolling: true
    }
  }
})

四、数据库初始化脚本

-- scripts/init-db.sql
CREATE TABLE IF NOT EXISTS books (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    author VARCHAR(100) NOT NULL,
    isbn VARCHAR(13) UNIQUE,
    price DECIMAL(10, 2),
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入一些示例数据
INSERT INTO books (title, author, isbn, price, description) VALUES
    ('深入理解计算机系统', '布莱恩特', '9787111544937', 139.00, '程序员必读经典'),
    ('代码大全', '麦康奈尔', '9787121022982', 128.00, '软件构建实践指南'),
    ('设计模式', 'GoF', '9787111618331', 89.00, '可复用面向对象软件的基础')
ON CONFLICT (isbn) DO NOTHING;

PostgreSQL的官方Docker镜像有一个贴心的功能:它会在首次启动时自动执行 /docker-entrypoint-initdb.d/ 目录下的所有 .sql.sh 文件。我们只需要把初始化脚本挂载到这个目录就行。


五、Nginx反向代理配置

# nginx/default.conf
upstream frontend {
    server frontend:3000;
}

upstream backend {
    server backend:4000;
}

server {
    listen 80;
    server_name localhost;

    # 前端请求
    location / {
        proxy_pass http://frontend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }

    # API请求转发到后端
    location /api/ {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # WebSocket支持(React热重载需要)
    location /ws {
        proxy_pass http://frontend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Nginx在这里扮演”交通警察”的角色:

  • 访问 / 的请求送到前端React服务
  • 访问 /api/ 的请求送到后端Node.js服务
  • WebSocket连接也正确转发(React的热重载需要WebSocket)

这样开发者只需要访问一个地址 http://localhost,Nginx会自动路由到正确的服务。


六、Docker Compose编排

基础配置

# docker-compose.yml
services:
  nginx:
    image: nginx:1.28-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - frontend
      - backend
    restart: unless-stopped
    networks:
      - app-network

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    volumes:
      - ./frontend/src:/app/src
      - ./frontend/public:/app/public
    environment:
      - WATCHPACK_POLLING=true  # 适用于webpack/CRA;Vite用户请在vite.config.js中配置server.watch.usePolling
    networks:
      - app-network

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    volumes:
      - ./backend/src:/app/src
    environment:
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_NAME=bookstore
      - DB_USER=bookuser
      - DB_PASSWORD=bookpass
      - REDIS_HOST=redis
      - PORT=4000
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: bookstore
      POSTGRES_USER: bookuser
      POSTGRES_PASSWORD: bookpass
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U bookuser -d bookstore"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks:
      - app-network

  redis:
    image: redis:7.2-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  pgdata:
  redis-data:

开发增强配置

# docker-compose.override.yml(开发环境自动加载)
services:
  postgres:
    ports:
      - "5432:5432"   # 暴露数据库端口,方便用DBeaver等工具连接

  redis:
    ports:
      - "6379:6379"   # 暴露Redis端口,方便用RedisInsight查看

  backend:
    ports:
      - "4000:4000"   # 暴露API端口,方便Postman直接测试
      - "9229:9229"   # Node.js调试端口
    command: nodemon --inspect=0.0.0.0:9229 --watch src src/index.js

七、启动、开发与调试

一键启动

# 首次启动(会构建镜像、创建网络、初始化数据库)
docker compose up -d

# 查看所有服务状态
docker compose ps

# 查看日志(确认没有错误)
docker compose logs

# 等所有服务Ready后,打开浏览器访问
# http://localhost      → 前端页面
# http://localhost/api/books  → API接口

日常开发流程

开发过程中,你的工作流大概是这样的:

  1. 早上到公司docker compose up -d 启动环境(如果昨天 stop 了的话)

  2. 写前端代码:直接在本地编辑 frontend/src/ 下的文件。因为Bind Mount的存在,改动实时同步到容器内,React开发服务器自动热重载,浏览器刷新

  3. 写后端代码:编辑 backend/src/ 下的文件。nodemon检测到变化后自动重启Node.js进程

  4. 查看后端日志docker compose logs -f backend

  5. 连接数据库调试:用你喜欢的数据库客户端连接 localhost:5432

  6. 添加新的npm依赖

    # 在后端添加新依赖
    docker compose exec backend npm install axios
    
    # 或者在宿主机上改了package.json后重建
    docker compose up -d --build backend
  7. 下班docker compose stop(不会删除数据)

调试Node.js应用

我们在override配置中开启了Node.js的调试端口(9229)。VS Code的调试配置:

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Docker: Attach to Node",
      "remoteRoot": "/app",
      "localRoot": "${workspaceFolder}/backend",
      "port": 9229,
      "restart": true
    }
  ]
}

配置好之后,在VS Code中按F5就能连接到容器内的Node.js进程。设断点、单步调试、查看变量——和本地开发一模一样的体验。

常见问题排查

# 某个服务启动失败?查看它的日志
docker compose logs backend

# 数据库连接不上?检查数据库是否就绪
docker compose exec postgres pg_isready

# 网络不通?检查容器的网络配置
docker compose exec backend sh -c "nslookup postgres"

# 端口被占用?查看端口映射情况
docker compose port nginx 80

# 想重新初始化数据库?删除Volume后重启
docker compose down -v
docker compose up -d

# 容器内安装一个调试工具
docker compose exec backend apk add --no-cache curl
docker compose exec backend redis-cli -h redis ping

⚠️ 常见误区

  • 误区一:“开发环境和生产环境用同一个Dockerfile”。开发环境需要热重载、调试端口、更多的日志输出;生产环境需要精简镜像、安全加固、性能优化。应该分别维护。
  • 误区二:“在容器里直接npm install修改了node_modules就行”。如果node_modules是Bind Mount从宿主机映射的,确实可以。但如果不是(镜像自带的),容器删除后安装的包就丢了。最好修改package.json后用 --build 重建镜像。
  • 误区三:“数据库密码直接写在docker-compose.yml里很危险”。在本地开发环境中这样做问题不大。但在生产环境中,应该使用Docker Secrets或环境变量文件(.env),并且把这些文件加入 .gitignore

八、团队协作最佳实践

新人入职指南

把以下内容写进项目的README:

## 快速开始

1. 安装 Docker Desktop
2. 克隆项目:git clone ...
3. 复制环境变量:cp .env.example .env
4. 启动环境:docker compose up -d
5. 等待约1分钟后访问 http://localhost
6. API文档:http://localhost/api/docs

## 常用命令
- 启动环境:docker compose up -d
- 停止环境:docker compose stop
- 查看日志:docker compose logs -f [服务名]
- 重置数据库:docker compose down -v && docker compose up -d

环境变量管理

# .env.example(提交到git,作为模板)
DB_PASSWORD=change_me
REDIS_HOST=redis
API_SECRET=change_me

# .env(不提交到git,每人自己配置)
DB_PASSWORD=my_local_password
REDIS_HOST=redis
API_SECRET=my_local_secret

Makefile简化命令

# Makefile
.PHONY: up down logs restart rebuild clean

up:
	docker compose up -d

down:
	docker compose down

logs:
	docker compose logs -f

restart:
	docker compose restart

rebuild:
	docker compose up -d --build

clean:
	docker compose down -v
	docker system prune -f

db-shell:
	docker compose exec postgres psql -U bookuser -d bookstore

redis-shell:
	docker compose exec redis redis-cli

这样团队成员只需要记住 make upmake downmake logs 这些简单的命令就行了。

🤔 想一想 你的团队有人用macOS,有人用Windows,有人用Linux。在Docker化的开发环境中,你觉得还有哪些跨平台兼容性问题需要注意?(提示:文件系统大小写敏感性、行尾符、文件权限等)


📝 掌握度自测

  1. 在本章的架构中,为什么前端和后端各需要两个Dockerfile(开发版和生产版)?两者的主要区别是什么?

  2. 如何实现”修改后端代码后自动重启Node.js进程”?涉及哪些技术和配置?

  3. 新入职的同事执行 docker compose up -d 后发现后端报数据库连接错误,请列举你的排查步骤。

  4. 为什么在docker-compose.yml中要给PostgreSQL和Redis配置healthcheck?如果不配置会有什么问题?

  5. 描述你如何在容器化的开发环境中使用VS Code调试Node.js应用。需要哪些配置?

💡 自我评估

  • 全部答对:你已经能够独立设计和搭建容器化的全栈开发环境了。下一章我们将探索如何用Docker打通CI/CD流水线。
  • 答对3-4题:实战能力已经不错。建议自己实际搭建一次本章的完整项目来巩固。
  • 答对1-2题:不要气馁。本章涉及多项技术的综合应用,需要前面几章的知识作为基础。如果某些部分不清楚,建议回看对应章节。

购买课程解锁全部内容

告别「在我电脑上能跑」:Docker 容器化实战

¥29.90