Rename to Forwardarr, add unit tests, consolidate CI workflows, and update environment variables#8
Conversation
… tests Co-authored-by: eslutz <67699321+eslutz@users.noreply.github.com>
Co-authored-by: eslutz <67699321+eslutz@users.noreply.github.com>
|
@copilot I've started running into problems using this image. It's unable to connect to qbittorrent to update the port number setting. ericslutz@home-mac-mini Torrent-Services(main) > docker logs gluetarr
{"time":"2025-12-11T16:52:22.970551758Z","level":"INFO","msg":"starting gluetarr","gluetun_port_file":"/tmp/gluetun/forwarded_port","qbit_addr":"http://gluetun:8080","sync_interval":60000000000,"metrics_port":"9090"}
{"time":"2025-12-11T16:52:23.010206091Z","level":"ERROR","msg":"failed to create qBittorrent client","error":"initial login failed: login failed: status 200, body: Fails."}
{"time":"2025-12-11T16:52:23.3236803Z","level":"INFO","msg":"starting gluetarr","gluetun_port_file":"/tmp/gluetun/forwarded_port","qbit_addr":"http://gluetun:8080","sync_interval":60000000000,"metrics_port":"9090"}
{"time":"2025-12-11T16:52:23.365962758Z","level":"ERROR","msg":"failed to create qBittorrent client","error":"initial login failed: login failed: status 200, body: Fails."}
{"time":"2025-12-11T16:52:23.688287841Z","level":"INFO","msg":"starting gluetarr","gluetun_port_file":"/tmp/gluetun/forwarded_port","qbit_addr":"http://gluetun:8080","sync_interval":60000000000,"metrics_port":"9090"}
{"time":"2025-12-11T16:52:23.73216355Z","level":"ERROR","msg":"failed to create qBittorrent client","error":"initial login failed: login failed: status 200, body: Fails."}
{"time":"2025-12-11T16:52:24.251615175Z","level":"INFO","msg":"starting gluetarr","gluetun_port_file":"/tmp/gluetun/forwarded_port","qbit_addr":"http://gluetun:8080","sync_interval":60000000000,"metrics_port":"9090"}
{"time":"2025-12-11T16:52:24.293617508Z","level":"ERROR","msg":"failed to create qBittorrent client","error":"initial login failed: login failed: status 200, body: Fails."}
{"time":"2025-12-11T16:52:26.680893051Z","level":"INFO","msg":"starting gluetarr","gluetun_port_file":"/tmp/gluetun/forwarded_port","qbit_addr":"http://gluetun:8080","sync_interval":60000000000,"metrics_port":"9090"}
{"time":"2025-12-11T16:52:26.725949218Z","level":"ERROR","msg":"failed to create qBittorrent client","error":"initial login failed: login failed: status 200, body: Fails."}
{"time":"2025-12-11T16:52:28.516000052Z","level":"INFO","msg":"starting gluetarr","gluetun_port_file":"/tmp/gluetun/forwarded_port","qbit_addr":"http://gluetun:8080","sync_interval":60000000000,"metrics_port":"9090"}
{"time":"2025-12-11T16:52:28.558796469Z","level":"ERROR","msg":"failed to create qBittorrent client","error":"initial login failed: login failed: status 200, body: Fails."}This is how it's set up in the docker config file: services:
# Gluetun - VPN Client (ProtonVPN with Port Forwarding)
# Provides VPN tunnel with automatic port forwarding for qBittorrent
# Port forwarding enables incoming peer connections for better torrent performance
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
# ProtonVPN Configuration
- VPN_SERVICE_PROVIDER=protonvpn
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
# Server Selection
- SERVER_COUNTRIES=${SERVER_COUNTRIES:-}
- FREE_ONLY=${FREE_ONLY:-}
# Port Forwarding (automatic)
- VPN_PORT_FORWARDING=on
- VPN_PORT_FORWARDING_PROVIDER=protonvpn
# Timezone
- TZ=${TZ:-America/New_York}
ports:
# qBittorrent Web UI
- 8080:8080
# Forwarded port will be automatically assigned by ProtonVPN
# Check with: docker exec gluetun cat /tmp/gluetun/forwarded_port
volumes:
- ./config/gluetun:/gluetun
- gluetun-tmp:/tmp/gluetun
- ./scripts/healthchecks:/scripts:ro
restart: unless-stopped
networks:
- torrent-services-network
stop_signal: SIGTERM
stop_grace_period: 30s
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "sh", "/scripts/gluetun.sh"]
interval: 15s
timeout: 20s
retries: 3
start_period: 180s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mem_limit: ${GLUETUN_MEM_LIMIT:-256m}
cpus: ${GLUETUN_CPUS:-0.5}
# Gluetarr - Sync Gluetun forwarded port to qBittorrent with metrics + health endpoints
gluetarr:
image: ghcr.io/eslutz/gluetarr:latest
container_name: gluetarr
environment:
- GLUETUN_PORT_FILE=/tmp/gluetun/forwarded_port
- QBIT_ADDR=http://gluetun:8080
- QBIT_USER=${SERVICE_USER:-admin}
- QBIT_PASSWORD=${QBIT_PASSWORD}
- SYNC_INTERVAL=${GLUETARR_SYNC_INTERVAL:-60}
- LOG_LEVEL=${GLUETARR_LOG_LEVEL:-info}
volumes:
- gluetun-tmp:/tmp/gluetun:ro
ports:
- 127.0.0.1:9090:9090
depends_on:
gluetun:
condition: service_healthy
qbittorrent:
condition: service_healthy
networks:
- torrent-services-network
restart: on-failure:5
healthcheck:
test: ["CMD", "wget", "-qO-", "--timeout=5", "http://localhost:9090/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mem_limit: ${QBIT_PORT_SYNC_MEM_LIMIT:-128m}
cpus: ${QBIT_PORT_SYNC_CPUS:-0.1}
# qBittorrent - Torrent Client
# Routes through Gluetun VPN with automatic port forwarding
# Port forwarding enables incoming peer connections for optimal performance
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment:
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
- TZ=${TZ:-America/New_York}
- WEBUI_PORT=8080
volumes:
- ./config/qbittorrent:/config
- ${DATA_DIR:-./media}:/media
- ./scripts/healthchecks:/scripts:ro
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped
stop_grace_period: 60s
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "sh", "/scripts/qbittorrent.sh"]
interval: 30s
timeout: 10s
retries: 3
start_period: 90s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mem_limit: ${QBITTORRENT_MEM_LIMIT:-2g}
cpus: ${QBITTORRENT_CPUS:-2.0}
# Prowlarr - Indexer Manager
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
environment:
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
- TZ=${TZ:-America/New_York}
- PROWLARR_API_KEY=${PROWLARR_API_KEY:-}
- COMPlus_EnableDiagnostics=0
- XDG_RUNTIME_DIR=/config/tmp
volumes:
- ./config/prowlarr:/config
- ./scripts/healthchecks:/scripts:ro
ports:
- 9696:9696
depends_on:
torarr:
condition: service_healthy
restart: unless-stopped
networks:
- torrent-services-network
healthcheck:
test: ["CMD", "sh", "/scripts/prowlarr.sh"]
interval: 45s
timeout: 10s
retries: 3
start_period: 120s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mem_limit: ${PROWLARR_MEM_LIMIT:-512m}
cpus: ${PROWLARR_CPUS:-1.0}
# Sonarr - TV Show Management
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
- TZ=${TZ:-America/New_York}
- SONARR_API_KEY=${SONARR_API_KEY:-}
volumes:
- ./config/sonarr:/config
- ${DATA_DIR:-./media}:/media
- ./scripts/healthchecks:/scripts:ro
ports:
- 8989:8989
depends_on:
prowlarr:
condition: service_healthy
qbittorrent:
condition: service_healthy
restart: unless-stopped
stop_grace_period: 30s
networks:
- torrent-services-network
healthcheck:
test: ["CMD", "sh", "/scripts/sonarr.sh"]
interval: 45s
timeout: 10s
retries: 3
start_period: 90s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mem_limit: ${SONARR_MEM_LIMIT:-1g}
cpus: ${SONARR_CPUS:-1.0}
# Radarr - Movie Management
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
- TZ=${TZ:-America/New_York}
- RADARR_API_KEY=${RADARR_API_KEY:-}
volumes:
- ./config/radarr:/config
- ${DATA_DIR:-./media}:/media
- ./scripts/healthchecks:/scripts:ro
ports:
- 7878:7878
depends_on:
prowlarr:
condition: service_healthy
qbittorrent:
condition: service_healthy
restart: unless-stopped
stop_grace_period: 30s
networks:
- torrent-services-network
healthcheck:
test: ["CMD", "sh", "/scripts/radarr.sh"]
interval: 45s
timeout: 10s
retries: 3
start_period: 90s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mem_limit: ${RADARR_MEM_LIMIT:-1g}
cpus: ${RADARR_CPUS:-1.0}
# Bazarr - Subtitle Management
bazarr:
image: lscr.io/linuxserver/bazarr:latest
container_name: bazarr
environment:
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
- TZ=${TZ:-America/New_York}
- BAZARR_API_KEY=${BAZARR_API_KEY:-}
volumes:
- ./config/bazarr:/config
- ${DATA_DIR:-./media}:/media
- ./scripts/healthchecks:/scripts:ro
ports:
- 6767:6767
depends_on:
sonarr:
condition: service_healthy
radarr:
condition: service_healthy
restart: unless-stopped
networks:
- torrent-services-network
healthcheck:
test: ["CMD", "sh", "/scripts/bazarr.sh"]
interval: 60s
timeout: 10s
retries: 3
start_period: 120s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mem_limit: ${BAZARR_MEM_LIMIT:-512m}
cpus: ${BAZARR_CPUS:-0.5}
# Torarr - SOCKS5 proxy for anonymous indexer access
# Provides access to .onion sites and bypasses blocks
torarr:
image: ghcr.io/eslutz/torarr:latest
container_name: torarr
environment:
- TZ=${TZ:-America/New_York}
ports:
- 127.0.0.1:9050:9050 # SOCKS5 proxy
- 127.0.0.1:8085:8085 # Health/metrics endpoint
volumes:
- tor-data:/var/lib/tor
restart: on-failure:5
networks:
- torrent-services-network
healthcheck:
test: ["CMD", "wget", "-qO-", "--timeout=10", "http://localhost:8085/health"]
interval: 60s
timeout: 15s
retries: 3
start_period: 180s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mem_limit: ${TOR_PROXY_MEM_LIMIT:-128m}
cpus: ${TOR_PROXY_CPUS:-0.25}
# qBittorrent Exporter for Prometheus
qbittorrent-exporter:
image: ghcr.io/martabal/qbittorrent-exporter:latest
container_name: qbittorrent-exporter
environment:
- QBITTORRENT_BASE_URL=http://gluetun:8080
- QBITTORRENT_USERNAME=${SERVICE_USER:-admin}
- QBITTORRENT_PASSWORD=${QBIT_PASSWORD}
- EXPORTER_PORT=8090
- LOG_LEVEL=INFO
- ENABLE_TRACKER=true
depends_on:
qbittorrent:
condition: service_healthy
ports:
- "8090:8090"
networks:
- torrent-services-network
restart: on-failure:5
profiles:
- monitoring
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "2"
mem_limit: ${QBITTORRENT_EXPORTER_MEM_LIMIT:-128m}
cpus: ${QBITTORRENT_EXPORTER_CPUS:-0.25}
# Scraparr - Unified *arr Prometheus exporter
scraparr:
image: ghcr.io/thecfu/scraparr:latest
container_name: scraparr
environment:
- GENERAL_PORT=7100
- GENERAL_PATH=/metrics
- SONARR_URL=http://sonarr:8989
- SONARR_API_KEY=${SONARR_API_KEY}
- SONARR_INTERVAL=30
- RADARR_URL=http://radarr:7878
- RADARR_API_KEY=${RADARR_API_KEY}
- RADARR_INTERVAL=30
- PROWLARR_URL=http://prowlarr:9696
- PROWLARR_API_KEY=${PROWLARR_API_KEY}
- PROWLARR_INTERVAL=30
- BAZARR_URL=http://bazarr:6767
- BAZARR_API_KEY=${BAZARR_API_KEY}
- BAZARR_INTERVAL=30
depends_on:
sonarr:
condition: service_healthy
radarr:
condition: service_healthy
prowlarr:
condition: service_healthy
bazarr:
condition: service_healthy
ports:
- "7100:7100"
networks:
- torrent-services-network
restart: on-failure:5
profiles:
- monitoring
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "2"
mem_limit: ${SCRAPARR_MEM_LIMIT:-256m}
cpus: ${SCRAPARR_CPUS:-0.5}
autoheal:
image: tmknight88/docker-autoheal:latest
container_name: autoheal
restart: unless-stopped
read_only: true
network_mode: none
environment:
- AUTOHEAL_CONNECTION_TYPE=socket
- AUTOHEAL_MONITOR_ALL=true
- AUTOHEAL_INTERVAL=30
- AUTOHEAL_START_DELAY=300
- AUTOHEAL_STOP_TIMEOUT=30
- AUTOHEAL_LOG_PERSIST=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /etc/localtime:/etc/localtime:ro
- autoheal-logs:/opt/docker-autoheal
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "2"
mem_limit: ${AUTOHEAL_MEM_LIMIT:-128m}
cpus: ${AUTOHEAL_CPUS:-0.1}
# Bootstrap - Orchestrates setup scripts
bootstrap:
image: mcr.microsoft.com/playwright/python:v1.49.0-jammy
container_name: bootstrap
profiles:
- bootstrap
volumes:
- .:/app
- /var/run/docker.sock:/var/run/docker.sock
working_dir: /app
environment:
- TZ=${TZ:-America/New_York}
- PROWLARR_URL=http://prowlarr:9696
- SONARR_URL=http://sonarr:8989
- RADARR_URL=http://radarr:7878
- BAZARR_URL=http://bazarr:6767
- QBIT_URL=http://gluetun:8080
# Pass host project directory for correct volume mounting in dind
# Uses HOST_PROJECT_DIR from .env if set (Windows), otherwise falls back to shell PWD (macOS/Linux)
- HOST_PROJECT_DIR=${HOST_PROJECT_DIR:-${PWD}}
command: >
sh -c "apt-get update && apt-get install -y docker.io docker-compose-plugin && pip install requests && python3 scripts/setup/bootstrap.py"
depends_on:
prowlarr:
condition: service_healthy
sonarr:
condition: service_healthy
radarr:
condition: service_healthy
bazarr:
condition: service_healthy
qbittorrent:
condition: service_healthy
networks:
- torrent-services-network
networks:
torrent-services-network:
name: torrent-net
driver: bridge
volumes:
gluetun-tmp:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
autoheal-logs:
tor-data:What needs to be done to fix this or at least enable better troubleshooting? I've confirmed the correct password is store in .env as QBIT_PASSWORD and the username is stored as SERVICE_USER. Is there any other info you need to troubleshoot the issue? |
Co-authored-by: eslutz <67699321+eslutz@users.noreply.github.com>
The issue is an environment variable mismatch. The application expects Fix: Change this line in your docker-compose file: - QBIT_PASSWORD=${QBIT_PASSWORD}To: - QBIT_PASS=${QBIT_PASSWORD}This will pass the password from your The error "login failed: status 200, body: Fails." occurs because qBittorrent is rejecting the credentials - the application was using the default password "adminadmin" instead of your actual password from the environment. For better troubleshooting in the future:
|
|
@copilot Ah, lets make that name more general then, something like |
…T_PASSWORD Co-authored-by: eslutz <67699321+eslutz@users.noreply.github.com>
Done in commit b31fad2. The environment variables have been renamed: Changes:
Updated files:
All tests pass and the build succeeds with the new environment variable names. |
|
@copilot Generate a |
Co-authored-by: eslutz <67699321+eslutz@users.noreply.github.com>
Created in commit 636a133. The Project Overview
Key Components (detailed documentation for each)
Prometheus Metrics
Development Guides
Common Tasks
Architecture Flow
Troubleshooting
The file is located at the repository root for easy discovery. |
|
@copilot still seeing some errors. Run golangci-lint: internal/server/handlers.go#L14Error return value of
|
Co-authored-by: eslutz <67699321+eslutz@users.noreply.github.com>
Fixed in commit 88ef1de. All linting errors have been resolved: Changes:
All tests pass and the code now satisfies the errcheck linter requirements. |
|
@copilot still getting a few linting errors that need to be resolved.
|
…ient code Co-authored-by: eslutz <67699321+eslutz@users.noreply.github.com>
Fixed in commit e13f090. All 11 linting errors have been resolved:
Linter now reports 0 issues. |
There was a problem hiding this comment.
@copilot trivy image vulnerability scan should also be included in this pipeline and the results uploaded to the security tab.
There was a problem hiding this comment.
Added in commit a2ddf55. Trivy image vulnerability scanning now runs in the release workflow after tests and CodeQL analysis, before building and publishing release images. Results are uploaded to the Security tab with category trivy-container-release.
Co-authored-by: eslutz <67699321+eslutz@users.noreply.github.com>
|
@copilot add unit tests to increase code coverage to >= 60% to pass code coverage requirement. |
|
@copilot add an exclusion for the main package and and any other code that is not typically included in code coverage requirements. |
|
@copilot Attempt to add unit tests to increase code coverage to >= 60%. Be sure to exclude from the code coverage results the main package and any other code not typically included when computing code coverage. |
|
This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation. |
Forwardarr - Pull Request Template
Description
Renames project from Gluetarr to Forwardarr, implements comprehensive unit test coverage, consolidates GitHub Actions workflows to run tests, linting, CodeQL security analysis, and Trivy image vulnerability scanning on all PRs and releases, and updates environment variables to use more generic naming for better compatibility with different torrent clients.
Breaking Changes:
gluetarr_*→forwardarr_*gluetarr→forwardarrgithub.com/eslutz/gluetarr→github.com/eslutz/forwardarrQBIT_USER→TORRENT_CLIENT_USERQBIT_PASS→TORRENT_CLIENT_PASSWORDQBIT_ADDR→TORRENT_CLIENT_URLChanges:
Rename to Forwardarr
cmd/gluetarr/→cmd/forwardarr/forwardarr_info,forwardarr_current_port,forwardarr_sync_total,forwardarr_sync_errors,forwardarr_last_sync_timestamp.env.exampleheader to use Forwardarr namegluetarrbinary from.gitignoreEnvironment Variable Updates
QBIT_USER→TORRENT_CLIENT_USERfor more generic torrent client supportQBIT_PASS→TORRENT_CLIENT_PASSWORDfor more generic torrent client supportQBIT_ADDR→TORRENT_CLIENT_URLfor more generic torrent client support.env.examplewith "Torrent Client Connection Details" sectionUnit Tests (all passing with race detection)
internal/config: Environment variable loading, defaults, edge cases (100% coverage)internal/qbit: HTTP client with mock server, authentication, reauthentication (80% coverage)internal/sync: Port file parsing, validation, whitespace handling (16.1% coverage)internal/server: Health/ready/status endpoints (58.8% coverage)pkg/version: Version string formatting (100% coverage)Consolidated CI/CD Workflows
Code Quality
w.Write,json.Encode,json.Decodein tests and handlersos.Setenvin test setupresp.Body.Closeandwatcher.Closewith defer closuresType of change
Checklist
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.