A lightweight Flask server that receives Pi-hole blocklists pushed from the phlist desktop app and serves them as plain-text URLs that Pi-hole can subscribe to via gravity.
- You build and push a combined blocklist from the phlist desktop app
- phlist-server stores it as a
.txtfile on disk - Pi-hole fetches the list via a plain HTTP URL
- Pi-hole runs gravity — done
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/ |
GET | None | Web dashboard; rate limited to 30 req/min |
/health |
GET | Bearer token | Authenticated connection test; rate limited to 10 req/min |
/api/stats |
GET | None | Dashboard system stats JSON (CPU, RAM, disk, uptime, temp); rate limited to 30 req/min |
/lists/ |
GET | None | JSON inventory of stored lists with slug, size, line count, and mtime; rate limited to 30 req/min |
/lists/{slug}.txt |
PUT | Bearer token | Receive, validate, and atomically store a blocklist pushed by phlist; max request body defaults to 2 GB (PHLIST_MAX_UPLOAD_MB=2048) and rate limit is 5 req/min |
/lists/{slug}.txt |
GET | None | Serve full list to Pi-hole, or the first 100 lines with ?preview=1 |
/lists/{slug}.txt |
DELETE | Delete password bearer token | Delete a stored list; rate limited to 10 req/min |
In the phlist desktop app, source fetch timeout, per-source max size, and push timeout are client-side settings. This server still enforces its own upload cap, fixed 100-line preview mode, 300-second Gunicorn worker timeout in the deployed systemd service, and 10-second optional Pi-hole gravity trigger timeout.
git clone https://github.com/appaKappaK/phlist-server.git
cd phlist-server
pip install -r requirements.txt
cp .env.example .env
# Edit .env — set PHLIST_API_KEY, PHLIST_DELETE_PWD, and PHLIST_HOST
python phlist_server.pysudo mkdir -p /opt/phlist-server
sudo cp phlist_server.py /opt/phlist-server/
sudo cp -r templates static /opt/phlist-server/
python3 -m venv /opt/phlist-server/venv
/opt/phlist-server/venv/bin/pip install flask flask-limiter python-dotenvsudo mkdir -p /etc/phlist-server
sudo cp .env.example /etc/phlist-server/.env
sudo nano /etc/phlist-server/.env
# Set PHLIST_API_KEY
# Set PHLIST_DELETE_PWD for dashboard deletes
# Keep PHLIST_HOST=127.0.0.1 for local-only access
# Set PHLIST_HOST=100.x.y.z to restrict to Tailscale peers only
# Set PHLIST_HOST=0.0.0.0 only if Pi-hole must reach this server over your LAN
sudo chmod 600 /etc/phlist-server/.envsudo useradd -r -s /bin/false phlist
sudo mkdir -p /var/lib/phlist/lists
sudo chown phlist:phlist /var/lib/phlist/listssudo cp systemd/phlist-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now phlist-server
sudo systemctl status phlist-serverFrom a fresh checkout on the server:
git pull
sudo scripts/update-deployed.shThe updater replaces only the deployed app files in /opt/phlist-server, refreshes the virtualenv dependencies, reinstalls the systemd unit, restarts phlist-server, and runs an authenticated /health check. It does not overwrite /etc/phlist-server/.env or /var/lib/phlist/lists.
| Variable | Default | Description |
|---|---|---|
PHLIST_API_KEY |
(required) | Bearer token for authentication. Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" |
PHLIST_DELETE_PWD |
falls back to PHLIST_API_KEY |
Password required by the web dashboard delete modal and DELETE endpoint |
PHLIST_LIST_DIR |
/var/lib/phlist/lists |
Directory where blocklist .txt files are stored |
PHLIST_MAX_UPLOAD_MB |
2048 |
Maximum accepted upload size for a pushed list, in megabytes |
PHLIST_HOST |
127.0.0.1 |
IP address to bind to. 127.0.0.1 is local-only. Use your Tailscale IP (100.x.y.z) to restrict access to Tailscale peers. Use 0.0.0.0 only when another LAN device, such as Pi-hole, must connect directly; it listens on every network interface. |
PHLIST_PORT |
8765 |
TCP port |
PHLIST_PIHOLE_URL |
(unset) | Optional: Pi-hole base URL for auto-gravity trigger after each push (e.g. http://pi.hole) |
PHLIST_PIHOLE_KEY |
(unset) | Optional: Pi-hole API key used with PHLIST_PIHOLE_URL |
Choose the narrowest bind address that still lets Pi-hole reach the server:
Local-only: Keep PHLIST_HOST=127.0.0.1. Only software running on the same machine can connect. This is the safest default, but a Pi-hole on another device cannot subscribe to it directly.
Tailscale-only: Set PHLIST_HOST to your Tailscale IP (100.x.y.z). Pi-hole subscribes via that same Tailscale IP:
http://100.x.y.z:8765/lists/slug.txt
No TLS needed at the Flask level — Tailscale handles encryption end-to-end.
LAN / all-interfaces: Set PHLIST_HOST=0.0.0.0 only when Pi-hole is on another LAN device and cannot reach this server through Tailscale. 0.0.0.0 is not a private LAN address; it means "listen on every network interface", including Wi-Fi, Ethernet, VPN, and any other active interface. Pi-hole subscribes via your server's LAN IP:
http://.PUT.IP.HERE:8765/lists/slug.txt
If you use 0.0.0.0, protect the host with your firewall and do not expose port 8765 to the internet.
- Bearer token auth with constant-time comparison (
hmac.compare_digest) — prevents timing attacks - HTTP security headers —
X-Frame-Options: DENY,X-Content-Type-Options: nosniff, andContent-Security-Policyon every response; protects the dashboard against clickjacking and MIME-sniffing - Strict content validation — every uploaded line must be ASCII-only and match a known blocklist format; non-ASCII characters (Unicode homoglyphs, zero-width chars, bidi overrides) are rejected with a detailed error showing which line failed
- Rate limiting — 10 req/min on health check, 5 req/min on PUT uploads
- Atomic writes — lists are written to a temp file and renamed, so Pi-hole never reads a partial file
- Slug validation — only
[a-z0-9-]allowed, prevents path traversal - Safe dashboard rendering — list slugs and URLs are inserted via
textContent(notinnerHTML), preventing XSS if slug validation were ever loosened - No key in logs — gravity trigger logs the Pi-hole base URL only;
PIHOLE_KEYnever appears in the systemd journal - systemd hardening —
ProtectSystem=strict,UMask=0022, dedicatedphlistuser,ReadWritePathslocked to list directory
pip install pytest
pytest tests/ -vphlist_server.py — Server (Flask app, routes, content validation, system stats)
templates/
dashboard.html — Web dashboard template
static/
style.css — Dashboard styles
dashboard.js — Dashboard interactivity
favicon.svg — Browser tab icon
tests/
test_server.py — 48 tests (auth, CRUD, slug, content validation, dashboard, delete, security headers, gravity-log key-leak, stats, preview)
systemd/
phlist-server.service — systemd unit for production deployment
scripts/
update-deployed.sh — Update an existing /opt/phlist-server systemd deployment
.env.example — Config template