Self-hosted 服务搭建思路:awesome-selfhosted、Docker 与 Nginx Proxy Manager
编写时间:2024-04-28
硬件、系统和网络入口都接上以后,我才开始真正往这台机器里放长期服务。这里的重点不是“我部署过哪些项目”,而是整理出一套可重复的 self-hosted 接入流程:先从需求出发找项目,再用 Docker 管运行,用 Nginx Proxy Manager 或手写 Nginx 管入口。
挑项目时可以参考 awesome-selfhosted。这个列表很适合作为索引:想做文件管理、媒体库、监控、笔记、自动化时,先看有哪些成熟项目,再回到自己的机器上判断它是否真的值得部署。
接入流程
我后面部署服务基本都按同一套流程走:
这套流程的关键是边界清晰:
| 层 | 负责什么 | 不负责什么 |
|---|---|---|
| Docker Compose | 服务启动、依赖、数据卷、重启策略 | 公网入口和证书 |
| Nginx Proxy Manager / Nginx | 域名反代、SSL、WebSocket、基础访问入口 | 业务数据 |
| Cloudflare Zero Trust | 管理后台访问控制 | 普通公开内容 |
| 数据目录 | 保存不可丢的数据 | 保存临时容器状态 |
服务本身可以换,但接入方式最好稳定。这样后面新增项目时,不需要重新思考整套网络和证书。
从需求到项目
原始待办里有很多想做的事情:博客、备案首页、文件入口、媒体自动化、远程管理、个人小工具。整理成博客时,不需要把每个项目都展开,只挑几个能说明方法的例子:
| 需求 | 代表例子 | 重点问题 |
|---|---|---|
| 博客和个人主页 | Mix Space + Shiro | 前后端拆分、API 域名、CORS、双前端 |
| 媒体自动化 | AutoBangumi + qBittorrent + Jellyfin | 下载、整理、播放的目录边界 |
| 文件入口 | AList | 本地目录挂载、上传限制、公开范围 |
| 管理入口 | Portainer / Cockpit | 只能内网或受保护访问 |
这几个例子覆盖了大部分 self-hosted 服务会遇到的问题:有状态服务、无状态前端、容器网络、反向代理、文件挂载、管理入口保护。
Docker 基本模板
先创建一个给反向代理使用的共享网络:
docker network create nginx每个需要通过域名访问的服务,都加入这个外部网络:
networks:
nginx:
external: true一个新的 Web 服务,通常可以从这个模板开始:
services:
app:
image: example/app:latest
volumes:
- ./config:/config
- /mnt/vault/app-data:/data
environment:
- TZ=Asia/Shanghai
networks:
- nginx
restart: unless-stopped
networks:
nginx:
external: true这里我会先问三个问题:
- 哪些目录是配置,哪些目录是真正的数据;
- 这个服务是否需要被公网访问;
- 如果容器删掉重建,哪些东西必须还在。
只要这三个问题想清楚,Compose 文件通常不会太乱。
NPM 入口模板
当时我选 Nginx Proxy Manager,主要是因为对 Nginx 还不够熟,面板能把域名、证书和反代先跑起来。这个选择对常规 self-hosted 服务很友好,但后来会感觉不够自由:复杂路径、缓存、header、特殊 location 还是手写 Nginx 更清楚。
如果用 NPM,反代规则尽量保持简单:
Domain Names = app.example.net
Scheme = http
Forward Host = app
Forward Port = 8080
WebSocket = enabledNPM 在容器里时,localhost 指向的是 NPM 容器自己。后端服务最好加入同一个 Docker 网络,然后在 NPM 里填容器名,例如 app、shiro、mx-server。
如果要申请泛域名证书,可以用 DNS Challenge。真实凭据只放在 NPM 的凭据配置里,不进入文章、仓库或 Compose。复杂服务可以直接用 Nginx 配置接管,NPM 更适合作为起步和常规服务面板。
例子一:博客系统
博客是最典型的有状态 self-hosted 服务。后端、数据库、缓存和前端职责不同:
后端保存文章、配置和对象信息,前端只是渲染页面。这个拆法带来一个好处:后端只跑一份,前端可以有多份。例如家庭服务器跑完整后端和一个前端,固定 IP 机器只跑轻量前端,用于备案域名入口。
需要特别注意的是:
| 项 | 处理方式 |
|---|---|
| API 域名 | 单独给后端一个域名,例如 api.example.net |
| 前端域名 | 可以有多个,例如 blog.example.net、example.cn |
| CORS | 后端允许所有前端域名 |
| 后台路径 | 和反代配置保持一致 |
| 数据层 | MongoDB / Redis 不暴露公网 |
Compose 里后端加入业务网络和 nginx 网络,数据库只留在业务网络:
services:
mx-server:
image: innei/mx-server:latest
environment:
- TZ=Asia/Shanghai
- NODE_ENV=production
- DB_HOST=mongo
- REDIS_HOST=redis
- ALLOWED_ORIGINS
- JWT_SECRET
volumes:
- ./data/mx-space:/root/.mx-space
networks:
- mx-space
- nginx
restart: unless-stopped
mongo:
image: mongo
volumes:
- ./data/db:/data/db
networks:
- mx-space
restart: unless-stopped
redis:
image: redis:alpine
volumes:
- ./data/redis:/data
networks:
- mx-space
restart: unless-stopped
networks:
mx-space:
nginx:
external: true例子二:媒体自动化
媒体服务不是一个容器能解决的,它更像一条流水线:
这里最重要的是目录边界。下载器负责写入下载目录,Jellyfin 负责读取媒体目录,不要让播放器随便修改源文件。
services:
jellyfin:
image: jellyfin/jellyfin
network_mode: host
volumes:
- ./config:/config
- ./cache:/cache
- /mnt/vault/media:/media:ro
devices:
- /dev/dri:/dev/dri
restart: unless-stopped/dev/dri 用于硬件转码;媒体目录用只读挂载,避免误删。AutoBangumi 和 qBittorrent 之间要能互相访问,如果不在同一个 Docker 网络里,就需要确认容器内网地址或把它们接入同一网络。
例子三:文件入口
文件入口更接近 NAS 需求,但它不等同于备份系统。AList 这类服务适合聚合目录和轻量分享:
services:
alist:
image: xhofe/alist:latest-ffmpeg
volumes:
- ./data:/opt/alist/data
- /mnt/vault/alist:/localCloud
ports:
- "5244:5244"
networks:
- nginx
restart: unless-stopped这里要先想清楚公开范围:哪些目录只是自己内网用,哪些可以分享给外部。大文件上传如果走 Cloudflare 入口,可能遇到上传大小限制;文件管理和 NAS 场景更适合内网、直连 DDNS,或者在自己搭好的虚拟局域网里访问。
管理入口
管理类服务不要当普通 Web 服务裸奔公网。Portainer、Cockpit、NPM 管理面板这类入口,我会优先放在 Cloudflare Application / Zero Trust 后面。
管理服务的反代不是为了公开,而是为了统一入口和认证。
小结
self-hosted 的关键不是部署越多越好,而是把每个服务都纳入同一套流程:需求明确、Docker 编排、数据目录固定、NPM 或 Nginx 反代、管理入口受保护、配置和数据可备份。
这样以后再从 awesome-selfhosted 里挑新项目时,决策成本会低很多:只要它能稳定用 Docker 跑起来,能接入 NPM 或 Nginx,数据目录和暴露边界清楚,就可以作为候选;否则先放一放。