一台 2C2G 云主机跑 4 个服务:Caddy 自签反代 + systemd user 托管
为什么不用 80/443,也不装 Nginx 面板
我有一台最低配的云主机:2 核 2G、Ubuntu 24.04、没有备案域名。上面要同时跑四个自建服务:
| 端口 | 服务 | 说明 |
|---|---|---|
| 8443 | 管理面板(Go 单文件) | 需要 HTTPS,否则 Cookie 带 Secure 标记不生效 |
| 8444 | MQTT broker(EMQX) | 控制台要 HTTPS,1883 端口只给设备用 |
| 8445 | 记账服务(Python) | 手机浏览器访问,必须有 HTTPS 才能加到主屏 |
| 8446 | 自建 Agent(Web) | 内部使用,但要求 WebSocket 走 wss |
不用 80/443 是因为没备案域名,也不想去申请证书;不装 Nginx 面板是因为 2G 内存经不起额外的 PHP/Nginx/MySQL 三件套。最后的选择是 Caddy + 自签证书 + 四个高端口,配置文件不到 40 行。
一、Caddy 做自签 HTTPS 反代
Caddy 装完就自带一个内部 CA,给 IP 或任意域名签发证书,不需要 ACME、不需要域名:
{
# 用 Caddy 内置 CA 给本机地址签证书
local_certs
auto_https disable_redirects
}
# 面板
https://:8443 {
reverse_proxy 127.0.0.1:8317
encode gzip
log {
output file /var/log/caddy/panel.log
roll_size 10mb
roll_keep 3
}
}
# MQTT 控制台(含 WebSocket)
https://:8444 {
reverse_proxy 127.0.0.1:18083
}
# 记账服务
https://:8445 {
reverse_proxy 127.0.0.1:5000
}
# Agent Web(SSE/WS 都要,关掉缓冲)
https://:8446 {
reverse_proxy 127.0.0.1:9119 {
flush_interval -1
}
}
几个容易踩的点:
local_certs必须写在全局块里,写在站点块里不生效;- 端口用
https://:8443这种写法,Caddy 才会在这个端口上做 TLS 监听;写成http://:8443就变成明文了; flush_interval -1是给 SSE / 长连接用的,不加的话流式响应会被缓冲到结束才吐出来;- 日志按大小轮转写在 Caddyfile 里,别指望系统 logrotate 帮你管容器外的文件。
浏览器第一次访问一定报「不安全」,因为自签证书不在信任链里。个人用可以选择把 Caddy 的根证书(/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt)导进系统或手机,就不再有告警:
# 在服务器上找到根证书
sudo find /var/lib/caddy -name 'root.crt'
# 拉到本地后,Linux 导入
sudo cp root.crt /usr/local/share/ca-certificates/caddy-local.crt
sudo update-ca-certificates
二、用 systemd --user 托管,而不是 systemd 系统级
四个服务都不需要 root,用 systemd --user 更干净:每个 unit 文件放在 ~/.config/systemd/user/,服务以当前用户身份跑,权限最小。
# ~/.config/systemd/user/bee.crt.service
[Unit]
Description=BeeCount
After=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/apps/bee
ExecStart=%h/apps/bee/venv/bin/python app.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
Environment=PORT=5000
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now bee.service
systemctl --user status bee.service
关键的一步是 enable-linger:
sudo loginctl enable-linger $USER
不开这个,用户退出 SSH 之后 user 级 systemd 会被回收,所有服务在注销瞬间全挂——表现是「我明明 start 了,一关终端就 502」。开了之后 user manager 常驻,还能跟着系统启动。
两个常见的坑:
- PATH 不一样。user unit 里没有登录 shell 的 PATH,
ExecStart里调node、uv这类命令会报No such file or directory。要么写绝对路径,要么在 unit 里显式Environment=PATH=...。 - XDG_RUNTIME_DIR 缺失。root 直接
systemctl --user会报Failed to connect to bus,得su - <user>或者在 crontab/脚本里手动设XDG_RUNTIME_DIR=/run/user/$(id -u)。
三、两处网络都要放通,少一处就是打不开
云主机的「打不开」九成卡在两层防火墙上:
# 第一层:云控制台的安全组 —— 入方向放通 8443-8446/TCP
# 第二层:系统防火墙
sudo ufw allow 8443,8444,8445,8446/tcp
sudo ufw status verbose
# 本机监听是否真的在
sudo ss -lntp | grep -E '8443|8444|8445|8446'
顺序建议:先 ss -lntp 确认进程在监听,再 ufw,最后才是安全组。反过来的话你会花很多时间去怀疑 Caddy 配置,而问题其实在外面。
四、2G 内存怎么活下来
四个服务 + Docker + systemd 常驻,2G 内存是硬约束。几个实际有效的动作:
# 1) 加 2G swap(云主机默认常常没开)
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# 2) 把 swappiness 调高一点,宁可换页也别 OOM Kill
echo 'vm.swappiness=60' | sudo tee /etc/sysctl.d/99-swap.conf
sudo sysctl --system
# 3) journald 限容,不然 /var/log 会吃掉几个 G
sudo sed -i 's/^#\?SystemMaxUse=.*/SystemMaxUse=200M/' /etc/systemd/journald.conf
sudo systemctl restart systemd-journald
再加两条习惯:
- 镜像瘦身:能用
-alpine、-slim就用,别装完整发行版镜像;docker image prune -f挂个每周定时; - 别在 2G 机器上跑 Elasticsearch、Kibana、Jenkins 这类常驻大户,一个能吃掉 1.5G。真要跑,先看
docker stats再决定。
内存紧张时的排查顺序:free -h → ps aux --sort=-%mem | head → journalctl -k | grep -i 'killed process'。如果看到 Out of memory: Killed process,说明是 OOM Killer 干的,不是服务自己崩的,加 swap 或者砍进程,别急着改代码。
五、最后的目录结构
~/apps/
panel/ # 8443
emqx/ # 8444(docker compose)
bee/ # 8445(venv)
agent/ # 8446
~/.config/systemd/user/
bee.service agent.service panel.service
/etc/caddy/Caddyfile
四个服务、一个 Caddyfile、三个 user unit,重启机器后全部自动回来(前提是打开了 linger)。整套方案的资源占用常驻在 600MB 左右,剩下的留给 Docker 和临时构建——2C2G 的小机器上,这已经是能兼顾「多服务」和「可维护」的平衡点。
如果哪天有了备案域名,改 Caddyfile 两行就能让 Caddy 自动申请证书,反代结构完全不用动。
- 点赞
- 收藏
- 关注作者
评论(0)