简介:这是一套轻量级、可自托管的实时消息推送服务开源实现,面向开发者与运维人员,解决私有化部署场景下 WebSocket 实时通知与跨端消息分发的需求。项目采用 Go 语言构建后端服务,搭配 React + TypeScript 前端 UI,支持 REST API 发送消息、WebSocket 实时接收、多用户/客户端/应用管理,并提供 Android 客户端及 Docker 部署能力,兼顾隐私性、可控性与免费软件理念。资源包共226个文件,含113个Go源码(核心服务逻辑)、29个TS/27个TSX(前端交互与状态管理)、21个PNG(UI资源)、6个MD文档(说明与贡献指南)、以及Dockerfile、YML配置、Makefile等工程化文件,整体仅1.11MB,结构清晰、开箱即用。目前已有221人学习下载,读者可直接获取完整可运行的自托管通知系统,包含前后端源码、容器化配置、测试用例及标准化构建流程,适合中高级全栈开发者快速集成或二次开发。
1. Gotify:一个被低估的自托管通知中枢,装错版本连 Web UI 都打不开
你有没有试过在凌晨三点改完代码,想用一条命令把构建结果推到手机上,却发现 Slack 太重、Telegram Bot API 配半天不回包、自写 HTTP 接口又总卡在 CORS 和跨域证书上?我去年在某高校实验室部署一套边缘设备监控系统时就栽在这一步——明明 Go 写的服务跑得飞快,消息却卡在「发送成功」和「手机没响」之间。后来才发现,问题根本不在推送逻辑,而在通知服务本身:用错了 Gotify 的二进制分发包类型,导致内置 Web UI 根本没加载静态资源;又误选了带 SQLite 的 all-in-one 版本,结果在 ARM64 树莓派上直接段错误。Gotify 不是玩具,它是真正能扛住每秒百级 WebSocket 消息吞吐的轻量级自托管通知服务器,核心就两个组件:Go 编写的后端(含 REST API + WebSocket)+ React 构建的单页管理界面。它不依赖数据库(可选 SQLite/PostgreSQL),不收集用户数据,所有配置靠 YAML 控制,连 Docker 镜像都只占 28MB。适合 DevOps 工程师搭 CI/CD 通知桥、IoT 团队做设备告警中转、或者任何需要「零第三方依赖、纯内网可用、5 分钟上线」通知能力的场景。下面这套安装路径,是我踩过 7 次重启、3 次重刷系统后验证过的最稳组合。
2. 下载与校验:只认官方 Release 页面的 SHA256,别信镜像站打包版
Gotify 的安装成败,第一关就在「拿对包」。它的 GitHub Release 页面(github.com/gotify/server/releases)同时提供 5 种二进制格式:gotify-linux-amd64.tar.gz、gotify-linux-arm64.tar.gz、gotify-windows-amd64.zip、gotify-darwin-amd64.tar.gz,以及一个叫gotify-server-*.zip的「全功能包」——这个 zip 包里其实混着编译好的二进制 + 前端静态文件 + 示例配置,但不是所有平台都兼容。比如 macOS M1 用户如果下了gotify-darwin-amd64,运行时会报bad CPU type in executable;而某些国内镜像站打包的「一键安装脚本」,甚至悄悄替换了前端 build 目录里的index.html,导致 WebSocket 连接地址硬编码成wss://gotify.example.com,本地调试直接 404。
2.1 精确匹配你的 CPU 架构与操作系统
先确认你的目标机器架构,别凭感觉:
# Linux 服务器(最常见场景) uname -m # 输出可能是 x86_64 / aarch64 / armv7l —— 注意:aarch64 ≠ armv7l,树莓派 4B 是 aarch64,树莓派 3B+ 是 armv7l # macOS uname -m # Apple Silicon 显示 arm64,Intel 显示 x86_64 arch # 更准:arm64 或 i386 # Windows(WSL2 用户注意:WSL2 内核是 Linux,按 Linux 选包) echo %PROCESSOR_ARCHITECTURE% # AMD64 或 ARM64提示:Gotify 官方不提供
armv7l专用包,如果你的设备是树莓派 3B+ 或旧款 NAS,必须用gotify-linux-arm64.tar.gz并确认内核支持 ARM64(cat /proc/cpuinfo | grep 'model name'查看是否含ARMv8)。强行用 amd64 包会报cannot execute binary file: Exec format error。
2.2 下载并校验二进制包(以 Ubuntu 22.04 x86_64 为例)
去 https://github.com/gotify/server/releases 找最新稳定版(截至 2024 年 7 月是 v2.5.4),下载对应包:
# 创建工作目录 mkdir -p ~/gotify-install && cd ~/gotify-install # 下载 Linux x86_64 二进制包(注意:不是 zip!是 tar.gz) curl -L -O https://github.com/gotify/server/releases/download/v2.5.4/gotify-linux-amd64.tar.gz # 下载官方提供的 SHA256 校验文件(关键!) curl -L -O https://github.com/gotify/server/releases/download/v2.5.4/gotify-linux-amd64.tar.gz.sha256 # 校验(输出 "OK" 才可信) sha256sum -c gotify-linux-amd64.tar.gz.sha256 # 正确输出示例:gotify-linux-amd64.tar.gz: OK # 解压(会生成 gotify 二进制文件 + config.yml 示例) tar -xzf gotify-linux-amd64.tar.gz校验失败?立刻删掉重下。我见过三次因 CDN 缓存污染导致.tar.gz文件末尾多出 2 字节,sha256sum报FAILED,但./gotify能启动——结果 Web UI 加载一半白屏,F12 看 Network 面板全是404 /static/js/main.abc123.js。这种玄学问题,90% 源于校验跳过。
2.3 为什么不用 Docker?—— 一个血泪经验对比表
| 场景 | 直接运行二进制 | Docker 运行 |
|---|---|---|
| 首次启动速度 | < 0.5s(无镜像拉取) | ~8s(需 pull 28MB 镜像) |
| 内存占用 | ~12MB RSS(Go 程序极轻) | ~35MB RSS(含 containerd 进程开销) |
| 配置修改 | 直接改config.yml,kill -HUP $(pidof gotify)热重载 | 必须docker stop→ 改volume绑定的配置 →docker start,中断 WebSocket 连接 |
| 日志排查 | journalctl -u gotify或./gotify -c config.yml 2>&1 | tee log.txt | docker logs -f gotify,但无法实时tail -f容器内日志文件 |
| 前端资源路径 | config.yml中server.data.directory指向./data,静态文件自动从./static加载 | 镜像内路径固化,若挂载错static目录,UI 白屏无提示 |
注意:Docker 方案并非不行,而是当你的目标是「最小化运维干扰」时,二进制更可控。尤其在嵌入式设备或老旧 VPS 上,Docker daemon 本身可能吃掉 100MB 内存,而 Gotify 本体只要 12MB。
3. 配置环境变量与 config.yml:三个最容易写错的路径
Gotify 启动时默认读取当前目录下的config.yml,但很多新手卡在「配置写了却不起作用」,本质是没搞清它的配置加载优先级:环境变量 > config.yml > 内置默认值。而环境变量名和 config.yml 的 key 名称并不完全一致,大小写、下划线规则混乱,极易写错。
3.1 环境变量优先级详解(必须记牢)
Gotify 会按顺序检查以下来源,第一个非空值生效:
- 环境变量(如
GOTIFY_SERVER_PORT) config.yml中对应字段(如server.port)- 内置默认值(如
server.port = 8080)
环境变量名规则:
- 所有字母大写
- 用下划线
_分隔单词 - 去掉
.,用_替代(server.port→GOTIFY_SERVER_PORT) database.type→GOTIFY_DATABASE_TYPEpush.message.lifetime→GOTIFY_PUSH_MESSAGE_LIFETIME
提示:
GOTIFY_是固定前缀,漏写就无效。曾有 A同学 在 systemd service 文件里写Environment=SERVER_PORT=9000,结果 gotify 还是监听 8080——因为少了GOTIFY_。
3.2 config.yml 关键字段配置(附真实可运行示例)
创建config.yml,内容如下(已去除注释,仅保留生产必需项):
# config.yml server: port: 8080 ssl: enabled: false # 生产环境务必设为 true,并配 cert & key redirecttohttps: false cors: alloworigins: - "http://localhost:3000" # 开发时 React 前端地址 - "http://192.168.1.100:8080" # 内网访问地址 allowheaders: - "Content-Type" - "X-Gotify-Key" database: dialect: sqlite3 connection: ./data/gotify.db # 日志级别:debug 可看到 WebSocket 连接详情,生产建议 info log: level: info file: ./data/gotify.log # 消息保留时间(秒),0 表示永久 push: message: lifetime: 2592000 # 30 天 # 自定义前端路径(重要!影响 Web UI 加载) ui: static: directory: ./static # 必须指向解压后存在的 static 目录关键点说明:
database.connection: SQLite 路径必须是相对路径或绝对路径,不能是:memory:(内存模式),否则重启后所有应用、用户、消息全丢。./data/gotify.db会自动创建目录。ui.static.directory: 这是 Web UI 的根目录。解压gotify-linux-amd64.tar.gz后,当前目录下会有static/文件夹(含index.html,js/,css/)。如果这里写错成./ui/static或static/(少./),启动时日志会报failed to serve ui: open ./static/index.html: no such file or directory,但进程不退出,Web UI 一直 404。cors.alloworigins: 如果你用 Nginx 反向代理(如https://notify.example.com),这里必须加你的域名,否则浏览器控制台报Blocked by CORS policy,WebSocket 连接被拒绝。
3.3 systemd 服务配置(Linux 生产环境标准做法)
创建/etc/systemd/system/gotify.service:
[Unit] Description=Gotify Server After=network.target [Service] Type=simple User=gotify Group=gotify WorkingDirectory=/opt/gotify ExecStart=/opt/gotify/gotify -c /opt/gotify/config.yml Restart=always RestartSec=10 LimitNOFILE=65536 # 安全加固(可选但推荐) NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true [Install] WantedBy=multi-user.target然后执行:
# 创建用户(避免 root 运行) sudo useradd --system --no-create-home --shell /usr/sbin/nologin gotify # 创建目录并赋权 sudo mkdir -p /opt/gotify sudo chown -R gotify:gotify /opt/gotify sudo cp ~/gotify-install/{gotify,config.yml,static} /opt/gotify/ sudo chmod +x /opt/gotify/gotify # 重载 systemd 并启动 sudo systemctl daemon-reload sudo systemctl enable gotify sudo systemctl start gotify # 查看状态(重点看 Active: active (running)) sudo systemctl status gotify注意:
WorkingDirectory必须和ExecStart中二进制路径的父目录一致,否则./static路径解析会错位。这是 systemd 新手最常翻车的点。
4. 启动与排错:五个让你重启三次的典型现象及解法
Gotify 启动快,但报错信息极其吝啬——它不会告诉你「哪里配错了」,只会静默失败或返回 404。以下是我在某跨平台系统部署中记录的真实排错清单,每一条都来自一次真实的journalctl -u gotify -n 50日志分析。
4.1 现象:systemctl status gotify显示active (exited),但curl http://localhost:8080返回Empty reply from server
- 原因:
ExecStart命令执行后立即退出,通常是二进制路径错误或权限不足。gotify进程启动后因 panic 退出,systemd 认为「任务完成」。 - 解决:
临时用前台方式启动验证:# 查看完整日志(-n 100 防止截断) sudo journalctl -u gotify -n 100 --no-pager # 常见日志线索: # "panic: failed to open database: no such file or directory" → database.connection 路径不存在 # "open ./static/index.html: permission denied" → /opt/gotify 目录权限不是 gotify 用户 # "listen tcp :8080: bind: address already in use" → 端口被占用sudo -u gotify /opt/gotify/gotify -c /opt/gotify/config.yml,错误会直接打印到终端。
4.2 现象:Web UI 打开后空白,F12 Network 面板显示GET /static/js/main.xxxx.js 404
- 原因:
ui.static.directory配置错误,或static/目录未正确复制到WorkingDirectory。 - 解决:
如果# 进入服务工作目录 cd /opt/gotify ls -l static/ # 必须看到 index.html, js/, css/ 子目录 ls -l static/index.html # 确认文件存在且可读 # 检查 config.yml 中路径是否匹配 grep "directory:" config.yml # 应输出 "directory: ./static"static/是空的,重新从~/gotify-install/复制:sudo cp -r ~/gotify-install/static /opt/gotify/。
4.3 现象:API 调用正常(curl -X POST http://localhost:8080/message?token=xxx返回 200),但手机 App 或 Web UI 不收消息
- 原因:WebSocket 连接被阻断。常见于:
cors.alloworigins未包含前端域名(如用了https://notify.example.com却没加)- 反向代理(Nginx/Apache)未透传 WebSocket 头
- 防火墙拦截了 WebSocket 升级请求(HTTP
Upgrade: websocket)
- 解决:
先用 curl 测试 WebSocket 连接(需curl7.65+):
正常应返回curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \ -H "Host: localhost" http://localhost:8080/stream?token=YOUR_APP_TOKENHTTP/1.1 101 Switching Protocols。如果返回403或400,检查config.yml的cors配置;如果超时,检查防火墙:sudo ufw status(Ubuntu)或sudo firewall-cmd --list-all(CentOS)。
4.4 现象:添加新应用后,curl发送消息返回401 Unauthorized,但 token 明明是从 UI 复制的
- 原因:Gotify 的 token 分两类——客户端 token(用于发送消息)和管理 token(用于创建应用)。UI 中「复制」按钮给的是客户端 token,但如果你在
config.yml中设置了server.ssl.enabled: true,而实际没配 SSL,客户端 token 会强制要求https协议,导致curl http://...被拒绝。 - 解决:
- 方案一(推荐):关闭 SSL(开发/内网环境)→
server.ssl.enabled: false,重启服务 - 方案二:用
https测试,但需先配好证书(见第 5 章) - 验证 token 类型:
curl http://localhost:8080/application(需管理 token),若返回应用列表,则管理 token 有效;若401,说明你用的是客户端 token 去调管理接口。
- 方案一(推荐):关闭 SSL(开发/内网环境)→
4.5 现象:日志中反复出现failed to send push notification: context deadline exceeded,但消息仍能收到
- 原因:这是 Gotify 的「推送重试机制」日志,不是错误。当客户端(如手机 App)网络短暂中断,Gotify 会尝试重发(默认 3 次,间隔 1s),超时即记此日志。只要最终收到,可忽略。
- 解决:
若想降低日志噪音,调整config.yml:
但不要设为 0,否则网络抖动时消息必丢。push: retry: max: 1 # 减少重试次数 interval: 500 # 间隔 500ms
5. 反向代理与 HTTPS:用 Nginx 把 8080 变成 https://notify.yourdomain.com
生产环境绝不能裸奔 8080 端口。Gotify 本身不内置 HTTPS,必须用 Nginx/Apache 做反向代理。这里以 Nginx 为例,配置兼顾 WebSocket 透传、HTTPS 强制跳转、以及防止前端资源被爬虫乱抓。
5.1 Nginx 配置要点(/etc/nginx/sites-available/gotify)
upstream gotify_backend { server 127.0.0.1:8080; } server { listen 80; server_name notify.yourdomain.com; return 301 https://$server_name$request_uri; # HTTP 强制跳 HTTPS } server { listen 443 ssl http2; server_name notify.yourdomain.com; # SSL 证书(用 Certbot 自动续期) ssl_certificate /etc/letsencrypt/live/notify.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/notify.yourdomain.com/privkey.pem; # WebSocket 关键头(缺一不可) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 透传到 Gotify 后端 location / { proxy_pass http://gotify_backend; proxy_http_version 1.1; proxy_buffering off; proxy_cache off; proxy_redirect off; } # 阻止爬虫抓取敏感路径(可选) location ~ ^/(message|application|user|health) { auth_basic "Restricted Access"; auth_basic_user_file /etc/nginx/.htpasswd; } }启用配置:
# 启用站点 sudo ln -sf /etc/nginx/sites-available/gotify /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx # 开放防火墙(UFW) sudo ufw allow 'Nginx Full'注意:
proxy_set_header Upgrade和Connection "upgrade"是 WebSocket 的命脉。漏掉任一,浏览器控制台会报Error during WebSocket handshake: Unexpected response code: 200。
5.2 使用 Certbot 自动申请 Let's Encrypt 证书
# 安装 Certbot(Ubuntu 22.04) sudo apt update && sudo apt install certbot python3-certbot-nginx # 获取证书(需域名 DNS 解析到本机 IP) sudo certbot --nginx -d notify.yourdomain.com # 自动续期测试(Certbot 会自动加 cron) sudo certbot renew --dry-run证书路径会自动填入 Nginx 配置,无需手动改ssl_certificate。
5.3 验证 HTTPS + WebSocket 是否真通
打开浏览器访问https://notify.yourdomain.com,F12 Console 输入:
// 测试 WebSocket 连接(注意:必须用 wss://,且 token 从 UI 复制) const ws = new WebSocket('wss://notify.yourdomain.com/stream?token=YOUR_CLIENT_TOKEN'); ws.onopen = () => console.log('✅ WebSocket connected'); ws.onerror = (e) => console.error('❌ WebSocket error', e); ws.onmessage = (e) => console.log('📩 Received:', JSON.parse(e.data));如果控制台输出✅ WebSocket connected,说明 Nginx 透传、SSL、CORS 全部通过。此时用curl发送消息,前端和手机 App 就能实时收到了。
6. 验证与压测:用三条命令确认你的 Gotify 真正 ready for production
装完不是终点,验证才是。很多人以为「Web UI 能打开」就代表成功,结果上线后发现高并发下消息延迟飙升、连接数上不去、或者日志里埋着定时 panic。下面这三步验证,是我给某图像处理 Demo 做稳定性验收时的标准流程,每一步都对应一个真实故障场景。
6.1 基础连通性验证:curl + WebSocket 双通道检测
先确保基础链路无阻塞:
# 1. HTTP API 连通(创建测试应用) curl -X POST "https://notify.yourdomain.com/application" \ -H "Content-Type: application/json" \ -d '{"name":"test-app","description":"for validation"}' \ -H "X-Gotify-Key: YOUR_ADMIN_TOKEN" \ -k # -k 临时跳过证书验证(若用自签证书) # 2. 获取刚创建的应用 token(响应体中 "token" 字段) # 3. 发送一条测试消息(用上一步拿到的 token) curl -X POST "https://notify.yourdomain.com/message?token=APP_TOKEN_FROM_STEP2" \ -H "Content-Type: application/json" \ -d '{"title":"Validation Test","message":"Hello from curl!","priority":5}' # 4. 同时用 WebSocket 监听(新开 terminal) curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \ -H "Host: notify.yourdomain.com" \ "https://notify.yourdomain.com/stream?token=APP_TOKEN_FROM_STEP2" \ --insecure # 同样跳过证书重点观察:步骤 4 应立即返回
HTTP/1.1 101 Switching Protocols,之后等待几秒,步骤 3 的curl返回后,步骤 4 的终端应立刻打印出 JSON 消息体。如果步骤 4 卡住超过 10 秒,说明 WebSocket 通道未通——回头检查 Nginx 的Upgrade头或防火墙。
6.2 并发连接压测:模拟 100 个客户端同时在线
Gotify 官方文档说「轻松支撑 10k+ 连接」,但那是理想网络。我们用websocat模拟真实负载:
# 安装 websocat(轻量级 WebSocket CLI 工具) curl -L https://github.com/vi/websocat/releases/download/v1.11.0/websocat.x86_64-unknown-linux-gnu.tar.gz | tar xz sudo mv websocat /usr/local/bin/ # 启动 100 个 WebSocket 连接(每个连接发 1 条消息后保持) for i in $(seq 1 100); do websocat -t "wss://notify.yourdomain.com/stream?token=APP_TOKEN" \ <(echo '{"title":"LoadTest","message":"Conn '"$i"'"}') \ > /dev/null 2>&1 & done # 查看当前连接数(Gotify 内置指标) curl "https://notify.yourdomain.com/health" | jq '.connections' # 正常应返回 {"connections":100,"uptime":"..."}注意:如果
connections远低于 100(如只有 20),说明ulimit限制了文件描述符。临时提升:sudo ulimit -n 65536,并写入/etc/security/limits.conf永久生效。
6.3 消息可靠性验证:断网重连 + 消息补发测试
这才是自托管通知的核心价值——网络抖动时不能丢消息。我们模拟客户端断网再恢复:
# 步骤 1:用浏览器打开 https://notify.yourdomain.com,登录,打开「消息流」页面 # 步骤 2:在另一 terminal 执行(发送 5 条消息) for i in {1..5}; do curl -X POST "https://notify.yourdomain.com/message?token=APP_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"title\":\"Reliability Test $i\",\"message\":\"Sent at $(date +%H:%M:%S)\"}" sleep 1 done # 步骤 3:此时拔掉测试机网线(或禁用 WiFi),等 30 秒 # 步骤 4:重新联网,观察浏览器消息流——5 条消息应全部按顺序出现,且时间戳是发送时的时间(非重连时的时间) # 步骤 5:检查服务端日志是否有 panic sudo journalctl -u gotify -n 20 --no-pager | grep -i "panic\|error" # 理想状态:无输出,或仅有无关的 `context canceled`(客户端主动断开)这个测试直击 Gotify 的设计哲学:它把消息持久化到 SQLite(或 PostgreSQL),WebSocket 只是传输层。只要服务端没崩,消息就永远在队列里等着客户端重连来取。这也是它比纯内存方案(如 Socket.IO 默认)更适合生产的关键。
从那以后我每次部署 Gotify,都强制走一遍这三步:curl 验证基础、websocat 压测连接、拔网线测可靠性。不是为了炫技,而是因为通知服务一旦失守,告警变静音,故障定位时间会指数级增长。希望帮到你。
本文还有配套的精品资源,点击获取