![码头集装箱吊装](https://blog.kbmp.club/upload/docker.jpg) 本地 `docker compose up -d` 一把梭很爽。但同一份配置直接放到公网服务器上,常见的结果是第一天一切正常,第三十天出问题:磁盘被日志写满、容器崩了没人拉起来、数据库端口裸奔在公网上。 下面是我每次部署前会过一遍的清单,针对「单台 VPS 跑几个服务」这个最常见的场景。 ## 镜像版本写死,别用 latest `image: nginx:latest` 意味着你下一次 `docker compose pull` 拿到的可能是另一个版本,而且你不会提前知道。生产环境一律写具体版本号——升级应该是主动行为,不是意外。 ```yaml services: web: image: nginx:1.27.4 # 好 # image: nginx:latest # 坏 ``` ## 给每个服务配 restart 策略 容器崩溃或宿主机重启后,没有重启策略的服务就躺在那儿了。 ```yaml restart: unless-stopped ``` `unless-stopped` 与 `always` 的区别:手动 `docker stop` 之后,前者不会在守护进程重启时把容器又拉起来。日常用前者更符合直觉。 ## 数据库要等它真的就绪 `depends_on` 默认只等容器「启动」,不等服务「可用」。应用启动时数据库还在初始化,连不上就退出,然后无限重启。加上健康检查才是真等: ```yaml services: app: depends_on: db: condition: service_healthy db: image: postgres:16 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s retries: 5 ``` ## 数据必须放卷里 容器的可写层是临时的,容器一删数据就没了。数据库、上传的文件、配置文件,一律用命名卷: ```yaml volumes: db_data: services: db: volumes: - db_data:/var/lib/postgresql/data ``` 顺带一句:**卷不等于备份**。卷只解决「容器重建不丢数据」,硬盘挂了照样没。该做的备份还是得做。 ## 限制日志体积 默认的 json-file 日志驱动会一直写,直到把磁盘填满——这是小 VPS 上最常见的猝死方式。 ```yaml logging: driver: json-file options: max-size: "10m" max-file: "3" ``` ## 只发布需要暴露的端口 这是最容易出事的一条。Docker 会自己写 iptables 规则,一个 `5432:5432` 很可能绕开你配好的 UFW 直接暴露到公网。数据库和缓存根本不需要发布端口,它们在 compose 的内部网络里就能互通。 Web 服务绑到回环地址,让反代去收口: ```yaml ports: - "127.0.0.1:3000:3000" ``` ## 用反代统一收口 80/443 只让 Nginx / Caddy / Traefik 监听 80 和 443。TLS 终止、证书续期、路由都在这一层做,之后要加压缩、限流、安全头也只有一个地方要改。 ## 密钥别写进镜像和 Git 密码、API Key 放 `.env` 并加进 `.gitignore`,或者用 compose secrets 从文件读: ```yaml services: db: environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password secrets: db_password: file: ./secrets/db_password.txt ``` ## 给容易爆内存的服务加资源限制 ```yaml deploy: resources: limits: memory: 512M cpus: "1.0" ``` ## 最后:定期看一眼磁盘 ```bash docker system df # 看镜像、容器、卷分别占多少 docker system prune -a # 清理无用镜像和构建缓存(执行前确认没有要保留的) ``` ## 小结 这份清单不复杂,但每一条对应的都是真实会发生的故障:版本漂移、崩溃不恢复、数据丢失、磁盘写满、端口裸奔。部署前花十分钟过一遍,能省掉后面好几个半夜的排查。 后面会写 Docker + Nginx 反代 + 证书自动续期的完整部署流程,把这篇里没展开的部分补上。