All apps · 0 apps
vpn-gateway
Docker app from ProphetSe7en's Repository
Overview
Readme
View on GitHubvpn-gateway
VPN gateway with nftables bandwidth management, scheduling, hot-reload, and per-interface rate limiting.
Built as a layer on top of hotio/base:alpinevpn — adds bandwidth limiting via nftables limit rate rules, with time-based scheduling and automatic config hot-reload.
Features
- Web UI — configure rules, rates, and schedule from a browser (port 6050)
- Simple config — set rates in MB/s, no conversion needed
- Hot-reload — edit
traffic.confand changes apply within 10 seconds, no restart required - Time-based scheduling — different rates for different times of day and days of the week
- Midnight carry-over — schedule rules persist across midnight until the next rule takes over
- VPN reconnect recovery — watchdog detects when hotio rebuilds its nft table and re-applies rules
- Upload + download — independent rate limits for each direction
- Burst control — configurable burst buffer for smooth TCP throughput
- Traffic stats — real-time bandwidth graphs, 72-hour ring buffer, 365-day daily volumes, per-service breakdown
- Per-service monitoring — track bandwidth for qBittorrent (API), SABnzbd (API), and Dispatcharr (nftables counters for smooth 3s updates). Active Streams panel for Dispatcharr shows live channel/client info
- Stats persistence — all traffic data survives container restarts (saved every 5 min + on shutdown)
How it works
nftables rate-limit rules are inserted into hotio's existing firewall chains. Traffic exceeding the configured rate is dropped (policing). TCP congestion control adapts to the limit — in testing, effective throughput was consistently ~97% of the configured rate.
All containers sharing the VPN gateway's network namespace (e.g., qBittorrent instances using --net=container:vpn-gateway) are affected by the same limits. The rate is aggregate, not per-container.
Quick start
Build locally
git clone https://github.com/ProphetSe7en/vpn-gateway.git
cd vpn-gateway
docker build -t vpn-gateway:latest .
Pull from GHCR
⚠️ Use a pinned version tag, not
latest. This container manages your VPN and network routing — if an update introduces breaking changes, every container routed through it (qBittorrent, etc.) loses connectivity and won't recover until vpn-gateway is fixed or rolled back. Pin to a version and update manually when you're ready.
Latest version: v1.4.7 — all tags
docker pull ghcr.io/prophetse7en/vpn-gateway:v1.4.7
Run
Use this image as a drop-in replacement for ghcr.io/hotio/base:alpinevpn. All hotio configuration (WireGuard, port forwarding, DNS, etc.) works exactly the same.
docker run -d \
--name vpn-gateway \
--cap-add=NET_ADMIN \
--sysctl net.ipv6.conf.all.disable_ipv6=1 \
-v /path/to/config:/config \
-e VPN_ENABLED=true \
-e VPN_CONF=wg0 \
-p 6050:6050 \
-e VPN_EXPOSE_PORTS_ON_LAN=6050/tcp \
-e PRIVNET=192.168.0.0/24 \
ghcr.io/prophetse7en/vpn-gateway:v1.4.7
On first start, a default traffic.conf is created in /config/ with all options documented.
First-run setup: vpn-gateway redirects to /setup on first visit to create an admin account. See Authentication below.
Web UI
The web UI is available on port 6050. To enable it:
- Map port 6050 in your container config (
-p 6050:6050) - Add
6050/tcptoVPN_EXPOSE_PORTS_ON_LANso hotio's firewall allows LAN access - Open
http://<server-ip>:6050in your browser — first visit redirects to/setupto create an admin account
The UI has three tabs:
- Traffic — real-time throughput graph, per-service breakdown, Active Streams panel (Dispatcharr)
- Volume — historical bandwidth data (1h to all-time), per-service period summaries
- Settings — sidebar navigation with Bandwidth, Schedule, Service Monitoring, and Tools sections
Changes saved via the UI are written to both /config/.traffic-ui.json (UI model) and /config/traffic.conf (bash config). The config watcher picks up changes within 10 seconds.
You can also edit traffic.conf manually — the UI reads whichever file is newer.
Authentication
As of v1.4.0, vpn-gateway requires a login before you can reach the web UI. The model mirrors Radarr/Sonarr's Security panel.
Authentication — how users log in:
- Forms (login page) (default) — standard username/password form + session cookie (30-day TTL).
- Basic — HTTP Basic Auth (browser popup). Use this only when a reverse proxy in front is already handling login.
- None — disables auth entirely. Requires password confirmation to enable because the blast radius is catastrophic: anyone who reaches the port is admin and can change rules, disable shaping, see credentials. Only safe on a host unreachable from other devices.
Authentication Required — who must log in:
- Disabled for Trusted Networks (default) — devices on the "trusted" CIDR list skip the login page. Convenient for LAN-only deployments.
- Enabled (all traffic) — every request needs credentials, even from your own LAN.
First-run setup
- Open
http://your-host:6050— you'll be redirected to/setup - Create an admin username and password (min 10 characters, 2+ of upper/lower/digit/symbol)
- You're logged in automatically and land in the main UI
Credentials are bcrypt-hashed (cost 12) and stored in /config/auth.json. Sessions persist across container restarts via /config/sessions.json.
Trusted Networks
By default "trusted" means all private IPv4 + IPv6 ranges (RFC1918, link-local, ULA, loopback — Radarr-parity). Anything in this list gets full admin access without a password — that includes every other container on your Docker host and every device on your home WiFi.
To tighten: go to Settings → Security and list specific IPs/CIDRs:
192.168.0.0/24— whole home VLAN10.0.0.0/24— WireGuard tunnel192.168.0.5/32— a single device
Loopback (127.x) is always trusted so Docker healthchecks work regardless of this list.
Host-level lockdown: set the TRUSTED_NETWORKS env var in your Unraid template or docker-compose.yml (same place as VPN_ENABLED, VPN_CONF, etc.) with the same comma-separated CIDR format. When set, the UI field is disabled with an amber banner — the trust boundary can only be changed by editing the template and restarting. Defends against UI-takeover attackers (session hijack, XSS, local-bypass peer).
API Key
Every install gets an API key (visible in Settings → Security, rotatable). Send it on requests as:
X-Api-Key: <your-key>
or as a query parameter (legacy — leaks to access logs and browser history):
?apikey=<your-key>
Use this for scripts, Uptime Kuma, and any /api/* endpoint. API-key auth bypasses both the login requirement and CSRF protection.
Exceptions — public endpoints (no auth needed):
/api/health— liveness probe for Docker HEALTHCHECK, Uptime Kuma, reverse-proxy health tests. Returns{"ok":true}./api/stats/widget— formatted aggregate stats for Homepage. Same risk profile as/api/health(no secrets, no enumeration), kept public so existing Homepage installs keep working after the v1.4.0 upgrade without re-configuring.
Homepage widget
The Homepage widget described below keeps working with no changes — /api/stats/widget is in the public-endpoint list above.
Reverse-proxy deployment
Behind SWAG / Authelia / Traefik / Caddy that terminates TLS:
- Set Trusted Proxies to the proxy's IP (or use
TRUSTED_PROXIESenv var to lock at host level — same format asTRUSTED_NETWORKS). - Ensure the proxy sends
X-Forwarded-ForandX-Forwarded-Proto: https. - Pick either Forms (vpn-gateway handles login) or Basic (reverse proxy handles login upstream).
vpn-gateway only trusts X-Forwarded-* headers when the direct peer IP matches a configured Trusted Proxy — prevents header spoofing from other containers on the same bridge network.
Lost password recovery
No email reset flow — by design, /config/auth.json is authoritative. To recover:
- Stop the container
- Delete
/config/auth.json(credentials only — your schedule, bandwidth config, and traffic stats all live in/config/traffic.conf,/config/.traffic-ui.json, and/config/.traffic-stats.json, untouched) - Start the container
- Open the web UI — you'll be redirected to
/setupagain to create new credentials
This is safe on a machine where you have /config access. If someone ELSE can delete that file, they can also take over vpn-gateway — which is expected behavior for a local admin tool.
Configuration
Edit /config/traffic.conf or use the web UI. Changes are detected automatically within 10 seconds.
Default rates
# Values in MB/s. Set to 0 for unlimited.
DEFAULT_DOWN=75
DEFAULT_UP=75
Schedule
Each rule says "from this time, use this rate". Rules stay active until the next rule takes over — even across midnight.
SCHEDULE_ENABLED=true
# Weekday nights: full speed
SCHEDULE_1_TIME="23:00"
SCHEDULE_1_DOWN=0
SCHEDULE_1_UP=0
SCHEDULE_1_DAYS="mon-thu"
# Weekday mornings: limited
SCHEDULE_2_TIME="06:00"
SCHEDULE_2_DOWN=75
SCHEDULE_2_UP=75
SCHEDULE_2_DAYS="mon-fri"
# Midday: full speed (everyone at work/school)
SCHEDULE_3_TIME="07:30"
SCHEDULE_3_DOWN=0
SCHEDULE_3_UP=0
SCHEDULE_3_DAYS="mon-fri"
# Afternoon/evening: limited
SCHEDULE_4_TIME="15:00"
SCHEDULE_4_DOWN=75
SCHEDULE_4_UP=75
SCHEDULE_4_DAYS="mon-fri"
# Weekends overnight: full speed
SCHEDULE_5_TIME="01:00"
SCHEDULE_5_DOWN=0
SCHEDULE_5_UP=0
SCHEDULE_5_DAYS="sat,sun"
# Weekends daytime: limited
SCHEDULE_6_TIME="11:00"
SCHEDULE_6_DOWN=75
SCHEDULE_6_UP=75
SCHEDULE_6_DAYS="sat,sun"
Day filters support ranges (mon-fri), lists (mon,wed,fri), single days (tue), or omit for every day.
Advanced
# Burst buffer in milliseconds (default: 500)
# Higher = smoother throughput, Lower = stricter enforcement
BURST_MS=500
# Log rate changes to container log (default: true)
LOG_CHANGES=true
Useful commands
# Show active rules and packet counters
docker exec vpn-gateway nft-apply status
# Remove all limits (unlimited)
docker exec vpn-gateway nft-apply clear
# Force re-read config now (instead of waiting 10s)
docker exec vpn-gateway nft-apply reload
# View rate changes and schedule triggers
docker logs vpn-gateway
Config upgrades
When a new version adds config options, the service automatically adds missing options to your existing traffic.conf while preserving all your settings. A CONFIG_VERSION field tracks this — don't edit it manually.
Architecture
traffic.conf ──→ svc-traffic (s6 service)
├── nft-apply (insert/replace/delete nft rules)
├── crond (schedule triggers + verify watchdog every 60s)
└── config watcher (md5sum poll every 10s → hot-reload)
svc-webui (s6 service, port 6050)
├── GET /api/status — current rates + active rule
├── GET /api/config — full config (JSON or parsed bash)
├── PUT /api/config — save config (writes both JSON + bash)
├── GET /api/stats/stream — SSE live traffic stats (3s intervals)
├── GET /api/stats/latest — current stats snapshot
├── GET /api/stats/history — ring buffer history (1h/6h/24h/72h)
├── GET /api/stats/daily — daily volume data (365 days)
├── POST /api/stats/reset — clear all statistics
└── static files — Alpine.js SPA (embedded at build time)
Traffic measurement:
wg0 rx_bytes - nft dropped bytes = actual VPN throughput
(nft drops excess packets; wg0 counts them before drop)
nft rules are inserted into hotio's existing inet hotio table:
output chain: upload limit before hotio's wg0 accept rule
input chain: download limit before hotio's wg0 ct state accept rule
Traffic Monitoring
The Stats tab shows real-time and historical bandwidth data:
- VPN-gateway total — all traffic through the WireGuard tunnel (payload + TCP/IP headers + WireGuard encryption + protocol overhead)
- Per-service totals — application-level data for qBittorrent (API), SABnzbd (API), and Dispatcharr (nft byte counters). Each service shows individual download/upload rates and cumulative totals.
VPN total includes all tunnel traffic (data + protocol overhead + encryption). Per-service totals track application data only, so they will always be lower than the VPN total.
Stats are persisted to /config/.traffic-stats.json every 5 minutes and on graceful shutdown. Data includes a 72-hour ring buffer (3-second samples), 365 days of daily volumes, and per-service cumulative totals. The file is ~13 MB at maximum size and does not grow beyond that.
Routing qBittorrent through vpn-gateway
Route one or more qBittorrent containers through the VPN gateway so all torrent traffic is encrypted, while the Web UI remains accessible on your LAN.
Step-by-step setup guide with screenshots — covers TorGuard/WireGuard, port mapping, Docker Compose example, and multiple instances.
Quick troubleshooting
qBittorrent Web UI not accessible:
- Check that the port appears in all three vpn-gateway locations (port mapping,
VPN_EXPOSE_PORTS_ON_LAN, container port) - Check that
WEBUI_PORTSon qBit matches the container port on vpn-gateway - Check vpn-gateway logs:
docker logs vpn-gateway
qBittorrent won't start:
- Remove all port mappings from the qBit container — they conflict with container network mode
- Set
VPN_ENABLED=falseon qBit (or remove VPN variables entirely) — the gateway handles VPN
Multiple instances conflict:
- Each qBit instance must have a different
WEBUI_PORTSvalue - You cannot have two containers both listening on the same port on the same network stack
Port forwarding not working:
- Verify
VPN_PORT_REDIRECTSformat:vpn_port@container_port/tcp - Verify qBittorrent's incoming connection port matches the container port (after
@) - Verify the port forward is active in your VPN provider's account
Unraid
Install via Community Apps: Search for vpn gateway (without hyphen) in the Apps tab — click Install and configure your WireGuard settings.
Or install manually: Go to Docker → Add Container, set Repository to ghcr.io/prophetse7en/vpn-gateway:v1.4.7, and add the required paths, ports, and capabilities (see above).
The Web UI is available at http://your-unraid-ip:6050.
Container Network Fix (Recommended)
When vpn-gateway restarts, Docker assigns it a new container ID. Containers using container:vpn-gateway network mode (like qBittorrent) keep the old reference and lose network connectivity. On Unraid, the only fix is to force-recreate each dependent container (edit → Apply without changes).
containernetwork-autofix automates this — it detects when a network-parent container restarts and automatically recreates dependent containers. Install it alongside vpn-gateway to avoid manual intervention after restarts or updates.
Important: Use the ProphetSe7en/containernetwork-autofix fork — the original has parser bugs that cause it to miss containers with certain label formats.
Updating: Change the version tag in the Repository field to the new version, then click Apply. Do not use latest — see Pull from GHCR for why.
Homepage Widget
vpn-gateway has a built-in widget endpoint for Homepage dashboards. Add this to your services.yaml:
- VPN Gateway:
icon: https://raw.githubusercontent.com/prophetse7en/vpn-gateway/main/icon.png
href: http://YOUR_IP:6050
widget:
type: customapi
url: http://vpn-gateway:6050/api/stats/widget
refreshInterval: 10000
display: block
mappings:
- field: dlSpeed
label: DL Speed
- field: ulSpeed
label: UL Speed
- field: totalDl
label: Total DL
- field: totalUl
label: Total UL
Replace YOUR_IP with your server IP, and vpn-gateway in the widget URL with the container hostname (or IP if Homepage is on a different Docker network).
Available fields
| Field | Example | Description |
|---|---|---|
dlSpeed |
45.2 MB/s |
Current download speed |
ulSpeed |
12.8 MB/s |
Current upload speed |
totalDl |
2.35 TB |
Total downloaded (since stats reset) |
totalUl |
8.71 TB |
Total uploaded |
dailyDl |
124.5 GB |
Downloaded in the last 24 hours |
dailyUl |
48.3 GB |
Uploaded in the last 24 hours |
All values are pre-formatted — no scale, suffix, or format needed in Homepage. Pick the fields you want by adding or removing entries from mappings.
Credits
Built on hotio/base:alpinevpn by hotio. All VPN functionality (WireGuard, firewall, DNS leak protection) is provided by the hotio base image. vpn-gateway only adds the bandwidth management layer.
Support
For questions, help, or bug reports:
- Discord:
#prophetse7en-appson the TRaSH Guides Discord (under Community Apps) - GitHub: prophetse7en/vpn-gateway/issues
Development
vpn-gateway is developed with active AI assistance (Claude, Anthropic) under human direction. Architectural decisions, code review, testing on a real Unraid + qBittorrent stack, and releases are done by ProphetSe7en. Issues and PRs go through a human review.
License
MIT
Requirements
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/prophetse7en/vpn-gateway:v1.4.7Runtime arguments
- Web UI
http://[IP]:[PORT:6050]- Network
bridge- Shell
bash- Privileged
- false
- Extra Params
--hostname=vpn-gateway.internal --cap-add=NET_ADMIN
Template configuration
Traffic monitor and bandwidth management web UI
- Target
- 6050
- Default
- 6050
Web UI port for a container routed through the gateway (e.g. qBittorrent, Deluge, IPTV). Must match the listening port on that container and be included in VPN_EXPOSE_PORTS_ON_LAN. Add additional port mappings for more containers.
- Target
- 7075
- Default
- 7075
Config directory (WireGuard conf, traffic.conf, stats)
- Target
- /config
- Default
- /mnt/user/appdata/vpn-gateway
Container Variable: VPN_ENABLED
- Default
- true|false
Container Variable: VPN_CONF
- Default
- wg0
Container Variable: VPN_PROVIDER
- Default
- generic|proton|pia
Container Variable: VPN_LAN_NETWORK
- Default
- 192.168.1.0/24
Container Variable: VPN_LAN_LEAK_ENABLED
- Default
- false|true
Container Variable: VPN_EXPOSE_PORTS_ON_LAN
Container Variable: VPN_AUTO_PORT_FORWARD
- Default
- false|true
Container Variable: VPN_PORT_REDIRECTS
Container Variable: VPN_HEALTHCHECK_ENABLED
- Default
- true|false
Container Variable: VPN_NAMESERVERS
- Default
- 1.1.1.1,8.8.8.8
Container Variable: VPN_PIA_USER
Container Variable: VPN_PIA_PASS
Container Variable: VPN_PIA_PREFERRED_REGION
Container Variable: VPN_PIA_DIP_TOKEN
Container Variable: VPN_PIA_PORT_FORWARD_PERSIST
- Default
- false|true
Container Variable: PUID
- Default
- 99
Container Variable: PGID
- Default
- 100
Container Variable: UMASK
- Default
- 002
Optional: pin the Trusted Networks CIDR list at host level. Comma-separated list (e.g. 192.168.0.0/24, 192.168.0.5/32). When set, overrides the UI value and the UI field is locked. Leave empty to manage from the Security panel.
Optional: pin the Trusted Proxies list at host level for reverse-proxy deployments (SWAG, Authelia). Comma-separated list of proxy IPs that are allowed to set X-Forwarded-For. When set, overrides the UI value and the UI field is locked.