从零搭建全栈开发环境 —— 用Docker构建完整的Web应用开发与调试环境
理论学了七章,现在是检验学习成果的时候了。我们要用前面所有知识,从零搭建一个包含前端、后端、数据库、缓存的全栈开发环境。
📋 开篇自测:你已经知道多少?
- 你能用Docker Compose搭建一个包含至少三个服务的开发环境吗?
- 如何实现代码修改后容器内自动热重载?
- 你知道如何在容器化环境中调试应用程序吗?
一、我们要搭建什么
假设你加入了一家创业公司,要开发一个在线书店应用。技术栈如下:
- 前端: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接口
日常开发流程
开发过程中,你的工作流大概是这样的:
-
早上到公司,
docker compose up -d启动环境(如果昨天stop了的话) -
写前端代码:直接在本地编辑
frontend/src/下的文件。因为Bind Mount的存在,改动实时同步到容器内,React开发服务器自动热重载,浏览器刷新 -
写后端代码:编辑
backend/src/下的文件。nodemon检测到变化后自动重启Node.js进程 -
查看后端日志:
docker compose logs -f backend -
连接数据库调试:用你喜欢的数据库客户端连接
localhost:5432 -
添加新的npm依赖:
# 在后端添加新依赖 docker compose exec backend npm install axios # 或者在宿主机上改了package.json后重建 docker compose up -d --build backend -
下班:
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 up、make down、make logs 这些简单的命令就行了。
🤔 想一想 你的团队有人用macOS,有人用Windows,有人用Linux。在Docker化的开发环境中,你觉得还有哪些跨平台兼容性问题需要注意?(提示:文件系统大小写敏感性、行尾符、文件权限等)
📝 掌握度自测
-
在本章的架构中,为什么前端和后端各需要两个Dockerfile(开发版和生产版)?两者的主要区别是什么?
-
如何实现”修改后端代码后自动重启Node.js进程”?涉及哪些技术和配置?
-
新入职的同事执行
docker compose up -d后发现后端报数据库连接错误,请列举你的排查步骤。 -
为什么在docker-compose.yml中要给PostgreSQL和Redis配置healthcheck?如果不配置会有什么问题?
-
描述你如何在容器化的开发环境中使用VS Code调试Node.js应用。需要哪些配置?
💡 自我评估
- 全部答对:你已经能够独立设计和搭建容器化的全栈开发环境了。下一章我们将探索如何用Docker打通CI/CD流水线。
- 答对3-4题:实战能力已经不错。建议自己实际搭建一次本章的完整项目来巩固。
- 答对1-2题:不要气馁。本章涉及多项技术的综合应用,需要前面几章的知识作为基础。如果某些部分不清楚,建议回看对应章节。
购买课程解锁全部内容
告别「在我电脑上能跑」:Docker 容器化实战
¥29.90