Simple deployment and management dashbaord of *Arr or STARR media stack with a Go + React dashboard. Deploy on Debian/Ubuntu. Arch in development, use at own risk here. Feel free to make this better in your own repo. I have not tested with anything other than Mullvad Wireguard at this point.
This is a "vibe" coded project born from frustations born of building and reworking a compose.yml file for weeks to get services to run correctly. Top that with needing to watch logs for issues and trying to combine time frames to see what dropped where. So I created a dashboard to glance at running services first that allowed easy access to the service I wanted to access. From there, I wanted to change my VPN config easier. Then I wanted to stop, start, or restart services. Then came the log views. Finally, I asked Claude Opus 4.5 to help build a deployment and make it look pretty.
File structure is based off of TRASH Guides.
Known issues:
"Update All" seems to hang
Disk usage is reporting whole disk and not data directory
ProtonVPN Free WG configs not configuring properly on Gluetun
IPv6 will hang Gluetun, remove it before from WireGuard Address before saving or in your config files. In Mullvad, Generate Key -> Select Country and Server -> Advanced Settings -> Tunnel Traffic: IPv4 Only -> Download Zip. This will make sure it works from the start.
If it crashes:
sudo docker compose -f /opt/executarr/compose/docker-compose.yml restart gluetungit clone https://github.com/darthvibe/Executarr
cd Executarr
chmod +x deploy.sh
sudo bash deploy.shThat's it. The script detects your OS, installs dependencies, creates the restricted user, builds the directory structure, writes credentials, and starts the dashboard.
┌─────────────────────────────────────────────────────┐
│ Host Machine │
│ │
│ ┌─────────────┐ ┌────────────────────────────── ┐ │
│ │ deploy.sh │ │ /opt/executarr/ │ │
│ │ (root) │ │ ├── compose/ │ │
│ └─────────────┘ │ │ ├── docker-compose.yml │ │
│ │ │ │ ├── gluetun.env ← secret│ │
│ │ │ │ └── .env ← secret│ │
│ ▼ │ ├── config/ │ │
│ ┌─────────────┐ │ │ ├── dashboard.json←secret│ │
│ │ executarr │ │ │ ├── ports.json │ │
│ │ (sys user) │ │ │ └── <service>/ │ │
│ │ no shell │ │ └── logs/ │ │
│ │ docker grp │ └────────────────────────────── ┘ │
│ └─────────────┘ │
│ │
│ /mnt/data/ │
│ ├── torrents/{books,movies,music,tv} │
│ ├── usenet/incomplete │
│ ├── usenet/complete/{books,movies,music,tv} │
│ └── media/{books,movies,music,tv} │
└─────────────────────────────────────────────────────┘
The following services use network_mode: service:gluetun — all their traffic exits through the Gluetun tunnel:
- Radarr, Sonarr, Jackett, FlareSolverr, qBittorrent, Dispatcharr
SABnzbd (Usenet) connects directly — Usenet is encrypted SSL by default.
| Service | Default Port | VPN Routed | Web UI |
|---|---|---|---|
| Executarr | 8081 | No | http://host:8081 |
| Gluetun | 8888 | — | HTTP proxy (optional) |
| Radarr | 7878 | ✓ | http://host:7878 |
| Sonarr | 8989 | ✓ | http://host:8989 |
| Jackett | 9117 | ✓ | http://host:9117 |
| FlareSolverr | 8191 | ✓ | http://host:8191 |
| qBittorrent | 8080 (web) | ✓ | http://host:8080 |
| Dispatcharr | 6969 | No | http://host:6969 |
| SABnzbd | 8090 | No | http://host:8090 |
| Plex | 32400 | No | http://host:32400/web |
| Emby | 8096 | No | http://host:8096 |
Port conflicts are detected at dashboard startup and automatically reassigned. A warning banner appears in the UI.
/mnt/data/ ← configurable at deploy time
├── torrents/
│ ├── books/
│ ├── movies/
│ ├── music/
│ └── tv/
├── usenet/
│ ├── incomplete/
│ └── complete/
│ ├── books/
│ ├── movies/
│ ├── music/
│ └── tv/
└── media/
├── books/
├── movies/
├── music/
└── tv/
All services mount /mnt/data as /data inside their containers, keeping paths consistent.
- Created as a system account with
--system - Shell:
/usr/sbin/nologin— cannot log in interactively - No password set — account is locked
- Not in
sudo,wheel, oradmingroups — zero privilege escalation - Only group membership:
docker— required to manage containers - File ownership:
root:executarrwith2775(setgid) permissions
| File | Contains | Permissions |
|---|---|---|
compose/gluetun.env |
Mullvad WireGuard private key | 640 (root:executarr) |
compose/.env |
PUID/PGID/TZ | 640 (root:executarr) |
config/dashboard.json |
bcrypt password hash, session secret | 640 (root:executarr) |
None of these files are ever placed inside docker-compose.yml.
- Passwords stored as bcrypt hashes only (cost factor 12)
- Sessions: 256-bit random tokens, stored in-memory, 8-hour TTL
- Cookies:
HttpOnly,SameSite=Strict - Security headers on all responses:
X-Frame-Options: DENY, CSP,X-Content-Type-Options
- Mounted read-only (
/var/run/docker.sock:ro) in the dashboard container - Lifecycle actions go through
docker composeCLI (respects dependency ordering)
Both are defined in docker-compose.yml but gated behind Compose profiles:
plexprofile → starts Plexembyprofile → starts Embyjellyfinprofile → starts Jellyfin
The dashboard Media Server panel toggles between them. Only one can be active at a time.
executarr/
├── deploy.sh ← One-click bootstrap (run as root)
├── .gitignore ← Protects secrets from git
├── README.md
│
├── dashboard/
│ ├── Dockerfile ← Multi-stage: Node → Go → slim runtime
│ ├── go.mod
│ ├── cmd/server/
│ │ ├── main.go ← Entry point, graceful shutdown
│ │ ├── config.go ← Config loader, structured logger
│ │ ├── router.go ← HTTP route wiring
│ │ └── handlers.go ← Request handlers + middleware
│ ├── internal/
│ │ ├── auth/auth.go ← bcrypt auth, session management
│ │ ├── compose/manager.go ← docker compose CLI wrapper
│ │ ├── docker/client.go ← Docker SDK: list services/status
│ │ ├── ports/ports.go ← Port collision detection
│ │ └── system/monitor.go ← Disk usage stats
│ └── web/
│ ├── embed.go ← Go embed directive for React dist/
│ └── src/
│ ├── src/
│ │ ├── App.jsx ← Router + auth context
│ │ ├── api.js ← API client (all fetch calls)
│ │ ├── pages/
│ │ │ ├── LoginPage.jsx
│ │ │ └── DashboardPage.jsx
│ │ └── components/
│ │ ├── Header.jsx
│ │ ├── ServiceGrid.jsx ← Service cards + start/stop/restart
│ │ ├── DiskPanel.jsx ← Disk usage visualisation
│ │ ├── MediaServerPanel.jsx ← Plex/Emby selector
│ │ └── PortConflictBanner.jsx
│ └── index.html
│
├── compose/
│ ├── docker-compose.yml ← All services (generated by deploy.sh)
│ ├── gluetun.env ← VPN credentials (SECRET — never commit)
│ └── .env ← PUID/PGID/TZ (SECRET — never commit)
│
├── config/
│ ├── dashboard.json ← Admin creds + session secret (SECRET)
│ └── ports.json ← Port registry (auto-updated on conflict)
│
└── scripts/
├── create-user.sh ← Restricted user creation
└── hashpw.go ← bcrypt hash utility (deploy-time only)