Skip to content

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:

sudo chown -R 1000:1000 /path/to/your/media

If your path is owned by a different UID/GID, configure the storage charm to match:

# Check current ownership
ls -ln /path/to/your/media
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:

ip -4 addr show | grep -oP 'inet \K[\d./]+'

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

tofu init && TF_VAR_wireguard_private_key="your-key" tofu apply -auto-approve

Or without VPN (when enable_vpn = false):

tofu init && tofu apply -auto-approve

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

tofu init && TF_VAR_wireguard_private_key="your-key" tofu apply -auto-approve

Or without VPN (when enable_vpn = false):

tofu init && tofu apply -auto-approve

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:

module "charmarr" {
  # ... existing config ...

  enable_istio = true
  enable_mesh  = true
}
TF_VAR_wireguard_private_key="your-key" tofu apply

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:

tofu destroy