Skip to content

JDownloader2 over Mullvad (Saltbox + Gluetun on saltierpoop)

Standalone JDownloader2 with VPN egress only — Plex, Sonarr, and the rest of saltierpoop stay on normal networking.

Prerequisites

  • Mullvad account with a WireGuard config generated at mullvad.net/account
  • Use PrivateKey and IPv4 Address (with /32) from any downloaded .jsonnot the “Wireguard key” shown on the devices page
  • UFW disabled on saltierpoop (Saltbox requirement). Homelab sets common_manage_firewall: false on this host so ansible-pull does not re-enable it. Perimeter is UDM ZBF + Traefik.

Where config lives (Saltbox inventory, not settings.yml)

Saltbox role-refactor puts per-app overrides in the inventory system:

File Purpose
/srv/git/saltbox/inventories/host_vars/localhost.yml Gluetun, JDownloader2 network_mode, Traefik themes, etc.
/srv/git/saltbox/settings.yml Global Saltbox settings (downloads path, rclone, shell) — homelab SOPS deploys this
/srv/git/saltbox/accounts.yml App passwords — homelab SOPS deploys this

Edit inventory on the host:

ssh someone@192.168.6.243
sb edit inventory

Docs: Saltbox inventory, Gluetun role (role-refactor mirror: role-refactor Gluetun).

1. Add Gluetun + JD2 routing to inventory

Append to localhost.yml (Mullvad example from Saltbox Gluetun docs):

# Mullvad WireGuard — keys from mullvad.net wireguard config JSON
gluetun_vpn_service_provider: "mullvad"
gluetun_vpn_type: "wireguard"
gluetun_wireguard_private_key: "YOUR_PRIVATE_KEY"
gluetun_wireguard_addresses: "10.x.x.x/32"
gluetun_server_cities: "los angeles"   # optional city slug

# Route JDownloader2 egress through Gluetun only (not Plex/Sonarr/etc.)
jdownloader2_docker_network_mode: "container:gluetun"

# Skip Cloudflare DNS for JD2 when using MyJDownloader only, or when global
# dns.ipv6 is enabled but public IPv6 detection fails (common behind Gluetun).
jdownloader2_role_dns_enabled: false

If Gluetun already carries another app (e.g. qBittorrent) and ports clash, use a second instance (gluetun2 + container:gluetun2) per Saltbox Gluetun docs.

Validate before install:

sb validate-config

Secrets: Mullvad keys belong in localhost.yml on the host today (not in homelab settings.sops.yaml). Restrict file permissions; consider backing up inventories/host_vars/ with your Saltbox backup workflow.

2. Deploy

sb install gluetun
sb install sandbox-jdownloader2

3. Verify

# Mullvad exit IP from Gluetun namespace
docker exec gluetun curl -s ifconfig.me
# or: docker exec gluetun wget -qO- https://am.i.mullvad.net/json

# Kill switch: stop Gluetun — JD2 must not reach internet
docker stop gluetun
docker exec jdownloader2 wget -qO- --timeout=5 https://ifconfig.me || echo "blocked OK"
docker start gluetun

4. Use from your PC (remote control, not local downloads)

JDownloader2 runs on saltierpoop in the jdownloader2 container. Downloads happen on the server and egress through Mullvad via Gluetun. Your PC does not tunnel through Gluetun — it only talks to the remote JD2 instance.

  • Install MyJDownloader browser extension or desktop app on your PC
  • Sign in with a MyJDownloader account
  • Link the saltierpoop instance when prompted (container registers on first start)
  • Add download links from your browser; JD2 on saltierpoop fetches them over VPN
  • Optional web UI: https://jdownloader2.<your-saltbox-domain> via Traefik (requires DNS — we skipped Cloudflare records; use MyJDownloader instead)

Troubleshooting

sb install fails: UFW is active

Saltbox requires UFW inactive (Docker manages iptables). Homelab Ansible was re-enabling UFW on saltierpoop until common_manage_firewall: false is merged in infra/ansible/inventory/host_vars/saltierpoop.yml. If sb install fails on the sanity check:

sudo ufw disable
sudo ufw status   # must show inactive

After merging that host var, ansible-pull will not turn UFW back on.

sandbox-jdownloader2 fails on Cloudflare IPv6 validation

With dns.ipv6: yes in adv_settings.yml, Saltbox tries to create AAAA records using detected public IPv6. Containers on container:gluetun do not have that address; validation fails when detection is also broken. Set jdownloader2_role_dns_enabled: false in inventory (see section 1) if you only need MyJDownloader, or set jdownloader2_role_dns_ipv6 to your host's real public IPv6 if you want the Traefik hostname.

Notes

  • Legacy qbittorrentvpn (binhex / PIA) is separate from Saltbox Gluetun; it was in a restart loop at 2026-06 audit — ignore or remove independently.
  • Only containers with *_docker_network_mode: "container:gluetun" use the VPN.
  • Plex, Sonarr, Traefik, etc. are unchanged unless you add the same network_mode for those roles in inventory (do not do that for this detour).