Community-Script LXCs
Configuration for applications installed straight into an LXC from the
community-scripts.org helpers, rather than run as Docker
Compose stacks. 64 of the 66 LXCs carry the community-script tag — this is how most
of the fleet is installed. What lives here is the subset whose configuration is worth
version-controlling.
A helper script installs the app natively — a systemd unit, files under /opt/<app>, no
Docker — so the assumptions the rest of this repo runs on do not hold:
| Compose stacks | Community-script LXCs | |
|---|---|---|
| Deployed by | docker compose up -d under bws run | A one-shot installer, run once by hand |
| Config lives | In the compose file, in git | Inside the guest, at whatever path the app wants |
| Routed via | traefik-web network + provider | Traefik labels in the Proxmox guest description |
| Secrets | Injected by bws run at start | Not injected — whatever the app stores on disk |
Nothing re-applies these files: a guest rebuilt from its script comes back with stock defaults. Keeping the file here is what makes the customisation recoverable.
Layout
Section titled “Layout”One directory per application, mirroring the path inside the guest:
community-scripts-lxc/└── dashy/ ├── README.md # the guest, the service, how to update it └── user-data/ └── conf.yml # ← /opt/dashy/user-data/conf.yml in CT 270| Application | Guest | Notes |
|---|---|---|
| Dashy | CT 270 | The homelab homepage — 89 tiles across 11 sections |
Installed applications
Section titled “Installed applications”Every guest running an application natively — tagged community-script without also
carrying docker or docker-template, since those are Docker hosts whose configuration
belongs in docker-compose-apps/. That is 40 of the
66 tagged guests; only a couple have configuration tracked here so far, and the rest are
listed so the gap is visible.
Routed reports whether the guest’s description carries Traefik labels. no is often
fine — plenty of these are reached by IP or consumed by another service. One exception
is worth knowing:
joplin-serveris deliberately unrouted. Joplin validates theHostheader against its ownAPP_BASE_URLand 404s anything else; pointing that at atoolsera.lanname works but then breakshttp://192.168.1.20:22300. Reach it by IP.
| Guest | Application | Address | Routed | Tags |
|---|---|---|---|---|
| CT 258 | autobrr | 192.168.1.76 | yes | arr |
| CT 208 | bookorbit | 192.168.1.197 | yes | books, library, reading |
| CT 221 | cloudflare-tunnel | 192.168.1.105 | no | os |
| CT 261 | dagu | 192.168.1.44 | yes | os |
| CT 270 | dashy | 192.168.1.25 | yes | dashboard |
| CT 232 | flaresolverr-byparr | 192.168.1.49 | no | byparr, flaresolverr |
| CT 231 | freshrss | 192.168.1.19 | yes | rss |
| CT 212 | garage | 192.168.1.15 | no | object-storage |
| CT 265 | gatus | 192.168.1.64 | yes | monitoring |
| CT 252 | glance | 192.168.1.158 | yes | dashboard |
| CT 222 | jackett | 192.168.1.22 | yes | jackett |
| CT 226 | jellyfin | 192.168.1.62 | yes | jellyfin, media |
| CT 215 | joplin-server | 192.168.1.20 | no | notes |
| CT 236 | komga | 192.168.1.166 | yes | comic, ebook, media |
| CT 246 | linkwarden | 192.168.1.183 | yes | bookmark |
| CT 203 | metube | 192.168.1.21 | yes | media, youtube |
| CT 227 | n8n | 192.168.1.46 | yes | automation |
| CT 264 | nx-tempo-prod | 192.168.1.104 | no | os |
| CT 253 | opengist | 192.168.1.189 | yes | development |
| CT 132 | pihole | 192.168.1.184 | yes | adblock |
| CT 204 | plex-us-24 | 192.168.1.13 | yes | media |
| VM 224 | prod-vm-us | 192.168.1.148 | no | — |
| CT 267 | profilarr | 192.168.1.113 | yes | arr, config, radarr, sonarr |
| CT 230 | prowlarr | 192.168.1.53 | yes | arr |
| CT 244 | pulse | 192.168.1.50 | yes | monitoring, proxmox |
| CT 210 | pve-scripts-local | 192.168.1.110 | yes | pve-scripts-local |
| CT 209 | radarr | 192.168.1.97 | yes | arr |
| CT 254 | radarr-anime | 192.168.1.36 | no | arr |
| CT 213 | readeck | 192.168.1.144 | yes | bookmark |
| CT 266 | seerr | 192.168.1.139 | yes | media |
| CT 241 | semaphore | 192.168.1.137 | yes | dev_ops, semaphore |
| CT 207 | sonarr | 192.168.1.120 | yes | arr |
| CT 126 | sonarr-anime | 192.168.1.177 | yes | arr, sonar-anime |
| CT 202 | suggestarr | 192.168.1.24 | yes | arr, media |
| CT 247 | tautulli | 192.168.1.52 | yes | media |
| CT 271 | tracearr | 192.168.1.165 | yes | media |
| CT 242 | unbound | 192.168.1.95 | no | dns, unbound |
| CT 240 | upsnap | 192.168.1.196 | yes | network, upsnap |
| CT 248 | uptimekuma | 192.168.1.187 | yes | analytics, monitoring |
| CT 225 | whois | 192.168.1.98 | yes | arr |
Refreshing the list
Section titled “Refreshing the list”Run this on pve-dell (192.168.1.131) and replace the generated block with its output.
It asks each guest for its address — lxc-info for LXCs, the QEMU guest agent for VMs —
and only falls back to the address the installer recorded as a tag, which VMs never have.
_stopped_ means exactly that.
cd /etc/pvefor f in lxc/*.conf qemu-server/*.conf; do [ -e "$f" ] || continue id=$(basename "$f" .conf) case "$f" in lxc/*) kind="CT"; nk="hostname";; *) kind="VM"; nk="name";; esac name=$(grep -m1 "^$nk:" "$f" | cut -d" " -f2) tags=$(grep -m1 "^tags:" "$f" | cut -d" " -f2-) echo "$tags" | tr ";" "\n" | grep -qx "community-script" || continue echo "$tags" | tr ";" "\n" | grep -qxE "docker|docker-template" && continue if [ "$kind" = CT ]; then state=$(pct status "$id" 2>/dev/null | awk "{print \$2}") ip=$(lxc-info -n "$id" -iH 2>/dev/null | grep -m1 -E "^192\.168\.") else state=$(qm status "$id" 2>/dev/null | awk "{print \$2}") ip=$(qm agent "$id" network-get-interfaces 2>/dev/null \ | grep -oE "192\.168\.[0-9]+\.[0-9]+" | head -1) fi [ -n "$ip" ] || ip=$(echo "$tags" | tr ";" "\n" | grep -m1 -E "^192\.168\.") [ -n "$ip" ] || { [ "$state" = running ] && ip="—" || ip="_stopped_"; } # paste -d cycles its delimiter list, so join on a bare comma then space it out tagl=$(echo "$tags" | tr ";" "\n" \ | grep -vE "^[0-9]+\.|^community-script$|^tailscale$" \ | paste -sd , - | sed "s/,/, /g") grep -q "traefik.enable" "$f" && r=yes || r=no echo "| $kind $id | $name | $ip | $r | ${tagl:-—} |"done | sort -t"|" -k3stirling-pdf (CT 113) and sftpgo (CT 214) carry the project’s former
proxmox-helper-scripts tag and fall outside this filter. Retag them if they should
appear.
Nothing here is applied automatically
Section titled “Nothing here is applied automatically”This is the canonical copy; the guest holds a copy, and they drift unless someone pushes. Most LXCs refuse SSH, so pushing goes through the node:
pct push <vmid> <local-file> <path-inside-guest>pct exec <vmid> -- systemctl restart <service>See Host access notes for why direct SSH mostly fails.
Several of these apps can also edit their own configuration from their web UI, writing to the same file. Treat UI edits as a scratchpad and fold anything worth keeping back into the copy here.
Routing a community-script guest
Section titled “Routing a community-script guest”The traefik-proxmox-provider plugin reads Traefik labels out of the guest’s
description and points a service at its IP:port. Compose labels and container names
do nothing here.
A guest with no labels gets no router — and because the toolsera.lan zone wildcards to
Traefik, its hostname still resolves and answers 404, which reads as a broken app rather
than a missing route. The Dashy README has the label block and
the pct set command that applies it without destroying the installer’s description. The
plugin polls every 30 seconds; no Traefik restart is involved.
Adding an application
Section titled “Adding an application”- Install it from community-scripts.org onto the node.
- If it needs routing, append a Traefik label block to the guest’s description and confirm the route answers.
- Create
community-scripts-lxc/<app>/and copy in only the files worth tracking. - Write an
<app>/README.mdrecording the guest ID, systemd service, config path inside the guest, and port. - Add both to
SOURCESindocs/scripts/sync-content.mjsand to the sidebar indocs/astro.config.mjs— a listed source that goes missing fails the docs build rather than dropping the page silently.
Never copy in databases, caches, or anything holding a secret. Files here are plain git
objects with no bws run around them; the gitleaks hook is the backstop, not the policy.