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,回滚才有抓手。

把整棵树挪到另一台机器

某周末演练:

  1. 新机器装 Docker,创建 proxy 网络
  2. rsync /opt/stack 与 /data 对应卷
  3. 改各项目 .env 的域名与密钥
  4. 逐个 compose up -d,先 proxy 后业务

失败点通常长在树上:权限 uid 对不上、绝对路径写死成旧主机、忘记 external network、数据库大版本跳跃。演练一次比写十页「可迁移架构」踏实。我甚至故意在旧机器上 compose down 再在新机器拉起,确认没有「两台同时写同一块网络存储」的幻觉。

备份边界也按树划分:compose 与 .env 进私有 git(注意密钥);数据卷按服务分别备份——数据库用逻辑备份或官方工具,文件树用 restic。只备份卷文件而不备份「如何启动」,恢复时会猜谜。

Compose 不是编排银弹,它是家庭规模的秩序工具。目录干净、网络分层、版本钉死、能演练搬家,这套栈就能跟着你换硬盘、换主机、换城市。迷路时从 /opt/stack 重新走一遍,比从零搜教程快。树还在,人就还知道自己把生活数据放哪了。

← Back to Blog