很多开发者在本地跑得好好的项目,一部署到服务器就出各种问题------环境不一致、依赖版本冲突、配置文件散落各处。Docker 就是解决这些问题的利器,本文从零开始,手把手搭建一套包含 Nginx、Node.js、MySQL、Redis 的完整项目运行环境,所有服务一条命令启动,换任何机器都能一键复现。
项目结构
先把整体目录结构理清楚:
project/
├── docker-compose.yml
├── nginx/
│ ├── nginx.conf
│ └── conf.d/
│ └── default.conf
├── node-app/
│ ├── Dockerfile
│ ├── package.json
│ ├── app.js
│ └── ...
├── mysql/
│ ├── Dockerfile
│ ├── my.cnf
│ └── init/
│ └── init.sql
└── redis/
└── redis.conf
每个服务独立一个目录,配置和代码分离,结构清晰,方便维护。
第一步:编写 Node.js 应用
先写一个简单的 Express 应用,作为示例后端服务:
javascript
// node-app/app.js
const express = require('express');
const mysql = require('mysql2/promise');
const Redis = require('ioredis');
const app = express();
app.use(express.json());
// MySQL 连接池
const db = mysql.createPool({
host: process.env.DB_HOST || 'mysql',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'root123',
database: process.env.DB_NAME || 'myapp',
});
// Redis 连接
const redis = new Redis({
host: process.env.REDIS_HOST || 'redis',
port: process.env.REDIS_PORT || 6379,
});
// 健康检查接口
app.get('/health', async (req, res) => {
try {
await db.query('SELECT 1');
await redis.ping();
res.json({ status: 'ok', timestamp: new Date().toISOString() });
} catch (err) {
res.status(500).json({ status: 'error', message: err.message });
}
});
// 示例接口:获取用户列表
app.get('/api/users', async (req, res) => {
try {
// 先查 Redis 缓存
const cached = await redis.get('users:list');
if (cached) {
return res.json({ source: 'cache', data: JSON.parse(cached) });
}
// 缓存未命中,查数据库
const [rows] = await db.query('SELECT id, name, email FROM users');
await redis.set('users:list', JSON.stringify(rows), 'EX', 60);
res.json({ source: 'db', data: rows });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
第二步:编写 Node.js 的 Dockerfile
dockerfile
# node-app/Dockerfile
# 构建阶段:安装依赖
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 运行阶段:只复制必要文件
FROM node:20-alpine
WORKDIR /app
# 创建非 root 用户,提升安全性
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -s /bin/sh -D appuser
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER appuser
EXPOSE 3000
# 使用 dumb-init 处理信号转发,避免僵尸进程
RUN apk add --no-cache dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "app.js"]
几个关键点:
- 多阶段构建:把依赖安装和运行环境分开,最终镜像更小。
- 非 root 用户运行:容器内不要用 root 权限跑应用,降低安全风险。
- dumb-init :容器内的 PID 1 进程需要正确处理系统信号(如 SIGTERM),否则
docker stop时应用不能优雅退出。
第三步:配置 MySQL
sql
-- mysql/init/init.sql
CREATE DATABASE IF NOT EXISTS myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE myapp;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(200) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 插入测试数据
INSERT INTO users (name, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', 'lisi@example.com'),
('王五', 'wangwu@example.com');
MySQL 的自定义配置:
ini
# mysql/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default-time-zone='+08:00'
max_connections=200
innodb_buffer_pool_size=256M
slow_query_log=ON
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=1
[client]
default-character-set=utf8mb4
第四步:配置 Redis
conf
# redis/redis.conf
# 设置密码
requirepass redis123
# 最大内存限制
maxmemory 256mb
maxmemory-policy allkeys-lru
# 持久化策略
save 900 1
save 300 10
save 60 10000
# 关闭危险命令
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
第五步:配置 Nginx 反向代理
nginx
# nginx/conf.d/default.conf
upstream node_backend {
server node-app:3000;
# 如果有多个实例,可以加更多 server
# server node-app:3001;
# server node-app:3002;
}
server {
listen 80;
server_name localhost;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip 压缩
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1024;
# 反向代理到 Node.js
location /api/ {
proxy_pass http://node_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;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
}
location /health {
proxy_pass http://node_backend;
}
# 静态文件(如果有前端页面)
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
第六步:编写 docker-compose.yml
这是整个方案的核心,把所有服务编排在一起:
yaml
version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: myapp-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: app123
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/init:/docker-entrypoint-initdb.d
- ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
command: --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
# Redis 缓存
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis123", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
# Node.js 应用
node-app:
build:
context: ./node-app
dockerfile: Dockerfile
container_name: myapp-node
restart: unless-stopped
environment:
PORT: 3000
DB_HOST: mysql
DB_PORT: 3306
DB_USER: appuser
DB_PASSWORD: app123
DB_NAME: myapp
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: redis123
NODE_ENV: production
ports:
- "3000:3000"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
- frontend
# Nginx 反向代理
nginx:
image: nginx:alpine
container_name: myapp-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- node-app
networks:
- frontend
# 数据卷:持久化存储
volumes:
mysql_data:
driver: local
redis_data:
driver: local
# 网络隔离
networks:
frontend:
driver: bridge
backend:
driver: bridge
几个设计要点:
- depends_on + healthcheck:Node.js 应用会等 MySQL 和 Redis 健康检查通过后才启动,避免应用启动了但数据库还没就绪导致的连接错误。
- 网络隔离 :Nginx 和 Node.js 在
frontend网络通信,Node.js、MySQL、Redis 在backend网络通信。Nginx 无法直接访问数据库,提升了安全性。 - 数据卷持久化:MySQL 和 Redis 的数据存储在 Docker Volume 中,容器删除重建后数据不会丢失。
第七步:一键启动
所有配置写好后,在项目根目录执行:
bash
# 构建并启动所有服务
docker compose up -d --build
# 查看服务状态
docker compose ps
# 查看日志
docker compose logs -f node-app
第一次启动时,MySQL 会自动执行 init/init.sql 初始化数据库和表结构。等所有服务启动完成后,测试一下:
bash
# 健康检查
curl http://localhost/health
# 获取用户列表
curl http://localhost/api/users
常用运维命令
bash
# 停止所有服务(保留数据)
docker compose down
# 停止并删除数据卷(慎用,会清空数据库)
docker compose down -v
# 只重启某个服务
docker compose restart node-app
# 进入 MySQL 容器执行 SQL
docker exec -it myapp-mysql mysql -u root -proot123 myapp
# 进入 Redis 容器
docker exec -it myapp-redis redis-cli -a redis123
# 查看资源占用
docker stats
生产环境注意事项
以上配置适合开发和测试环境,上生产还需要做以下调整:
- 不要在 docker-compose.yml 中明文写密码 ,改用 Docker Secrets 或环境变量文件(
.env),并将.env加入.gitignore。 - MySQL 和 Redis 不要暴露端口到宿主机 ,去掉
ports映射,只通过内部网络通信。 - Nginx 配置 HTTPS,可以用 Let's Encrypt + certbot 自动申请证书。
- 日志收集:配置日志驱动,将容器日志输出到 ELK 或 Loki 等日志平台,方便排查问题。
- 资源限制:给每个服务设置 CPU 和内存上限,防止某个服务异常时耗尽宿主机资源:
yaml
services:
node-app:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
这套方案的核心价值在于环境一致性 和一键部署------新同事入职,clone 代码后一条命令就能跑起整个项目;服务器迁移,把代码和配置拷过去,同样一条命令就能恢复。再也不用对着 README 里一堆"先装这个再装那个"的步骤头疼了。