A lightweight, production-ready Go application that automatically synchronizes port forwarding changes from Gluetun VPN to qBittorrent. Built with observability and reliability in mind.
- Automatic Port Synchronization: Monitors Gluetun's forwarded port file and updates qBittorrent instantly
- Webhook Notifications: Send HTTP POST notifications when port changes occur for external integrations
- Full Observability: Prometheus metrics for monitoring and alerting
- Health & Readiness: Kubernetes-compatible health check endpoints
- Efficient File Watching: Uses fsnotify for real-time file system events
- Secure by Default: Runs as non-root user in Docker, minimal attack surface
- Lightweight: ~15MB Docker image, minimal resource footprint
- Production Ready: Automatic re-authentication, graceful error handling, fallback polling
An example docker-compose.yml file is available at docker-compose.example.yml.
docker run -d \
--name forwardarr \
-e GLUETUN_PORT_FILE=/tmp/gluetun/forwarded_port \
-e TORRENT_CLIENT_URL=http://qbittorrent:8080 \
-e TORRENT_CLIENT_USER=admin \
-e TORRENT_CLIENT_PASSWORD=adminadmin \
-v gluetun-data:/tmp/gluetun:ro \
-p 9090:9090 \
ghcr.io/eslutz/forwardarr:latestForwardarr is configured via environment variables. For a complete, ready-to-use configuration file, see docs/.env.example.
| Variable | Default | Description |
|---|---|---|
GLUETUN_PORT_FILE |
/tmp/gluetun/forwarded_port |
Path to Gluetun's forwarded port file |
TORRENT_CLIENT_URL |
http://localhost:8080 |
qBittorrent WebUI address |
TORRENT_CLIENT_USER |
admin |
qBittorrent username |
TORRENT_CLIENT_PASSWORD |
adminadmin |
qBittorrent password |
SYNC_INTERVAL |
300 |
Polling interval in seconds (0 to disable) |
METRICS_PORT |
9090 |
HTTP server port for health/metrics |
LOG_LEVEL |
info |
Log level: debug, info, warn, error |
STARTUP_RETRY_DELAY |
5 |
Base seconds between startup attempts (exponential backoff; attempts derived from timeout) |
STARTUP_TIMEOUT |
120 |
Overall startup deadline in seconds before exiting |
| Variable | Default | Description |
|---|---|---|
WEBHOOK_URL |
Webhook endpoint (leave empty to disable) | |
WEBHOOK_TEMPLATE |
json |
Format: json, discord, slack, gotify |
WEBHOOK_EVENTS |
port_changed |
Events to trigger webhooks |
WEBHOOK_TIMEOUT |
10 |
Request timeout in seconds |
📋 See docs/.env.example for complete configuration with detailed comments and examples.
┌─────────┐ ┌──────────┐ ┌────────────┐
│ Gluetun │ writes │ Port │ watched │ │
│ VPN ├────────►│ File ├────────►│ Forwardarr │
└─────────┘ └──────────┘ └─────┬──────┘
│
│ updates
▼
┌─────────────┐
│ qBittorrent │
└─────────────┘How it works:
- Gluetun establishes a VPN connection with port forwarding
- Gluetun writes the forwarded port to a file
- Forwardarr watches this file for changes using fsnotify
- When the port changes, Forwardarr updates qBittorrent's listening port via API
- A fallback ticker ensures sync even if file events are missed (configurable, can be disabled)
Forwardarr can send HTTP POST notifications when port changes occur. This is useful for integrating with other services or triggering automation workflows.
Configure webhooks via environment variables:
WEBHOOK_URL=http://your-server.com/webhook
WEBHOOK_TEMPLATE=json # Options: json, discord, slack, gotify
WEBHOOK_EVENTS=port_changed # Comma-separated event list
WEBHOOK_TIMEOUT=10 # Timeout in secondsForwardarr supports multiple webhook formats:
JSON (default) - Generic JSON payload
{
"event": "port_changed",
"timestamp": "2026-01-08T12:00:00Z",
"old_port": 8080,
"new_port": 9090,
"message": "Port changed from 8080 to 9090"
}Discord - Formatted for Discord webhooks with embeds
WEBHOOK_TEMPLATE=discord
WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_WEBHOOKSlack - Formatted for Slack webhooks with blocks
WEBHOOK_TEMPLATE=slack
WEBHOOK_URL=https://hooks.slack.com/services/YOUR_WEBHOOKGotify - Formatted for Gotify push notifications
WEBHOOK_TEMPLATE=gotify
WEBHOOK_URL=https://gotify.example.com/message?token=YOUR_TOKENControl which events trigger webhooks using WEBHOOK_EVENTS:
WEBHOOK_EVENTS=port_changed # Only port changes (default)Currently supported events:
port_changed- Triggered when the forwarded port is successfully updated in qBittorrent
Note: Currently, port_changed is the only event type available. The event filtering system is designed for extensibility, allowing additional events to be added in future releases (such as sync_error, startup, or shutdown).
- Webhooks are sent with
Content-Type: application/json - User-Agent is set to
Forwardarr-Webhook/1.0 - Consider using HTTPS URLs for webhook endpoints
- Implement signature verification on your webhook receiver if needed
- Webhook failures are logged but do not prevent port updates
| Endpoint | Purpose | Response |
|---|---|---|
GET /health |
Liveness probe | 200 OK if running |
GET /ready |
Readiness probe | 200 OK if qBittorrent is reachable |
GET /status |
Full diagnostics | JSON status object |
GET /metrics |
Prometheus metrics | Metrics in OpenMetrics format |
- /health: Configure this as a Liveness Probe. It indicates if the Forwardarr process is running. If this fails, the container should be restarted.
- /ready: Configure this as a Readiness Probe. It indicates if Forwardarr can successfully communicate with qBittorrent. If this fails, the container should remain running but not receive traffic/work until the dependency recovers.
- /status: Use this for manual debugging or external monitoring dashboards. It provides a JSON snapshot of the application's internal state, including version and connectivity status.
- /metrics: Configure your Prometheus scraper to target this endpoint to collect application performance data.
| Metric | Type | Description |
|---|---|---|
forwardarr_info |
Gauge | Build information (version, commit, date) |
forwardarr_current_port |
Gauge | Current forwarded port from Gluetun |
forwardarr_sync_total |
Counter | Total number of successful port syncs |
forwardarr_sync_errors |
Counter | Total number of failed sync attempts |
forwardarr_last_sync_timestamp |
Gauge | Unix timestamp of last successful sync |
# Current forwarded port
forwardarr_current_port
# Sync success rate (last 5m)
rate(forwardarr_sync_total[5m]) / (rate(forwardarr_sync_total[5m]) + rate(forwardarr_sync_errors[5m]))
# Time since last successful sync
time() - forwardarr_last_sync_timestamp
A pre-built Grafana dashboard is available at docs/dashboard.json. Import it into your Grafana instance to visualize:
- Application version and build info
- Current forwarded port with change history
- Sync operation success rates and error counts
- Go runtime metrics (memory, goroutines, CPU)
- Time since last successful sync
- Verify qBittorrent is accessible at the configured address
- Check credentials are correct
- Ensure network connectivity between containers
- Check logs:
docker logs forwardarr
- Verify Gluetun is writing to the port file:
cat /tmp/gluetun/forwarded_port - Check the port file path is correct in Forwardarr config
- Ensure the volume mount is working:
docker exec forwardarr cat /tmp/gluetun/forwarded_port - Increase log level to debug:
LOG_LEVEL=debug
- Increase
SYNC_INTERVALto reduce polling frequency - Check for excessive file system events in the watched directory
Contributions are welcome! Please follow these guidelines when submitting changes.
# Clone the repository
git clone https://github.com/eslutz/forwardarr.git
cd forwardarr
# Install dependencies
go mod download
# Build binary
go build -o forwardarr ./cmd/forwardarr
# Build Docker image
docker build -t forwardarr .# Run tests
go test ./...
# Run tests with race detector and coverage
go test -race -coverprofile=coverage.out -covermode=atomic ./...
# View overall coverage
go tool cover -func=coverage.out | tail -1
# View filtered coverage (CI excludes the entrypoint file)
grep -v "cmd/forwardarr/main.go" coverage.out > coverage-filtered.out
go tool cover -func=coverage-filtered.out | tail -1
# Run linter
golangci-lint run
# Run locally
export GLUETUN_PORT_FILE=/path/to/port/file
export TORRENT_CLIENT_URL=http://localhost:8080
export TORRENT_CLIENT_USER=admin
export TORRENT_CLIENT_PASSWORD=adminadmin
go run ./cmd/forwardarrCI enforces ≥60% coverage on the filtered profile (excluding the cmd/forwardarr/main.go entrypoint).
Before submitting a pull request:
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Run linters and tests
- Submit a pull request
See our Pull Request Template for more details.
Security is a top priority for this project. If you discover a security vulnerability, please follow responsible disclosure practices.
Reporting Vulnerabilities:
Please report security vulnerabilities through GitHub Security Advisories: https://github.com/eslutz/forwardarr/security/advisories/new
Alternatively, you can view our Security Policy for additional contact methods and guidelines.
Security Best Practices:
- Keep your installation up to date with the latest releases
- Use strong, unique passwords for qBittorrent credentials
- Avoid exposing the metrics port to the public internet
- Review and understand the volume mount permissions
- Regularly monitor logs for suspicious activity
This project is licensed under the MIT License. See the LICENSE file for details.
You are free to use, modify, and distribute this software under the terms of the MIT License.
This project is built with and inspired by excellent open-source software:
- Gluetun - VPN client with port forwarding support
- qBittorrent - Feature-rich BitTorrent client
- fsnotify - Cross-platform file system notifications for Go
- Prometheus - Monitoring system and time series database
Special thanks to the open-source community for their contributions and support.
Other tools in the ecosystem: