# 从裸机部署到全容器化：服务器现代化改造的完整路线

> 作者: Lazybones | 日期: 2026-09-05

# 从裸机部署到全容器化：服务器现代化改造的完整路线

> 本文是一份通用教程，梳理如何把一台跑着十几个裸机服务、apt 包和 systemd 单元散落各处的 Ubuntu 云主机，逐步重构成「全容器化 + 统一编排 + 自动 HTTPS + 可备份可恢复」的完整思路与常见坑。文中出现的域名、目录、端口、主机信息均为示例。

## 起点：裸机部署的混乱

假设你接手了一台云服务商 Ubuntu 24.04 LTS 主机，4 vCPU / 4GB 内存 / 60GB 磁盘，已经跑了半年以上，状态可以用「能跑，但没人敢动」来形容：

- **systemd 服务散落**：几个自建服务、早期的 Nginx、还有若干手写的 `.service` 单元，各自为政。哪个服务依赖哪个端口、崩了怎么拉起，全靠记忆。
- **apt 包与运行时混装**：PHP、Python、Redis、MariaDB 直接装在系统里，版本互相牵连。想升级 PHP 就得担心某个 PHP 应用崩，想动 Python 又怕影响别的东西。
- **端口冲突与裸露**：每个服务都想监听自己的端口，防火墙规则越加越乱，公网暴露面越来越大。
- **难维护、难迁移**：没有统一的编排描述，「这台机器现在到底跑了什么」这个问题回答不上来。真要换机器或重装，等于从零重建一遍心智模型。

最要命的是**没有可靠的备份与恢复路径**。数据散在系统目录、各种数据库文件、配置文件里，一旦磁盘故障或误操作，损失不可估量。

## 目标：现代化的四个硬指标

改造前先定下四条可验收的目标：

1. **全容器化**：能用容器跑的都用 Docker 跑，运行时依赖封进镜像，宿主机只保留 Docker 引擎和极少数必须裸机的服务（如需要直接操作网络栈/内核的网络代理）。
2. **统一编排**：所有服务用 Docker Compose 描述，统一放在 `/srv/stacks/` 下按服务分子目录，一条命令拉起/重建。
3. **自动 HTTPS**：反向代理层统一入口，证书自动签发续期，业务服务不再各自处理 TLS。
4. **可备份可恢复**：每日自动备份 + 定期恢复演练，备份不是「有」就行，而是「能恢复」。

## 第一步永远是全量备份

动手之前先备份，这是铁律。容器化重构会大量改动目录、数据库位置、网络配置，任何一步出错都可能丢数据。推荐做法：

- 对每个数据库（MariaDB、Redis、SQLite）先做一致性快照。SQLite 用 `docker cp` 从运行中的容器可写层拷出 live 数据库文件，再用 `PRAGMA integrity_check` 校验完整性，确认无损后才继续。
- 配置文件、compose 目录、密钥目录整体打包一份带时间戳的归档。
- 备份文件放到与源机隔离的位置，别把备份和原数据放同一个盘。

后文会反复看到，正是这个「先备份、校验、再迁移」的习惯，能在每一步都兜住底。

## 目录结构与 Docker 网络规范

重构后所有 Compose 项目统一收敛到 `/srv/stacks/` 下，一个服务一个子目录：

```text
/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 装的。迁移步骤：

1. 先在容器里起 MariaDB 10.11 和 Redis 7，挂命名卷做持久化。
2. 从裸机数据库 `mysqldump` 导出，导入容器实例。
3. 改业务应用的连接配置指向容器服务名 `mariadb` / `redis`。
4. 双跑验证一段时间，确认无异常后停掉裸机数据库服务。

Django 业务站用的是 SQLite，这里有一个非常隐蔽的坑：**`db.sqlite3` 被 `COPY app/` 打进了镜像**，而 compose 里挂的数据卷指向的却是一个从未被使用的空目录。结果每次 `compose up --build` 都会用镜像里的旧库覆盖容器可写层，运行期 migrate / 写入的数据（比如新建的博客文章）直接丢失，页面报 `no such table` 500。

根治办法是把数据库落到命名卷：

```python
# settings.py：数据库路径改到卷挂载点
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db' / 'db.sqlite3',   # 写入 app-db 卷
    }
}
```

```dockerfile
# 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/`。但**只备份不演练等于没备份**——建议定期做恢复演练：把备份拉到一台干净环境，按文档一步步还原，验证服务能起来、数据记录数对得上。演练中暴露的问题（比如某个卷没被备份脚本覆盖）比真出事时才发现要好得多。

## 经验总结

回看整个重构，几条经验最值钱：

1. **备份优先**：任何破坏性操作前先全量备份并校验，「先备份、校验、再迁移」形成肌肉记忆。
2. **compose 目录规范**：一服务一目录，统一根路径，配 `.dockerignore` 明确什么进镜像、什么落卷。数据永远落命名卷，不进镜像、不留在可写层。
3. **健康检查与渐进迁移**：新旧并存、逐个域名切换、验证通过再停旧的，不要一把梭。
4. **端口收敛**：容器互访走内部网络靠服务名解析，只有入口代理对外暴露，其余端口一律关闭。
5. **备份要能恢复**：定期恢复演练，用记录数比对确认零丢失。

下一篇讲 Traefik 自动 HTTPS 与 Authelia 统一登录门户的实践。
