我开始认真折腾个人 self-host,大概已经有五六年了。严格意义上开始管理我自己的 VPS 的时间点,应该还可以往前追溯。最初的动机其实很简单:有些服务我每天都在用,比如密 码管理器、音乐播放、笔记同步、个人网站、代码仓库等等。既然如此,为什么不干脆自己 部署?这样一来数据在自己手里,也不用被各种平台订阅和涨价牵着走。
这几年一路走下来,说实话,self-host 并不省心。我要维护系统、备份数据、关心安全、 偶尔还要救火。但它给我的回报也很明确:
- 数据完全掌控在自己手里
- 不再被订阅价格牵着走
- 对系统、网络和安全的理解明显更深
现在我一台服务器跑着十几个服务,成本只是一台服务器的钱。在这两年取消了不少第三方 平台的订阅后,算下来,整体成本可能反而更低。对我来说,这种“复杂但可控”的状态,反 而比依赖一堆外部服务更安心。当然不这适合所有人,我感觉有:有一定技术背景的人、喜 欢折腾的人以及愿意为稳定与掌控付出时间的人,这三者合一就适合这样。
到现在,我并不觉得自己成了什么“专家”,但有到现在两件事的感受越来越强烈:安全性 以及服务器环境的整洁程度,这也是这篇文章的两个主线。这两个问题,几乎决定了 你这台服务器上的各类服务能不能长期稳定地用下去。然后就是需要迁移的时候,比如换了 一个更便宜或者更好的 VPS 供应商,这个时候的工作量(我自己就换了好几次,知道真的 换不动了才稳定下来)。
草莽阶段
最开始的时候,我的做法非常朴素。我会直接在宿主机上安装服务,然后安装 Nginx 或者 Apache2(httpd)。比如用 Nginx 做反向代理,Nginx 这类 Web 服务器 的好处是配置文 件可以拆开,每个站点一份,用域名来区分各个服务而不是端口号,看起来井井有条。那时 候我觉得这套方案挺优雅的。
但随着服务慢慢变多,问题开始一点点浮现。有的服务需要特定版本的依赖,我很难在同一
个环境安装不同版本的各种依赖库或者数据库。而有的会在系统里留下各种临时文件;配置
文件散落在 /etc、数据放在 /srv,日志又在 /var。一开始你还能记得“这是哪个服
务的”,时间一长就完全混在一起了。
这期间,当我想删除一个不用的服务时,我已经不敢确定自己删得干不干净。特别是有的服 务提供“一键安装脚本”,安装的时候一时畅快,等到了你想摆脱它的时候,你会发现当初的 脚本不知道做了什么改动,如何复原都毫无头绪。你很难确认还有没有残留的配置、后台进 程,甚至不知道删错东西会不会影响别的服务。这种不确定感,本身就是一个风险,对于强 迫症来说,每每想起简直是如鲠在喉。
容器化阶段
后来我把几乎所有服务都迁移到了 Docker 里。而且,到现在越来越倾向于将所有能用的到 的服务或者工具软件容器化,再去部署。有些不提供标准的容器化部署方案的,但又很想用 的服务,我会直接自己进行容器化包装,形成自己的容器化的解决方案。
真正改变体验的不是 Docker 本身,而是 Docker Compose。容器这东西,本质上就不 应该被当成“长期维护对象”,它随时都可以被删掉、重建,你需要意识到这个。早期的时 候,我觉得 Docker Compose 配置文件麻烦,为什么不用命令直接创建一个容器,然后让这 个容器长期运行。后来等我要更新,或者这个服务依赖多个底层的其他容器的时候,问题就 逐渐浮现出来。如果当初将对服务的部署写下来,写到一个配置文件中,然后每次就执行这 个配置文件就能完美复现原先的部署,不需要自己额外配置什么 Bridge 网络,或者什么 hostname,亦或者是重启策略。这个节省了大量的时间和精力,并且配置的时候出错的可能 性,也大大降低了。
到现在,我能够总结出:真正需要被长期维护的,只有三样东西:
- 服务配置(包括版本,网络,存储等)
- 数据(主要是 Volumes 和其他映射的目录)
- 部署配置(Docker Compose 的各类 Yml 配置文件)
不管是重装系统,还是换一台机器,只要这三者还在,这些服务就能在这个新的地方完美地 克隆。而且,宿主机本身终于变得“干净”了。各个目录下不再到处是你几年之前装过、现在 已经忘记用途的东西。所以,在这个阶段,我单独创建了一个 git 仓库来收集和管理所有 我用到的服务的对应的 Docker Compose 配置文件,一年下来,我感觉这样是经得起各类场 景的考验的。
一个服务一个数据库
这个是一个值得一提的点。很长一段时间里,我的所有服务共用一个数据库实例。这在逻辑 上看起来非常合理:资源省、结构简单、管理集中。但真正长期维护下来,你会发现这其实 是一个非常不友好的设计。
每多一个服务,就多一套用户、权限、密码;想迁移一个服务,就得小心翼翼地把数据库剥 离出来;数据库版本一旦升级,所有服务都要跟着承担风险。对于不熟悉某个数据库的人来 说,管理权限和用户是一件头疼的东西,你需要通过命令行或者什么数据库管理软件,仔细 地区分和妥善保管各个服务的用户以及他们的密码。
后来我干脆反过来做了一件事:**每个服务,单独一个数据库实例。**也就是,每个 Docker Compose 中独立地会有一个数据库的 service。长期观察下来,对个人 self-host 来说,这并没有想象中那么“浪费”。一个数据库容器通常只占一两百兆内存,在一台 4c8g 的 VPS,甚至 2c4g, 或家用服务器上,是可以容忍的。
但换来的好处是非常实在的:服务之间彻底隔离,大大提高了安全性,然后后续也不用再为 权限和兼容性焦头烂额。直接一件将所有数据和配置打包带走即可,每个数据库就只有这个 对应服务的数据。
最小暴露面原则
早期我几乎把所有服务都直接暴露在公网,用反向代理加登录验证来保护:
- 一个 Nginx Proxy Manager
- 所有服务都有公网域名
- 靠应用层用户名/密码防护
当时我也觉得:“所有服务都设置有用户名和密码,应该没问题吧?”,后来我慢慢才意识 到,这就是自己骗自己。只要其中一个服务出现:暴力破解漏洞、认证绕过或者 RCE(远程代码执行),攻击者就可能拿到容器权限甚至宿主机权限。最初的是我遇到的情 况是,我非容器部署的 gitlab 服务,在 gitlab 有次出现严重的远程执行漏洞后,我没有 及时知道这件事。然后某一天,我发现服务器就被注入了木马,这还是 Cloud Provider 提 醒我我才知道的。如果那个时候我的服务器上存着照片、视频、备份数据,后果会是灾难性 的。
另外不同服务的安全成熟度差异巨大,技术栈五花八门;有些项目还很年轻,对安全的考虑 远不成熟;我也不可能每天盯着各类安全公告。一旦其中某一个服务出现漏洞,或者这些服 务用得框架或者依赖,比如最近的 React 技术栈相关的安全漏洞,那这个或者这些服务就 可能在短时间内爆雷。如果这个时候容器化以及隔离做的不好,攻击者拿到的不一定只是那 个服务,而很可能是整台机器。然后就是隐藏的木马脚本,或者挖矿脚本。
这也是我后来形成的一个核心原则:能不对公网开放的服务,就不要对公网开放。博客、静 态页面这种,被攻击了损失也有限;但笔记、密码管理器、管理后台这种东西,一旦出事就 是灾难。所以,需要好好评估自己现在对外暴露的服务。以我个人的经验来看,最能够让人 放心的就是各类静态网站,或者只是通过参数生成页面没有数据库的。但是,很多服务我都 想用,如何做到有些暴露有些不暴露?因此,我现在的实践是只对公网暴露真正需要的服 务,比如静态博客、极少数公共服务。Git 仓库尽量不对外或只暴露只读静态视图(通过 gitweb 或者 cgit 暴露),而动态服务、管理后台、笔记、密码管理器等一律不直接暴露 在公网。
部署私有 VPN
在后来,我发现解决这个问题的关键,是 VPN。我在服务器上部署了一个私有的 VPN(比如 WireGuard),把所有私有服务全部放到只在内网可访问的网络里。只有当我连接 VPN,拿到一个虚拟内网 IP 后,然后以这个内网 IP 为起点,才能访问这些服务。整体思 路是:
- VPN 提供一个虚拟私有网段(如
10.8.x.x) - Docker 内部服务使用另一个私有网段(如
172.x.x.x) - 两个网段通过路由互通
- 公网无法直接访问这些服务
这一步的意义在于:我不再需要为每个服务单独设计“防御方案”,因为它们根本就不在攻击 面上。再配合一个只在内网工作的反向代理和 DNS,使用体验反而比公网还好:域名清晰、 服务稳定,而且非常安心。
内部反代与域名解析
进一步优化体验,我在内网部署一个 内部 Nginx 容器(在 172.24.0.0/16 下),和对外 提供服务的 Nginx 实例(比如 172.20.0.0/16)完全隔离开来。这个内部 Nginx 实例固定 绑定到私有 IP(比如 172.24.0.2),只服务通过 VPN 网络连接进来的流量。
flowchart LR
%% Clients
PUB["Public Internet User"] -->|"HTTPS"| PNGX["Public Nginx (*.example.com)"]
PNGX --> PBLOG["Public Service (e.g., blog/static)"]
%% VPN access
DEV["Your Device\n(WireGuard Client)"] -->|"VPN Tunnel"| WG["WireGuard Server (10.8.0.0/24)"]
%% Internal name + reverse proxy (VPN-only)
WG -->|"DNS"| DNS["Internal DNS (172.24.0.4)"]
WG -->|"HTTPS"| INGX["Internal Nginx (172.24.0.2)"]
%% Private services
subgraph DOCKER["Docker Private Networks"]
INGX --> VAULT["Vaultwarden"]
INGX --> NOTES["Notes / Joplin"]
INGX --> GIT["Git Service"]
end
然后,再加一个强制的内部 DNS 服务,所有通过 VPN 进来的流量都只能够使用这个 DNS 提供的域名解析服务。实际的成熟的部署方案有很多,比如 AdguardHome 等,这还能够同 时解决广告拦截、域名劫持问题。这个 DNS 服务除了解析通过的普通的网络流量外,还专 门代理*.vpn.example.com 下的所有 DNS 解析,比如:
git.vpn.example.comnotes.vpn.example.comvault.vpn.example.com
这些 DNS 都统一解析到内部的 Nginx 的监听的 IP,172.24.0.2。这样在 VPN 已连接的情 况下,后续通过浏览器直接输入对应的域名,就能够直接访问对应的内部服务。所有流量都 在内网完成。这个域名,最好是要在自己名下,然后在外部 DNS 管理 中,*.vpn.example.com 设置为 NXDOMAIN 或者 VPS 的 IP 地址,来确保如果不小心没有 通过 VPN 来访问内部服务,流量不会被意外转发到其他地方去。
隔离数据库容器
数据库不应该和其他无关服务在同一个 Docker 网络里。为此我又做了一件事:让每个服 务、数据库单独一个 isolated bridge,在这个 bridge 下只有这两个容器能互相通信且该 bridge 不连接外部网络。也就是说,一个服务即便被攻破,它能接触到的,也只有自己的 数据库,而不是整台服务器上所有的数据库。比如这样:
services:
db:
image: postgres:16-alpine
...
networks:
- iso
restart: unless-stopped
joplin:
image: git.stdv.de/saturneric/joplin:latest
...
depends_on:
- db
networks:
- iso
- vpn
restart: unless-stopped
networks:
iso:
internal: true
vpn:
external: true
可以看到,db 只在 iso 网络中和 joplin 一对一通信,这个网络阻止该在网络内直接访问 外部网络(internal: true)。这样其他服务的被入侵后,一般无法通过网络的方式来访问 到我的 joplin 的数据库。但这并不是“绝对安全”,前提始终是攻击者没有拿到宿主机权 限。但在现实世界里,减少横向移动的可能性,本身就是非常重要的一层防护。
这样做其实也可以防止 hostname / database 名冲突,就是很多在同一个网络下的 database 公用同一个 hostname,那么很可能会导致有的时候某些服务访问错了数据库。
防火墙策略
如果你使用的是 VPS,我的建议是:优先使用 Cloud Provider 提供的防火墙,而尽量减少 宿主机防火墙规则。然后对于外部防火墙,尽可能地严格限制入方向的放行的端口,服务都 通过域名和反代来访问。原因包括:双重防火墙极易混乱,你很容易忘记配置了哪一个,哪 一个没有配置;然后,我也发现 Docker 端口映射有时会绕过本地防火墙,这个可能引起意 外的内部服务端口暴露,很多时候自己并没有发现。然后,云防火墙能提前挡掉大规模扫描 流量,这个也是将这个计算消耗转移到 Cloud Provider 身上。
一般来说云防火墙入方向只放行下面的这些端口:
- 80 / 443
- VPN 端口
- SSH
- 其他你认为真的有必要开的端口
其他端口即便在宿主机监听,只要云防火墙不放行,对外就是不可见的。你只能够通过 VPN 来访问到这些端口,这个就省心多了。
SSH 安全
这个领域有很多其他的优秀的文章,我就一些很基础但非常重要的点,这些也是我现在一直 在做的:
- 禁止 root 用户 SSH 登录。如果需要使用到 root 用户,则使用普通用户 + sudo
- 普通用户的 SSH 用户名不要太容易猜测,比如 admin 什么的。
- 普通用户设置长随机口令(建议密码管理器生成),而平时默认用密钥登录。
- 可选:只允许普通用户使用密钥登录,禁用密码。(要小心密钥没有备份然后设备丢失的 情况)
很多人会建议更换 SSH 的端口号,有关于个人环境下是否更换 SSH 端口,下面是个人的观 点:
- 个人环境下安全收益有限
- 后续某些配置复杂度会上升
- 最终还是视个人习惯决定
后记
我现在其实上了 Dedicated Server,而不再是 VPS,也就是类似在机房租了一台看得见的 物理服务器。因为我现在其实需要托管几十个容器(捋了一遍都是我要的)。这可能也是这 个 Self-Host 确实已经成为了自己每天离不开的技术基础设施的一种体现,也是说明,我 认为我每个月的投入更多的资金确实有其价值。
然后,需要指出的是,在某些地方,文章中提到的,通过私有 VPN 来一体化支持自己的内 部服务流量和普通上网的流量,很难做到。比如在这些地方,服务器的上行带宽非常贵或者 用的是流量计费。这个时候,我的建议是在自己家里的软路由中,提前进行分流,将对于 Self-Host 的流量先分离出来,然后正常的上网流量就走家庭带宽出去。
还有就是,如果你能做到有足够的服务器的上行带宽支撑私有 VPN,很多时候你可能并不想 自己的 VPS 的 IP 来浏览网页,这无疑直接暴露了你的身份。更好的情况是,你将流量引 入另一个 VPN Provider 的 VPN 服务器,然后再通过它提供的出口 IP 出来。对于这个问 题,我有一个长期自用的成熟的解决方案,你可以参考我的我博客文章:搭建个人链式 VPN 网络:高效隐私保护、设备无限制管理与远程访 问。