从裸机部署到全容器化:服务器现代化改造的完整路线
本文是一份通用教程,梳理如何把一台跑着十几个裸机服务、apt 包和 systemd 单元散落各处的 Ubuntu 云主机,逐步重构成「全容器化 + 统一编排 + 自动 HTTPS + 可备份可恢复」的完整思路与常见坑。文中出现的域名、目录、端口、主机信息均为示例。
起点:裸机部署的混乱
假设你接手了一台云服务商 Ubuntu 24.04 LTS 主机,4 vCPU / 4GB 内存 / 60GB 磁盘,已经跑了半年以上,状态可以用「能跑,但没人敢动」来形容:
- systemd 服务散落:几个自建服务、早期的 Nginx、还有若干手写的
.service单元,各自为政。哪个服务依赖哪个端口、崩了怎么拉起,全靠记忆。 - apt 包与运行时混装:PHP、Python、Redis、MariaDB 直接装在系统里,版本互相牵连。想升级 PHP 就得担心某个 PHP 应用崩,想动 Python 又怕影响别的东西。
- 端口冲突与裸露:每个服务都想监听自己的端口,防火墙规则越加越乱,公网暴露面越来越大。
- 难维护、难迁移:没有统一的编排描述,「这台机器现在到底跑了什么」这个问题回答不上来。真要换机器或重装,等于从零重建一遍心智模型。
最要命的是没有可靠的备份与恢复路径。数据散在系统目录、各种数据库文件、配置文件里,一旦磁盘故障或误操作,损失不可估量。
目标:现代化的四个硬指标
改造前先定下四条可验收的目标:
- 全容器化:能用容器跑的都用 Docker 跑,运行时依赖封进镜像,宿主机只保留 Docker 引擎和极少数必须裸机的服务(如需要直接操作网络栈/内核的网络代理)。
- 统一编排:所有服务用 Docker Compose 描述,统一放在
/srv/stacks/下按服务分子目录,一条命令拉起/重建。 - 自动 HTTPS:反向代理层统一入口,证书自动签发续期,业务服务不再各自处理 TLS。
- 可备份可恢复:每日自动备份 + 定期恢复演练,备份不是「有」就行,而是「能恢复」。
第一步永远是全量备份
动手之前先备份,这是铁律。容器化重构会大量改动目录、数据库位置、网络配置,任何一步出错都可能丢数据。推荐做法:
- 对每个数据库(MariaDB、Redis、SQLite)先做一致性快照。SQLite 用
docker cp从运行中的容器可写层拷出 live 数据库文件,再用PRAGMA integrity_check校验完整性,确认无损后才继续。 - 配置文件、compose 目录、密钥目录整体打包一份带时间戳的归档。
- 备份文件放到与源机隔离的位置,别把备份和原数据放同一个盘。
后文会反复看到,正是这个「先备份、校验、再迁移」的习惯,能在每一步都兜住底。
目录结构与 Docker 网络规范
重构后所有 Compose 项目统一收敛到 /srv/stacks/ 下,一个服务一个子目录:
/srv/stacks/
├── proxy/ # 反向代理 + ACME 自动 HTTPS
├── auth/ # 统一登录门户
├── web-a/ # 业务应用 A(如 PHP / Python 应用)
├── web-b/ # 业务应用 B
├── db/ # 数据库(容器化)
├── cache/ # 缓存(容器化)
├── monitor/ # 服务监控
├── logview/ # 容器日志查看
├── scripts/ # 运维脚本(备份/同步/健康检查)
├── backups/ # 每日自动备份落地目录
├── secrets/ # 密钥(不纳入版本控制)
└── logs/
网络方面,所有需要互访的容器挂到同一个自定义 Docker 网络里,靠服务名互相解析(如 PHP 应用连 mariadb、redis),端口不再往宿主机上乱映射。只有真正需要对外暴露的入口(Traefik 的 80/443)才映射到宿主机。
反向代理层:从 Nginx 切到 Traefik
原来宿主机上的 Nginx 手动维护着一堆 server 块和证书路径,续期靠 cron + certbot,改一次配置提心吊胆。更省心的做法是整体换成 Traefik v3.7.12:
- 自动 HTTPS:内置 ACME,对接 Let's Encrypt,证书自动签发、自动续期,业务服务完全不碰 TLS。
- 动态配置:路由规则走 File Provider(
/srv/stacks/traefik/config/dynamic/services.yml),改配置不用重启整个代理。 - 统一入口:所有域名收敛到 443 一个端口。
切换建议渐进进行:先让 Traefik 与旧 Nginx 并存,把域名一个个迁到 Traefik 的标准端口上,验证通过后再停掉裸机 Nginx。最终所有对外域名都走 Traefik:
| 域名(示例) | 后端服务(示例) |
|---|---|
| app.example.com | 某个业务 Web 应用 |
| status.example.com | 监控面板 |
| logs.example.com | 日志查看器 |
| auth.example.com | 统一登录门户 |
关于 Traefik 的两个大坑——Docker Provider 与 File Provider 的路由竞争导致认证被绕过、以及某日志查看器的 Service Worker 撞上 ForwardAuth——会在系列第二、三篇里专门展开。
数据层容器化:MariaDB / Redis 与数据迁移
某个 PHP 业务应用依赖的 MariaDB 和 Redis 原来是裸机 apt 装的。迁移步骤:
- 先在容器里起 MariaDB 10.11 和 Redis 7,挂命名卷做持久化。
- 从裸机数据库
mysqldump导出,导入容器实例。 - 改业务应用的连接配置指向容器服务名
mariadb/redis。 - 双跑验证一段时间,确认无异常后停掉裸机数据库服务。
Django 业务站用的是 SQLite,这里有一个非常隐蔽的坑:db.sqlite3 被 COPY app/ 打进了镜像,而 compose 里挂的数据卷指向的却是一个从未被使用的空目录。结果每次 compose up --build 都会用镜像里的旧库覆盖容器可写层,运行期 migrate / 写入的数据(比如新建的博客文章)直接丢失,页面报 no such table 500。
根治办法是把数据库落到命名卷:
# settings.py:数据库路径改到卷挂载点
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db' / 'db.sqlite3', # 写入 app-db 卷
}
}
# Dockerfile:预建目录,并在 .dockerignore 里排除 db.sqlite3,不再烘焙进镜像
WORKDIR /srv/app
RUN mkdir -p /srv/app/db
迁移流程严格遵守「备份 live 库 → PRAGMA integrity_check 校验 → 播种到卷 → 改 settings 指向卷 → 重建容器 → 逐表记录数比对」。这样做可以做到全表记录数 100% 一致、零丢失,顺带还能让镜像体积明显瘦身。教训:SQLite 必须放命名卷,绝不能躺在镜像内或容器可写层,否则重建即丢数据。
业务服务容器化
- Django + Gunicorn:主站用 Gunicorn 起 WSGI。这里也有一个坑——升级到 Gunicorn 26.x 后,
--keepalive参数已改名--keep-alive(带连字符),旧写法会导致unrecognized arguments直接退出、容器陷入 Restarting 循环。改成--keep-alive 5后正常,其余--max-requests、--workers、--threads、--timeout在 26.x 仍有效。经验:升级/调优参数前先gunicorn --help | grep <flag>核对当前版本的准确参数名。 - PHP 应用 + PHP-FPM:PHP 8.3 运行时封进镜像。为了让 rate limiting 生效,需要补装 PHP Redis 扩展(
pecl install redis+docker-php-ext-enable redis),并加上libhiredis-dev、sockets扩展。用sed改.config.php里$_ENV['enable_rate_limit']时,因为含$'_特殊字符,内容替换会反复静默失败,改成按行号定位sed -i '49s/false/true/'才成功。 - PHP 应用的 composer 陷阱:Dockerfile 里
RUN composer install ... 2>/dev/null || true会长期静默失败——镜像缺ext-mysqli/ext-yaml/ext-gmp,加上国内网络从 GitHub 拉依赖易超时。旧镜像能用,全靠COPY app/把宿主机现成的vendor/烘焙进去。一旦在.dockerignore里排除vendor/改由镜像内生成,就会得到「构建成功但 vendor 缺失」的残缺镜像。正确做法是保留 vendor 烘焙、补齐 PHP 扩展、并删掉|| true让失败暴露出来。
管理与监控工具容器化
除了业务应用,监控与日志类工具也可以一并容器化,遵守同样的「数据落命名卷 + 只经反代暴露」原则:
- 监控工具:Uptime Kuma v2(镜像标签
louislam/uptime-kuma:2)。用浏览器自动化建 TCP Monitor 时踩过一个前端坑——Vue 3 的下拉框点击不触发响应式更新,直接select.value='port'+dispatchEvent(new Event('change'))才生效。 - 日志查看工具:容器日志查看器,受 Authelia 保护,只经反代到内部域名访问,不直接对公网暴露。
有些带「安全登录 / 可信域名」机制的管理后台,经反代访问时会返回 "Access Temporarily Unavailable" 之类的错误。这通常不是路由配错了,而是后台自身的安全机制在拦截——需要在它的配置里显式声明可信域名(例如把
BindDomain设为反代域名)后重启服务。不同产品的配置项不同,遇到报错先查它的安全机制文档。
端口收敛与旧环境清理
容器化完成后,对外的 TCP 端口收敛到少数几个,攻击面大幅缩小(下表端口号均为示例):
| 端口(示例) | 服务 |
|---|---|
| 22 | SSH |
| 80 | Traefik(跳转 HTTPS) |
| 443 | Traefik(承载所有域名) |
| 自定义高位端口 | 确有需要、且无法放进容器网络的少量服务 |
UDP 端口同理,只保留业务确实需要的那些。清理旧环境时,逐个 systemctl stop && disable 停掉裸机的 Nginx、数据库、旧 PHP 服务,确认容器版本接管后再卸载 apt 包。需要直接操作网络栈/内核的服务评估后可以保留裸机——全容器化不是教条,该裸机的就裸机。
每日备份 + 恢复演练
最后一块拼图是备份。策略可以是每日凌晨 3 点自动备份,保留 7 天日备 + 周备,落到 /srv/stacks/backups/。但只备份不演练等于没备份——建议定期做恢复演练:把备份拉到一台干净环境,按文档一步步还原,验证服务能起来、数据记录数对得上。演练中暴露的问题(比如某个卷没被备份脚本覆盖)比真出事时才发现要好得多。
经验总结
回看整个重构,几条经验最值钱:
- 备份优先:任何破坏性操作前先全量备份并校验,「先备份、校验、再迁移」形成肌肉记忆。
- compose 目录规范:一服务一目录,统一根路径,配
.dockerignore明确什么进镜像、什么落卷。数据永远落命名卷,不进镜像、不留在可写层。 - 健康检查与渐进迁移:新旧并存、逐个域名切换、验证通过再停旧的,不要一把梭。
- 端口收敛:容器互访走内部网络靠服务名解析,只有入口代理对外暴露,其余端口一律关闭。
- 备份要能恢复:定期恢复演练,用记录数比对确认零丢失。
下一篇讲 Traefik 自动 HTTPS 与 Authelia 统一登录门户的实践。