一、从手工部署到编排:一次痛苦的运维经历
上个月接手一个Node.js项目,三个服务要部署到测试服务器:Nginx做反向代理、Express API服务、Redis做缓存。最初按老习惯手工部署,SSH登录后一个个装依赖、写systemd服务文件、配环境变量。结果踩了一堆坑:Redis忘了开持久化,重启服务器数据全丢;Nginx配置写错一个分号,服务直接挂掉;更别提每次发布新版本要手动执行六七个命令。
最崩溃的一次是周五下午,测试环境Redis连接数爆了,排查半天发现是Nginx upstream配置里没加keepalive,导致每个请求都新建TCP连接。当时就下定决心:必须用Docker Compose做编排,把这些服务的生命周期管理交给工具,而不是靠人肉记忆。
二、环境与版本:明确技术栈
先交代一下具体环境,方便大家对照:
- 服务器:CentOS 7.9,2核4G内存,50G SSD
- Docker版本:24.0.7(用
docker version确认) - Docker Compose版本:v2.23.0(注意新版compose命令是
docker compose,中间有空格,不是docker-compose) - 镜像版本:
- nginx:1.25.3-alpine(alpine版体积小,只有23MB)
- node:20-alpine(生产环境用alpine跑Node,体积比debian版小60%)
- redis:7.2.3-alpine(Redis 7.x自带多线程,性能比6.x提升明显)
这里有个小建议:镜像tag一定要指定具体版本,不要用latest。我吃过亏,某次latest标签的Node镜像更新后,项目依赖的某个原生模块编译失败,部署直接中断。
三、方案设计:网络、卷、健康检查三件套
设计这套编排方案时,重点考虑三个维度:
1. 网络隔离
用自定义bridge网络app-network,而不是默认的default网络。原因:自定义网络支持DNS解析,服务间能用服务名互访(比如Nginx里配proxy_pass http://api:3000),而默认网络只能用IP。另外自定义网络可以指定子网,避免和公司内网网段冲突。
2. 数据持久化
Redis数据必须落盘,否则容器重启就是一场灾难。Nginx日志也要持久化,方便排查问题。这里用命名卷(named volume),不推荐bind mount(绑定挂载),因为bind mount依赖宿主机目录结构,换台机器就得改路径。
3. 健康检查与启动顺序
Compose的depends_on有两个坑:
- 默认depends_on只控制容器创建顺序,不保证服务就绪。比如Redis容器起来了,但Redis服务可能还没初始化完,Node服务就开始连接,会报ECONNREFUSED。
- 解决办法:depends_on里加condition: service_healthy,配合每个服务的healthcheck探针。
四、核心实现:可运行的docker-compose.yml
直接上代码,这个配置我排过雷,复制就能用:
version: '3.8'
services:
redis:
image: redis:7.2.3-alpine
container_name: myapp-redis
restart: unless-stopped
volumes:
- redis-data:/data
- ./conf/redis.conf:/usr/local/etc/redis/redis.conf:ro
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
api:
build: ./api
image: myapp-api:1.0.0
container_name: myapp-api
restart: unless-stopped
expose:
- "3000"
environment:
- NODE_ENV=production
- REDIS_HOST=redis
- REDIS_PORT=6379
volumes:
- api-logs:/app/logs
depends_on:
redis:
condition: service_healthy
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
nginx:
image: nginx:1.25.3-alpine
container_name: myapp-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./conf/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
- nginx-logs:/var/log/nginx
depends_on:
api:
condition: service_healthy
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/healthz"]
interval: 10s
timeout: 3s
retries: 3
networks:
app-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
volumes:
redis-data:
api-logs:
nginx-logs:
再补充关键的Nginx配置片段,注意upstream里的DNS解析和keepalive:
# conf/nginx.conf 关键部分
upstream node_api {
# 关键:用服务名而不是IP,compose的DNS会自动解析
server api:3000;
keepalive 32; # 保持长连接,避免频繁握手
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://node_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 超时设置,后端接口慢时优化体验
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
}
五、踩坑记录:三个血泪教训
坑1:Redis持久化配置丢失
第一次部署时,我只写了volumes: redis-data:/data,但Redis默认快照策略是save 900 1,即900秒内至少1次写操作才触发RDB。结果容器重启后数据丢了。后来在redis.conf里显式配置:
appendonly yes
appendfsync everysec
开启了AOF持久化,配合RDB双保险。测试环境压测时,每秒写入5000条记录,重启容器后数据零丢失。
坑2:健康检查探针选错命令
Node服务的healthcheck最初用curl,但node:20-alpine镜像里没有curl!容器启动后一直报healthcheck failed。换成wget解决了(alpine自带BusyBox wget)。记住:alpine镜像默认不装curl,要么改用wget,要么在Dockerfile里手动装curl。
坑3:depends_on条件不生效
用depends_on: - redis时,Node容器总是比Redis先启动,导致连接失败。后来加了condition: service_healthy,但Compose会等Redis的healthcheck通过后才启动Node容器。注意:healthcheck的start_period要设置合理,Redis启动很快,5秒够了;Node应用加载依赖可能需要10秒,所以给了10秒。
六、效果数据:对比手工部署
改造成Compose编排后,最直观的变化:
| 指标 | 手工部署 | Docker Compose |
|---|---|---|
| 首次部署耗时 | 45分钟(装依赖、配服务) | 12分钟(写Dockerfile+compose文件) |
| 重启服务耗时 | 手动杀进程+重启,约45秒 | docker compose restart,12秒 |
| 服务器资源占用 | 3个进程各自为战,内存峰值2.1G | 容器共享内核,内存峰值1.7G |
| 故障恢复 | 宕机后需人工SSH操作 | restart: unless-stopped自动拉起,1分钟内恢复 |
另外说个细节:用docker compose config可以校验配置正确性,避免直接启动报错。上线前建议跑一遍docker compose up -d --build,然后docker compose ps查看健康状态。
七、总结与建议
这套编排方案现在稳定跑了两周,零故障。几点心得:
- 版本锁定是底线:镜像版本、Compose文件格式(
version: '3.8')都要锁定,别追新。 - 健康检查必须配:没有healthcheck的
depends_on只是摆设,服务未就绪照样启动。 - 日志和数据的持久化要提前规划:别等数据丢了再后悔,命名卷是正解。
最后留个思考题:如果你的Node服务有多副本需求(比如replicas: 2),Nginx的upstream配置该怎么改?提示:需要加resolver指令。下篇文章聊这个。