我为什么把自己的 Homelab 整理成一个 Git 仓库
折腾 Homelab 这件事,一开始往往都挺随意的。
先是一台云服务器,跑个数据库、跑个反向代理,再放几个自己常用的小服务。后来又多了一台便宜 VPS,一台家里的小主机,可能还会临时加一台测试机器。服务也慢慢变多:密码库、内网穿透、文件列表、定时任务、远程桌面、各种自己写的小应用。
刚开始我也觉得,用 Docker Compose 就够了。每个服务一个目录,docker compose up -d,看起来很清爽。
但时间一长,问题就冒出来了。
哪台机器上跑了哪些服务?某个服务的 .env 应该放在哪里?迁移服务器的时候,到底要先备份数据库,还是先停服务?某个应用依赖的 Redis 是本机的,还是另一台机器上的?更要命的是,等真出故障的时候,很多操作全靠当时的记忆,而记忆这东西在凌晨三点通常不太可靠。
所以我后来干脆把 Homelab 当成一个小型基础设施项目来管理:用 Git 记录结构,用 Docker Compose 管服务,用脚本约定部署、备份、恢复和迁移的入口。
这就是这个仓库的由来:
它不是那种下载下来就能直接跑满一整套生产环境的项目,更像是一个可扩展的 Homelab 骨架。你可以把它 clone 到自己的服务器上,然后按自己的机器、域名、密钥和服务逐步补齐。
我想解决的不是“怎么启动一个容器”
单个服务启动起来其实不难。
真正麻烦的是服务越来越多之后的日常维护。比如:
新增一台服务器时,基础目录、Docker、Restic、配置文件要怎么准备;
多台服务器分别运行哪些服务,要不要靠 README 里人工记录;
MySQL、PostgreSQL、Redis 这种公共服务,是每个应用各起一份,还是统一管理;
真实密码、Token、证书这些东西放在哪里,怎么避免误提交;
更新服务前要不要备份,备份产物放在哪里;
服务器迁移时,怎么保证能按顺序恢复数据、重新部署、再检查状态。
这些问题单独看都不大,但它们加起来就是 Homelab 后期最消耗耐心的地方。
所以这个项目没有把重点放在“提供一个超复杂的自动化平台”上,而是先把几个关键约定固定下来:目录怎么放、主机怎么声明、服务怎么分层、脚本入口叫什么、备份恢复怎么走。
有了这些约定之后,后面接新服务就不再是从零开始琢磨,而是照着同一套结构往里填。
简单说说设计思路
这个仓库的核心思路很朴素:
Git 仓库
-> hosts/*.yml 记录每台服务器要跑哪些服务
-> services/shared 放 MySQL、PostgreSQL、Redis 这类公共基础服务
-> services/apps 放具体应用服务
-> scripts/*.sh 提供部署、更新、备份、恢复、迁移入口
-> docs/*.md 记录架构和运维流程
我比较喜欢这种方式,是因为它足够直观。打开仓库,大概扫一眼目录,就能知道这个 Homelab 是怎么组织的。
hosts 目录负责描述主机。比如 hosts/oracle.yml 里写了 Oracle VPS 要运行 shared/mysql、shared/postgres 和 shared/redis;hosts/azure.yml 里则放了 PostgreSQL、Nginx Proxy Manager、Resource Tracker 和 CLI Proxy API。
services/shared 放公共依赖。数据库、缓存、反向代理这类基础能力如果每个应用都各自起一套,后期维护会很碎。把它们作为 shared 服务管理,应用通过 Docker 网络或 Tailscale 内网去连接,迁移时也更容易收口。
services/apps 放具体应用。每个应用目录尽量保持类似结构:
services/apps/example-app/
├── compose.yml 或 docker-compose.yml
├── .env.example
├── backup-exec
├── restore-exec
└── README.md
这里有两个小约定很重要。
第一,仓库只保存模板,不保存真实密钥。生产 .env、Token、证书、私钥都应该放到服务器的 /opt/homelab/config,不要进 Git。
第二,统一脚本只负责编排,具体服务自己负责细节。比如备份时,scripts/backup.sh 读取主机清单,找到服务目录里的 backup-exec,然后把统一的环境变量传进去。至于 MySQL 要 dump 成 all.sql.gz,Redis 要导出 dump.rdb,Vaultwarden 要备份哪个数据目录,这些由服务自己的脚本决定。
这个边界让我后面加服务时轻松很多。全局脚本不用理解每个应用的内部结构,应用也不用知道整个 Homelab 的部署拓扑。
推荐的服务器目录
在服务器上,我建议统一使用下面这套目录:
/opt/homelab/
├── repo/ # 本仓库 clone 位置
├── config/ # 真实环境变量、应用配置、证书、密钥
├── data/ # 数据库、应用持久化数据
├── backup/ # 本地备份产物
└── logs/ # 定时任务和维护日志
这个约定看起来普通,但实际很有用。因为一旦目录固定,备份、恢复、迁移脚本就有了共同语言。以后换机器时,也不需要临时猜某个服务的数据到底被挂到了哪里。
快速使用
先在服务器上准备目录并克隆仓库:
sudo mkdir -p /opt/homelab
sudo chown "$USER":"$USER" /opt/homelab
git clone https://github.com/DYS7516461/homelab.git /opt/homelab/repo
cd /opt/homelab/repo
给脚本加执行权限,然后初始化服务器:
chmod +x scripts/*.sh
./scripts/bootstrap-server.sh --host oracle
如果是中国大陆服务器,可以指定 Docker apt 镜像源:
./scripts/bootstrap-server.sh --host oracle --docker-apt-mirror aliyun
初始化脚本会安装基础工具、Docker、Docker Compose plugin、Restic,并创建 /opt/homelab/{repo,config,data,backup} 等目录。脚本如果把当前用户加入了 docker 用户组,记得退出 SSH 后重新登录一次,让用户组变更生效。
接着准备真实配置。仓库里只放 .env.example,生产配置建议复制到 /opt/homelab/config 后再修改:
cp services/shared/mysql/.env.example /opt/homelab/config/mysql.env
cp services/shared/postgres/.env.example /opt/homelab/config/postgres.env
cp services/shared/redis/.env.example /opt/homelab/config/redis.env
然后编辑这些文件,把里面的 change_me 换成真实密码:
chmod 600 /opt/homelab/config/*.env
部署前可以先 dry-run 看看脚本会做什么:
./scripts/deploy.sh oracle --dry-run
确认没问题后再真正部署:
./scripts/deploy.sh oracle
只部署某个服务也可以:
./scripts/deploy.sh oracle --service shared/mysql
查看服务状态:
./scripts/deploy.sh oracle --action ps
如果只是想手动跑某个 Compose,也没问题:
cd services/shared/mysql
docker compose -f compose.yml up -d
日常更新和备份
日常更新时,可以用:
./scripts/update.sh --host oracle
如果希望更新前先做一次备份:
./scripts/update.sh --host oracle --with-backup
如果已经配置了 Restic,并且希望备份后上传到远端仓库:
./scripts/update.sh --host oracle --with-restic
单独备份某个服务:
./scripts/backup.sh --host oracle --service shared/mysql
备份当前主机清单中的所有可备份服务:
./scripts/backup.sh --host oracle
加上 Restic 上传:
./scripts/backup.sh --host oracle --with-restic
Restic 默认读取:
/opt/homelab/config/restic.env
示例配置长这样:
export RESTIC_REPOSITORY=s3:https://<account-id>.r2.cloudflarestorage.com/homelab
export RESTIC_PASSWORD=change_me
export AWS_ACCESS_KEY_ID=change_me
export AWS_SECRET_ACCESS_KEY=change_me
看快照:
./scripts/restic-maintain.sh --snapshots
预览保留策略:
./scripts/restic-maintain.sh --forget --dry-run
执行保留策略并检查仓库:
./scripts/restic-maintain.sh --forget --check
恢复和迁移
恢复某台主机的服务:
./scripts/restore.sh --host oracle --timestamp 20260623-120000
只恢复某个服务:
./scripts/restore.sh --host oracle --service shared/mysql --timestamp 20260623-120000
如果备份在 Restic 里,可以先从 Restic 恢复备份目录,再执行服务恢复:
./scripts/restore.sh --host oracle --timestamp 20260623-120000 --from-restic
迁移服务器时,可以用迁移脚本把流程拆开:
./scripts/migrate-server.sh --source-host oracle --target-host azure --timestamp 20260623-120000 --phase backup --with-restic
./scripts/migrate-server.sh --source-host oracle --target-host azure --timestamp 20260623-120000 --phase restore --with-restic
./scripts/migrate-server.sh --source-host oracle --target-host azure --timestamp 20260623-120000 --phase deploy
./scripts/migrate-server.sh --source-host oracle --target-host azure --timestamp 20260623-120000 --phase verify
我个人会建议,别等真要迁移时才第一次跑这些命令。找一台临时机器演练一次恢复流程,比写十页文档都管用。
怎么新增服务
新增服务时,我通常按这个顺序来:
在
services/apps或services/shared下创建服务目录。添加
compose.yml或docker-compose.yml。添加
.env.example,只写示例值,不写真实密钥。如果需要统一备份,添加
backup-exec,把备份产物写入SERVICE_BACKUP_DIR。如果需要统一恢复,添加
restore-exec,从SERVICE_RESTORE_DIR读取备份产物。在目标主机的
hosts/<host>.yml中加入服务路径。用
deploy.sh --dry-run检查部署计划,再真正启动。
比如:
services/apps/my-app/
├── compose.yml
├── .env.example
├── backup-exec
├── restore-exec
└── README.md
然后在 hosts/tencent.yml 里加:
hostname: tencent
services:
- apps/my-app
之后就可以:
./scripts/deploy.sh tencent --service apps/my-app --dry-run
./scripts/deploy.sh tencent --service apps/my-app
它目前适合什么人
如果你只是在一台机器上跑两三个容器,这套结构可能会显得有点正式。
但如果你已经有多台服务器,或者准备长期维护一堆自托管服务,我觉得把这些东西早点整理起来是值得的。它不会让运维工作消失,但能减少很多“我当时到底怎么配的”这种低质量焦虑。
这个项目目前还是一个骨架,很多地方故意没有做得太重。比如 host YAML 解析只支持简单列表,脚本默认在目标服务器本机执行,不负责远程 SSH 分发,部分服务模板上线前也还需要按自己的环境补健康检查、安全参数和真实配置。
但我反而喜欢它现在这个状态:够用、清楚、可改。Homelab 最怕的不是不够自动化,而是自动化到最后自己都不敢动。
如果你也在维护自己的小型服务器群,可以直接拿这个仓库改起:
先从一台机器、一个 shared 服务、一个应用开始。等部署、备份、恢复这条链路跑通之后,再慢慢把更多服务迁进去。Homelab 不一定要一步到位,但最好从第一天开始就给未来的自己留条清楚的路。