「我把 docker compose up -d 跑了啊,容器也是 Up 状态,为什么浏览器打不开?」——上周一个朋友发来的原话。答案通常不在那一行命令里:端口没映射出来、应用在容器里监听了 127.0.0.1,或者它压根还没初始化完。从 clone 到真正跑通,中间隔着六七个 compose 不会替你想的坑。下面用 Gitea 走一遍,它官方自带 compose 文件。
准备:环境与前置条件
命令是 docker compose(带空格),不是 docker-compose(带连字符)。后者是独立 Python 写的 Compose v1,已经停止维护;现在用的是内置在 Docker CLI 里的 Go 插件。老教程里那个带连字符的写法,在新机器上往往直接 command not found。
顶层那个 version: '3' 不用写了。Compose Specification 已把它标记为过时,留着只多一条告警。
机器要给够资源。多容器项目里数据库、搜索引擎很吃内存,内存不够的表现不是慢,是容器直接退出、退出码 137(OOM 被杀)。起之前看一眼 free -h。
这步偷懒,后面每一步报错都长得像别的问题——我在搭建 mall 开发环境那篇里写过。
步骤一:用官方的 compose 文件,别自己造
这一步最容易被跳过,也最埋雷。第三方 compose 文件一搜一大把,缺健康检查、缺卷、端口裸奔,跑起来了就开始用。
正确做法是用仓库自带的,或直接抄官方的。Gitea 官方最小配置(SQLite 版):
networks:
gitea:
external: false
services:
server:
image: docker.gitea.com/gitea:28.0.0
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
restart: always
networks:
- gitea
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22" 注意它 pin 了具体版本号而不是 latest,这是有意的——Gitea 官方在 PostgreSQL 示例里写明:PostgreSQL 不会自己升级数据目录,跨大版本要 pg_upgrade 或 dump/restore。具体 tag 对照官方文档当时的最新版。
还有一条铁律:rootful 镜像和 rootless 镜像互不兼容,官方明令选定一种就别再换,不要靠改 compose 里的 image 值来回切。
步骤二:端口、卷、时区,先把这三个改对
拿到文件先别急着 up,改三处。
端口。"222:22" 这写法有讲究:左侧是宿主机端口,右侧是容器内端口。SSH 默认的 22 大概率已被宿主机占着,官方示例特意映射成 222。你的 3000 若被别的程序占了,改左边那个数即可:
ports:
- "3001:3000" # 宿主机 3001,容器内还是 3000
- "222:22" 另外,PostgreSQL、Redis 这类别往宿主机暴露 5432 / 6379,它们在 compose 默认私有网络里互通就够了,映射出来等于白送攻击面。
卷。官方示例用的绑定挂载 ./gitea:/data,文档紧跟一句警告:卷的所有者 UID/GID 要对上 USER_UID/USER_GID,权限不对容器就起不来,症状是反复重启、日志刷 permission denied。嫌麻烦就改用命名卷:
volumes:
gitea:
driver: local 官方说命名卷不用操心权限,Docker 自己处理;代价是不能直接翻文件,得 docker volume inspect 找路径。
时区。/etc/timezone、/etc/localtime 那两行只读挂载看着不起眼,少了它,容器里日志时间、定时任务触发时间全跟你差一截,排查时会把你带沟里。
步骤三:depends_on 不够,得加健康检查
你给应用加了 depends_on: - db,以为万事大吉。实际上 depends_on 只保证「容器启动了」,不保证里面的数据库已经初始化好、能接受连接。应用在数据库还在初始化时就去连,日志甩你一句:
sqlalchemy.exc.OperationalError: could not connect to server: Connection refused 或者 MySQL 场景下那句经典的 MySQL server has gone away。解法是给数据库加健康检查,依赖条件改成 service_healthy:
services:
db:
image: docker.io/library/postgres:18
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- ./postgres:/var/lib/postgresql
server:
depends_on:
db:
condition: service_healthy MySQL 用 mysqladmin ping。另外,容器里连另一个服务,主机名写服务名,不要写 localhost。官方写得很清楚——按上面这套 compose 起的数据库,主机名必须填 db。宿主机的 localhost 和容器网络里的 localhost 是两个东西。
结果校验
起:docker compose up -d,看状态 docker compose ps,追日志 docker compose logs -f 服务名。但「Up 状态」不等于「跑通」,这三件事要亲眼验一遍:
1. 端口真的通了。宿主机上 curl -I http://localhost:3000。不通的话常见原因是应用绑了 127.0.0.1 而不是 0.0.0.0——容器内的回环地址跟宿主机不是一回事,外面永远连不进来。
2. 走完应用的首次初始化。很多项目的 compose 只负责「把进程拉起来」,真正的安装还有一步浏览器向导。Gitea 就是:起来后访问 http://服务器IP:3000 跟着向导填库。HTTP 用的不是 3000 端口时,官方提醒要去 app.ini 改 LOCAL_ROOT_URL,否则生成的链接全是错的。
3. 数据真的落盘了。造一条数据,docker compose down 再 up -d,看还在不在。不在说明卷没挂对。别误用 down -v,那个 -v 会连命名卷一起删。
这步我吃过亏:只验了前两条就交付,对方 rebuild 一次数据全没。发票那套活里我写过——容错比准确率有时更重要,客户要的是结果,不是报错堆栈。放到这里就是:对方要的是一个重启之后还在的系统,不是一个刚刚还亮着的容器。
常见问题
容器起来了但浏览器打不开,第一步查什么?
按顺序:docker compose ps 看是不是 Up;docker compose port 服务名 容器端口 看映射;进容器 curl localhost:端口 确认进程在监听;最后才怀疑防火墙。bind: address already in use 是端口被占,改左侧那个数最简单。
日志刷 Connection refused,但数据库容器明明是 Up 的?
depends_on 只等启动不等就绪。加 healthcheck + condition: service_healthy,另外确认连接串里的主机名是服务名而不是 localhost。
数据卷权限报 permission denied?
绑定挂载要确保宿主机目录的属主 UID/GID 和容器里跑进程的那个用户一致(Gitea 就是 USER_UID/USER_GID)。不想管权限就换成命名卷。
怎么升级?直接 pull latest 吗?
官方给的流程是:修改 compose 里的版本号 → docker compose pull → docker compose up -d。追 latest 在开发环境图省事还行,正式环境建议 pin 版本,尤其是数据库不要随手跨大版本,pg_upgrade 比你想的麻烦。
这套能直接上生产吗?
Compose 的定位更偏开发与测试,官方也是这么提醒的。真要长期对外服务,至少补这几样:重启策略(restart: unless-stopped)、日志滚动(max-size/max-file,默认 json-file 不限制增长)、命名卷的日常备份,以及别把 .env 里的密钥提交进仓库。
如果你起的是偏运维向的项目,批量任务被封 IP、采一半崩掉这类问题多半不是 compose 能解决的,得回到应用层的重试与容错;做时间点对比的场景,记得像笔记批量采集那篇说的,把采集时间戳留下来。

