一个最小可用的 compose 文件
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./site:/usr/share/nginx/html:ro
restart: unless-stopped
保存为 docker-compose.yml,然后:
docker compose up -d # 后台启动
docker compose ps # 看状态
docker compose logs -f # 跟踪日志
docker compose down # 停止并删除容器
注意两个 2026 年的写法变化:顶层不再需要 version: 字段(Compose V2 会忽略它,官方已标记为过时),命令也从 docker-compose 变成了 docker compose 子命令。
ports:方向别搞反
ports:
- "8080:80" # 宿主机 8080 → 容器 80
- "127.0.0.1:5432:5432" # 只绑定到本机回环,外网访问不到
- "80" # 宿主机端口随机分配
顺序永远是 宿主机:容器,从外到内。
一个安全提醒:- "5432:5432" 会把数据库端口暴露到服务器的所有网卡上,配合一个弱密码就是公网可入侵的数据库。如果这个端口只给同一个 compose 网络里的其他服务用,根本不要写 ports——同网络内的容器可以直接用服务名互访:
services:
api:
image: myapi
environment:
DATABASE_URL: postgres://user:pass@db:5432/app # 直接用服务名 db
db:
image: postgres:16
# 不写 ports,外部就访问不到
只有确实需要从宿主机连进去调试时,才写成 "127.0.0.1:5432:5432",把它限制在回环地址上。
volumes:具名卷还是绑定挂载
两种写法长得像,行为完全不同:
volumes:
- ./site:/usr/share/nginx/html # 绑定挂载:宿主机的一个目录
- pgdata:/var/lib/postgresql/data # 具名卷:Docker 管理的存储
volumes:
pgdata: # 具名卷必须在顶层声明
绑定挂载 ./x:/y | 具名卷 name:/y | |
|---|---|---|
| 数据存在哪 | 你指定的宿主机目录 | Docker 管理的区域 |
| 你能直接编辑吗 | 能,改了容器里立刻可见 | 不方便 |
| 跨平台表现 | Windows/macOS 上 I/O 慢 | 一致且快 |
| 适合 | 源码、配置文件 | 数据库数据 |
经验法则:代码和配置用绑定挂载,数据库数据用具名卷。 数据库文件跑在 macOS 的绑定挂载上,性能会明显变差,还容易碰上文件权限问题。
只读挂载加 :ro 后缀,配置文件建议都加上,防止容器意外写坏:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
环境变量:三个东西别混淆
这是 Compose 里最容易绕晕的地方,涉及三个不同的概念:
1. environment —— 直接注入容器
environment:
NODE_ENV: production
DATABASE_URL: postgres://user:pass@db:5432/app
API_KEY: # 不写值 = 从当前 shell 环境透传
2. env_file —— 从文件批量注入容器
env_file:
- .env.production
3. .env 文件 —— 只给 compose 文件自己做插值
放在 compose 文件同级目录的 .env,作用是替换 compose 文件里的 ${VAR} 占位符,它不会自动进到容器里:
# .env 内容:TAG=1.2.3
services:
api:
image: myapi:${TAG} # 变成 myapi:1.2.3
优先级:environment 里写死的值 > env_file 里的值。而 .env 与前两者不在一个维度上,它管的是 compose 文件的渲染,不是容器的环境。
别把密码提交进 git。 .env 应该进 .gitignore,仓库里放一份 .env.example 说明需要哪些变量。
depends_on:它不等服务就绪
这是新手最常踩的坑:
services:
api:
depends_on:
- db # 只保证 db 容器"启动了"
db:
image: postgres:16
depends_on 只控制启动顺序,不关心容器里的程序有没有准备好。Postgres 容器起来后还要几秒才能接受连接,而 api 已经开始连了,于是启动瞬间报 Connection refused。
正确做法是给被依赖方加 healthcheck,依赖方用长语法等它健康:
services:
api:
image: myapi
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
start_period 是宽限期:这段时间内健康检查失败不计入 retries,专门留给启动较慢的服务。
即便如此,应用侧仍然应该有连接重试。容器编排能做的只是尽量按顺序启动,数据库在运行期间重启的情况它管不了。
restart:四种策略
restart: unless-stopped
| 取值 | 行为 |
|---|---|
no | 默认值,退出后不重启 |
on-failure | 只在非零退出码时重启 |
always | 总是重启,包括你手动 stop 之后重启 Docker 守护进程 |
unless-stopped | 总是重启,但你手动 stop 过的不再自动拉起 |
生产环境用 unless-stopped。它和 always 的唯一区别是:你手动 docker compose stop 停掉一个服务后,重启机器或 Docker 守护进程时,always 会把它重新拉起来,unless-stopped 会尊重你的决定。跑一次就结束的任务(数据迁移、备份脚本)用 no 或 on-failure。
下一步
- 不想手写 YAML?用 Docker Compose 生成器 勾选服务直接出文件;
- 要 Nginx + Postgres + Redis 的完整可用示例,见 Docker Compose 配置示例大全;
- 起不来、连不上、数据丢了,见 Docker Compose 启动失败怎么排查。