Self-Hosting ntfy: Push Notifications Under 50MB RAM
Table of Contents
A complete guide to setting up ntfy on a 1GB Debian Lightsail instance with Nginx, Let's Encrypt, WebSocket support, and Jenkins CI integration. I wanted push notifications from my homelab — Jenkins failures, K8s alerts, custom scripts — without depending on Slack, Telegram bots, or any third-party service. ntfy is a single Go binary that idles at 12–18 MB RAM. Perfect for a 1GB Lightsail instance already running PocketBase, Woodpecker CI, and Nginx. This is the exact setup I used to get ntfy (pronounced notify) is a pub-sub notification server. You POST a message to a topic URL, and any subscribed client (Android app, browser, curl) receives it instantly. No accounts, no SDKs — just HTTP. Download the latest binary directly from GitHub: Verify: Note: use Create the cache directory and write a RAM-optimised config: The key RAM-saving decisions: The trick here is to write an HTTP-only config first, let Certbot issue the certificate, then Certbot auto-injects the SSL block. If you pre-write the Verify it's live: The ntfy Android app will prompt you to switch to WebSockets for better battery life. This requires the ntfy uses a simple user database. Add an admin user (admin gets read/write on all topics automatically): From the server, your laptop, your K8s cluster — anywhere with internet: On your phone: install the ntfy Android app → tap + → topic Priority levels: You can also open Drop this on any server you want to send alerts from: Add this to any Jenkinsfile to get build failure alerts on your phone: Store On my instance after a day of use: Well within the 50MB constraint with plenty of headroom for everything else running on the server. +++ntfy.mrsubodh.xyz running in under 30 minutes. What is ntfy?
Prerequisites
ntfy.yourdomain.xyz Install
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_amd64.tar.gz
tar zxvf ntfy_2.20.1_linux_amd64.tar.gz
sudo cp -a ntfy_2.20.1_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir -p /etc/ntfy
sudo cp ntfy_2.20.1_linux_amd64/{client,server}/*.yml /etc/ntfyntfy serve --helpntfy serve --help, not ntfy --version. The version is printed at the bottom of the help output. Configuration
sudo mkdir -p /var/cache/ntfy
sudo nano /etc/ntfy/server.ymlbase-url: "https://ntfy.yourdomain.xyz"
listen-http: "127.0.0.1:2586"
# SQLite file cache — avoids in-memory storage
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "12h"
# Auth
auth-file: "/var/cache/ntfy/auth.db"
auth-default-access: "deny-all"
# Disable attachments entirely to save RAM
attachment-cache-dir: ""
attachment-total-size-limit: "0"
attachment-file-size-limit: "0"
attachment-expiry-duration: "0"
behind-proxy: true
log-level: "warn"cache-file uses SQLite on disk instead of RAMattachment-cache-dir: "" disables file attachments entirelylog-level: warn skips verbose INFO logs systemd Service
sudo useradd -r -s /usr/sbin/nologin ntfy
sudo chown -R ntfy:ntfy /var/cache/ntfy
sudo nano /etc/systemd/system/ntfy.service[Unit]
Description=ntfy notification server
After=network.target
[Service]
Type=simple
User=ntfy
Group=ntfy
ExecStart=/usr/local/bin/ntfy serve --config /etc/ntfy/server.yml
Restart=on-failure
RestartSec=5
MemoryMax=50M
MemorySwapMax=0
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/cache/ntfy
[Install]
WantedBy=multi-user.targetMemoryMax=50M is a hard kernel-enforced ceiling. If ntfy ever leaks past 50MB, the kernel kills and restarts it automatically.sudo systemctl daemon-reload
sudo systemctl enable --now ntfy
sudo systemctl status ntfy Nginx + TLS
ssl_certificate lines before the cert exists, nginx -t will fail and block Certbot.sudo nano /etc/nginx/sites-available/ntfyserver {
listen 80;
server_name ntfy.yourdomain.xyz;
location / {
proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
# WebSocket upgrade — required for ntfy Android app
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}sudo ln -s /etc/nginx/sites-available/ntfy /etc/nginx/sites-enabled/ntfy
sudo nginx -t && sudo systemctl reload nginx
# Issue cert — Certbot will auto-add HTTPS block
sudo certbot --nginx -d ntfy.yourdomain.xyzcurl https://ntfy.yourdomain.xyz/v1/health
# {"healthy":true} WebSocket note
Upgrade + Connection "upgrade" headers shown above. Without them you'll get a WebSocketNotSupportedException in the app. With them, it just works. User Setup
sudo ntfy user add --role=admin yourname
# prompts for password Send Your First Notification
curl -u yourname:yourpassword \
-H "Title: Hello from ntfy" \
-H "Priority: high" \
-H "Tags: rocket" \
-d "ntfy is live!" \
https://ntfy.yourdomain.xyz/homelabhomelab → server https://ntfy.yourdomain.xyz → enter credentials. The notification arrives in 1–2 seconds.min · low · default · high · urgent Reading Past Messages
# Pretty-print all cached messages
curl -s -u yourname:yourpassword \
"https://ntfy.yourdomain.xyz/homelab/json?poll=1" | jq .
# Readable one-liner per message
curl -s -u yourname:yourpassword \
"https://ntfy.yourdomain.xyz/homelab/json?poll=1" \
| jq -r '"[\(.time | todate)] \(.title // "no title") — \(.message)"'
# Query SQLite directly
sudo sqlite3 /var/cache/ntfy/cache.db \
"SELECT time, topic, message, title FROM messages ORDER BY time DESC LIMIT 10;"https://ntfy.yourdomain.xyz in a browser for the built-in web UI. Shell Helper
cat << 'EOF' | sudo tee /usr/local/bin/notify
#!/bin/bash
# Usage: notify "message" "title (optional)" "priority (optional)"
curl -s -u yourname:yourpassword \
-H "Title: ${2:-Server Alert}" \
-H "Priority: ${3:-default}" \
-d "$1" \
https://ntfy.yourdomain.xyz/homelab
EOF
sudo chmod +x /usr/local/bin/notifynotify "Disk at 90% on /dev/sda1" "Storage Warning" high Jenkins Integration
pipeline {
// ... your stages ...
post {
failure {
sh """
curl -s -u yourname:${NTFY_PASSWORD} \
-H "Title: Jenkins Failed" \
-H "Priority: high" \
-H "Tags: x" \
-d "Build #${BUILD_NUMBER} failed — ${JOB_NAME} (${BRANCH_NAME})" \
https://ntfy.yourdomain.xyz/homelab
"""
}
success {
sh """
curl -s -u yourname:${NTFY_PASSWORD} \
-H "Title: Jenkins Passed" \
-H "Priority: default" \
-H "Tags: white_check_mark" \
-d "Build #${BUILD_NUMBER} passed — ${JOB_NAME}" \
https://ntfy.yourdomain.xyz/homelab
"""
}
}
}NTFY_PASSWORD as a Jenkins credential (Secret Text), inject it via withCredentials. RAM Usage
systemctl status ntfy | grep MemoryMemory: 14.2M (hard limit: 50M) Summary
Component Choice Reason Binary ntfy v2.20.1 Single Go binary, no runtime deps Cache SQLite on disk Avoids in-memory message storage Auth deny-all + user DBNo anonymous access Transport WebSocket via Nginx Better battery on mobile RAM cap MemoryMax=50MKernel-enforced hard limit