Blog
用 Docker Compose 管理个人服务栈
服务超过三个之后,我不再 docker run 长命令。全部收进 Compose,目的只有一个:换机器时,拷目录 + 改 .env 就能起。这篇文章不按「概念章节」写,按目录树从上往下走——你的眼睛扫到哪,配置就解释到哪。
曾经有一周我把五个服务的启动命令记在备忘录里,换行都是反斜杠。后来硬盘坏道重装,备忘录还在,路径和卷名对不上,恢复像考古。从那天起我认定:秩序必须落在磁盘目录上,不能只落在脑海里。
先站在 /opt/stack 门口
/opt/stack/
README.md
proxy/
immich/
gitea/
_shared/
README.md 写人话:有哪些服务、域名、备份落点、昨晚改过什么。三个月后的你不会记得「为什么 Gitea 不走 3000 而走反代」。
_shared/ 放 .env.example 和公共说明,不放真密钥。真密钥在各项目 .env,权限收紧,不进 git。
每个业务一个子目录,不共享一个巨型 compose(除非强依赖)。巨型文件合并冲突凶、回滚颗粒粗。家庭规模「多文件 + 共享网络」比「单文件英雄」稳。
走进 proxy/:反代单独成栈
/opt/stack/proxy/
docker-compose.yml
# 可选:动态配置、证书缓存目录说明
这里通常跑 Traefik 或 Caddy。它加入 external network proxy,别的服务按需接入。创建网络一次即可:
docker network create proxy
原则:只有需要被反代的服务加入 proxy;数据库容器留在项目内部网络,不进 proxy。公网扫描到反代不等于扫到 Postgres 端口——前提是你没把数据库也挂到同一张「露脸网」。
proxy 栈我习惯最先 compose up,最后 compose down。业务容器可以随便折腾,入口层稳着,域名才不会集体 502。目录上它和业务平级,心里却要把它当基础设施。
走进 immich/:看一份典型 compose 骨架
/opt/stack/immich/
docker-compose.yml
.env
骨架我会写成这样(示意):
services:
app:
image: example/app:1.2.3
restart: unless-stopped
env_file: .env
networks:
- internal
- proxy
volumes:
- /data/app:/data
labels:
- traefik.enable=true
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:16
networks:
- internal
volumes:
- /data/app/db:/var/lib/postgresql/data
networks:
internal:
proxy:
external: true
逐项在目录语境里解释:
- image tag 钉死:少用
latest。更新是主动行为,不是抽奖。 /data/...绝对路径:数据在 compose 目录外,重装系统时栈目录和数据目录可分别恢复。env_file: .env:密钥不进 yml,yml 可以进私有 git。restart: unless-stopped:主机重启后服务自己起来;你手动 stop 的不会硬拉起。- logging 限大小:默认可把小盘写满,先限住。
时区统一塞进 environment 或 .env:
environment:
TZ: Asia/Shanghai
走进 gitea/:同样的树,不同的数据重量
/opt/stack/gitea/
docker-compose.yml
.env
Gitea 的仓库目录往往很大。目录约定仍然是「compose 在 /opt/stack/gitea,裸库在 /data/gitea」。有人把数据放进项目下 ./data,搬家时容易和 git 仓库本身搅在一起,我更喜欢物理分离。
depends_on 只保证启动顺序,不保证就绪。数据库要稳,用 healthcheck + 条件依赖,或接受「先失败再重启」的 unless-stopped 循环。别迷信「写了 depends_on 就永远不报 connection refused」。
走进 _shared/ 与 README:约定比工具重要
/opt/stack/_shared/
.env.example
.env.example 列出变量名和假值,注释里写「从哪里申请、轮换周期」。新机器上 cp .env.example .env 再填真值,比从聊天记录翻密钥文明。
README 里我固定有一张表:服务名、compose 路径、数据路径、是否进 proxy、备份命令入口。目录树负责「在哪」,表负责「怎么动」。
有人嫌目录碎。碎是有意的:单个服务炸了,你只进那个文件夹修,不在两千行 yml 里翻海。家庭实验室最怕的不是「不够优雅」,是半夜找不到该改哪一行。
在这棵树下如何更新
站在某个服务目录里:
cd /opt/stack/immich
docker compose pull
docker compose up -d
docker compose logs -f --tail=100
出问题就把 compose 里的 tag 改回上一版再 up -d。系统 apt upgrade 和批量容器 pull 错开;别在周五晚上同时做——出问题你要睡觉。
watchtower 全自动更新我不用在关键服务上。家庭实验室可以懒,不能无脑。树还在,版本号在 yml 里可 diff,回滚才有抓手。
把整棵树挪到另一台机器
某周末演练:
- 新机器装 Docker,创建
proxy网络 - rsync
/opt/stack与/data对应卷 - 改各项目
.env的域名与密钥 - 逐个
compose up -d,先 proxy 后业务
失败点通常长在树上:权限 uid 对不上、绝对路径写死成旧主机、忘记 external network、数据库大版本跳跃。演练一次比写十页「可迁移架构」踏实。我甚至故意在旧机器上 compose down 再在新机器拉起,确认没有「两台同时写同一块网络存储」的幻觉。
备份边界也按树划分:compose 与 .env 进私有 git(注意密钥);数据卷按服务分别备份——数据库用逻辑备份或官方工具,文件树用 restic。只备份卷文件而不备份「如何启动」,恢复时会猜谜。
Compose 不是编排银弹,它是家庭规模的秩序工具。目录干净、网络分层、版本钉死、能演练搬家,这套栈就能跟着你换硬盘、换主机、换城市。迷路时从 /opt/stack 重新走一遍,比从零搜教程快。树还在,人就还知道自己把生活数据放哪了。