Quick Deploy¶
Charmarr provides pre-configured media stack bundles as HCL modules, deployable with OpenTofu.
Note
Cluster already has Istiod? Use Manual Deploy instead. If you don't know, your cluster doesn't have one.
Bundles¶
| Bundle | Radarr |
Sonarr |
|---|---|---|
| charmarr | 1 (HD) | 1 (HD) |
| charmarr-plus | 3 (HD, UHD, Anime) | 3 (HD, UHD, Anime) |
Both bundles include:
Plex |
Overseerr |
Prowlarr |
FlareSolverr |
qBittorrent |
SABnzbd |
Gluetun |
Recyclarr |
Note
charmarr-plus has slightly higher CPU requirements due to additional Radarr/Sonarr instances. During initial deployment, expect higher CPU and RAM usage. It flatlines once settled.
Charmarr¶
Single Radarr and Sonarr with HD TRaSH profiles pre-configured.
1. Create a main.tf file¶
See the OpenTofu docs if you're curious about how it works.
variable "wireguard_private_key" {
type = string
sensitive = true
default = ""
}
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
model = "charmarr"
# Storage
storage_backend = "hostpath"
hostpath = "/mnt/storage/charmarr"
# VPN
enable_vpn = true
wireguard_private_key = var.wireguard_private_key
vpn_provider = "protonvpn"
cluster_cidrs = "10.1.0.0/16,10.152.183.0/24,192.168.1.0/24"
}
2. Configure Variables¶
Storage¶
Shared storage enables hardlinks between download clients and media managers. See Storage for why this matters.
Hostpath (recommended for single-node) - Storage on the same node as the cluster:
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
storage_backend = "hostpath"
hostpath = "/path/to/your/media"
}
Warning
Avoid NFS on the same node. Loopback mounts can cause deadlocks.
Native NFS (recommended for multi-node) - External NFS server:
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
storage_backend = "native-nfs"
nfs_server = "192.168.1.100"
nfs_path = "/export/charmarr"
}
StorageClass - Custom CSI driver (Rook-Ceph, etc.):
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
storage_backend = "storage-class"
storage_class = "rook-ceph-block"
storage_size = "1Ti"
}
Warning
StorageClass is experimental. Requires careful configuration of storage_size, access_mode, and cleanup_on_remove. Trivial for hostpath and native-nfs, not so for CSI drivers.
File Ownership (Hostpath & NFS)
For hostpath and NFS backends, the storage path must be owned by UID/GID 1000:1000 by default:
If your path is owned by a different UID/GID, configure the storage charm to match:
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
storage = {
config = {
puid = "1001"
pgid = "1001"
}
}
}
For NFS, ensure the NFS export allows write access for the configured PUID/PGID.
For StorageClass with CSI drivers, this is driver-dependent. Block storage drivers typically handle ownership automatically, while shared filesystem drivers (CephFS, NFS-based CSI) follow the same rules as NFS.
VPN¶
By default, enable_vpn = true deploys Gluetun and integrates it with qBittorrent, SABnzbd, and Prowlarr. All traffic from these apps routes through a VPN tunnel and their external IP is anonymized. See Networking for how this works.
Provider
WireGuard is the default and recommended protocol.
| Provider | Value |
|---|---|
| ProtonVPN | protonvpn (recommended) |
| NordVPN | nordvpn |
| Mullvad | mullvad |
| Private Internet Access | pia |
| Surfshark | surfshark |
| IVPN | ivpn |
| Windscribe | windscribe |
| Custom WireGuard | custom (experimental) |
For most commercial VPNs, only the wireguard_private_key is needed. Custom WireGuard setups require additional variables: wireguard_addresses, vpn_endpoint_ip, vpn_endpoint_port, and wireguard_public_key.
OpenVPN Support
OpenVPN is not officially supported. If your VPN provider only supports OpenVPN, or you need to pass custom environment variables to Gluetun, use the custom-overrides config to enter override mode. In override mode, WireGuard validation is bypassed and the provided JSON is merged on top of the charm's built-in environment.
Unlike wireguard_private_key which is stored as a Juju secret and encrypted at rest, custom-overrides is plain text charm config. Credentials passed here are not encrypted.
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
vpn_provider = "protonvpn"
gluetun = {
config = {
"custom-overrides" = jsonencode({
VPN_TYPE = "openvpn"
OPENVPN_USER = "your-username"
OPENVPN_PASSWORD = "your-password"
})
}
}
}
Override mode relaxes config validation. Misconfiguration may result in silent failures that require inspecting the Gluetun container logs to diagnose. See the Gluetun wiki for available environment variables.
Cluster CIDRs
Comma-separated list of CIDRs to exclude from VPN routing (required when VPN is enabled). Include:
- Pod CIDR - K8s pod network
- Service CIDR - K8s service network
- LAN CIDR - Your local network
MicroK8s defaults:
| CIDR | Default |
|---|---|
| Pod | 10.1.0.0/16 |
| Service | 10.152.183.0/24 |
Find CIDRs with kubectl:
# Pod CIDR (Calico CNI)
kubectl get ippools -o jsonpath='{.items[*].spec.cidr}'
# Service CIDR (check kubernetes service IP, typically x.x.x.0/24)
kubectl get svc kubernetes -o jsonpath='{.spec.clusterIP}'
Find your LAN CIDR:
Look for your network interface IP (e.g., 192.168.1.100/24 means your LAN CIDR is 192.168.1.0/24).
Disabling VPN
If you use a different tunneling solution (e.g., Tailscale exit node, network-level VPN), you can disable the built-in VPN:
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
enable_vpn = false
qbittorrent = {
config = {
"unsafe-mode" = "true"
}
}
sabnzbd = {
config = {
"unsafe-mode" = "true"
}
}
}
When enable_vpn = false, Gluetun is not deployed and download clients are not integrated with a VPN gateway. You must also enable unsafe-mode on qBittorrent and SABnzbd for them to start without VPN protection.
Warning
Without VPN integration, your real IP is exposed to torrent trackers and usenet providers. Only disable VPN if you have an alternative tunneling solution in place.
Plex Hardware Transcoding¶
If your hardware supports it:
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
plex = {
hardware_transcoding = true
}
}
Istio¶
Enable Istio for ingress and mesh security (see Compatibility Checklist first):
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
enable_istio = true
enable_mesh = true
# Only needed if not using MicroK8s
istio = {
config = {
platform = "minikube" # see table below
}
}
}
| Distribution | platform value |
|---|---|
| MicroK8s | microk8s (default) |
| Minikube | minikube |
| Standard K8s (GKE, EKS, AKS, kubeadm) | "" |
| K3s | k3s |
| k3d | k3d |
Path Prefixes
Path prefixes default to the Juju app name (e.g., deploying as radarr gives path /radarr). With a typical deployment:
| App Name | Default Path |
|---|---|
| radarr | /radarr |
| sonarr | /sonarr |
| prowlarr | /prowlarr |
| qbittorrent | /qbittorrent |
| sabnzbd | /sabnzbd |
With Istio ingress, these paths are automatically configured. If you're using your own ingress controller, configure it to route these paths to the respective services.
To use different paths, or set "/" to serve at root (no path prefix):
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
radarr = {
ingress_path = "/movies"
}
qbittorrent = {
ingress_path = "/" # serve at root
}
}
Ingress Port
The ingress listener port defaults to 80. To use a different port:
module "charmarr" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr?ref=track/1"
# ... your other config ...
radarr = {
ingress_port = 8080
}
}
3. Deploy¶
Or without VPN (when enable_vpn = false):
See the charmarr module for all available variables.
Charmarr Plus¶
Three Radarrs (HD, UHD, Anime) and three Sonarrs (HD, UHD, Anime) with appropriate TRaSH profiles.
1. Create a main.tf file¶
variable "wireguard_private_key" {
type = string
sensitive = true
default = ""
}
module "charmarr_plus" {
source = "git::https://github.com/charmarr/charmarr//terraform/charmarr-plus?ref=track/1"
model = "charmarr"
# Storage
storage_backend = "hostpath"
hostpath = "/mnt/storage/charmarr"
# VPN
enable_vpn = true
wireguard_private_key = var.wireguard_private_key
vpn_provider = "protonvpn"
cluster_cidrs = "10.1.0.0/16,10.152.183.0/24,192.168.1.0/24"
}
2. Configure Variables¶
Same as charmarr. See Storage, VPN, and Istio above.
3. Deploy¶
Or without VPN (when enable_vpn = false):
See the charmarr-plus module for all available variables.
Tip
Want a truly custom Charmarr with different Radarrs, multiple download clients, etc.? Use the charmarr and charmarr-plus modules as templates to create your own charmarr bundle.
Tip
After deployment, the Manual Deploy page can be used as a reference to customize your stack with the Juju CLI. It's fun.
Making Changes¶
Edit your main.tf and reapply. OpenTofu calculates the diff and applies only what changed.
For example, to enable Istio ingress later:
VPN key required on every apply
When VPN is enabled (enable_vpn = true), you must provide TF_VAR_wireguard_private_key on every tofu apply, not just the initial deployment. Running without it resets the secret to an empty value, causing gluetun to block.
See the OpenTofu CLI docs for more.
Removing Charmarr¶
To tear down the deployment:
Radarr
Sonarr
Plex
Overseerr
Prowlarr
FlareSolverr
qBittorrent
SABnzbd
Gluetun
Recyclarr