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.
- 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)
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.
git clone https://github.com/ProphetSe7en/vpn-gateway.git
cd vpn-gateway
docker build -t vpn-gateway:latest .
⚠️ Use a pinned version tag, notlatest. 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.3.0 — all tags
docker pull ghcr.io/prophetse7en/vpn-gateway:v1.3.0Use 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.86.0/24 \
ghcr.io/prophetse7en/vpn-gateway:v1.3.0On first start, a default traffic.conf is created in /config/ with all options documented.
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
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.
Edit /config/traffic.conf or use the web UI. Changes are detected automatically within 10 seconds.
# Values in MB/s. Set to 0 for unlimited.
DEFAULT_DOWN=75
DEFAULT_UP=75Each 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.
# 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# 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-gatewayWhen 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.
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
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.
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.
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
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.3.0, and add the required paths, ports, and capabilities (see above).
The Web UI is available at http://your-unraid-ip:6050.
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.
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 ULReplace 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).
| 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.
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.
For questions, help, or bug reports:
- Discord:
#prophetse7en-appson the TRaSH Guides Discord (under Community Apps) - GitHub: prophetse7en/vpn-gateway/issues
MIT