Repository navigation
Expand file tree
/
Copy patharrstack.sh
More file actions
executable file
·424 lines (383 loc) · 19.9 KB
/
Copy patharrstack.sh
File metadata and controls
executable file
·424 lines (383 loc) · 19.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
#!/usr/bin/env bash
# arrstack.sh — run on a Proxmox VE host. Creates ONE Debian 12 LXC, installs
# Docker inside it, and brings up the whole *arr media stack with Compose.
#
# bash -c "$(curl -fsSL https://raw.githubusercontent.com/abarbarich/arrstack/main/arrstack.sh)"
#
# Everything is configurable by environment variable; with a TTY you also get an
# interactive whiptail menu. See README.md for the full list.
set -Eeuo pipefail
# --------------------------------------------------------------------------- #
# Where to pull the repo (compose files + scripts) from (override for forks).
# --------------------------------------------------------------------------- #
REPO="${ARRSTACK_REPO:-abarbarich/arrstack}"
BRANCH="${ARRSTACK_BRANCH:-main}"
GIT_URL="https://github.com/${REPO}"
RAW_BASE="https://raw.githubusercontent.com/${REPO}/${BRANCH}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo /tmp)"
# Source helpers from disk (local clone) or from the repo (curl|bash).
if [ -f "${SCRIPT_DIR}/lib/helpers.sh" ]; then
. "${SCRIPT_DIR}/lib/helpers.sh"
else
# shellcheck disable=SC1090
source <(curl -fsSL "${RAW_BASE}/lib/helpers.sh") \
|| { echo "Failed to load helpers from ${RAW_BASE}/lib/helpers.sh" >&2; exit 1; }
fi
trap 'rc=$?; [ "$rc" -ne 0 ] && msg_err "arrstack.sh aborted (exit ${rc}) at line ${LINENO}."' ERR
# --------------------------------------------------------------------------- #
# Defaults (every one overridable via environment)
# --------------------------------------------------------------------------- #
CTID="${CTID:-}" # auto-assigned if empty
CT_HOSTNAME="${CT_HOSTNAME:-arrstack}"
CT_DISK="${CT_DISK:-20}" # GB rootfs (holds Docker images)
# Remember whether the caller pinned these, so the Jellyfin overlay can raise
# the DEFAULTS without ever overriding an explicit choice (see jellyfin_sizing).
CT_CORES_PINNED=0; [ -n "${CT_CORES:-}" ] && CT_CORES_PINNED=1
CT_RAM_PINNED=0; [ -n "${CT_RAM:-}" ] && CT_RAM_PINNED=1
CT_CORES="${CT_CORES:-4}"
CT_RAM="${CT_RAM:-4096}" # MB (2048 is enough; 4096 comfy)
# Bumped defaults when Jellyfin runs in-stack: transcoding wants cores, and the
# /transcode tmpfs draws from the CT's RAM.
CT_CORES_JELLYFIN="${CT_CORES_JELLYFIN:-8}"
CT_RAM_JELLYFIN="${CT_RAM_JELLYFIN:-8192}"
CT_SWAP="${CT_SWAP:-2048}" # MB
CT_BRIDGE="${CT_BRIDGE:-vmbr0}"
CT_STORAGE="${CT_STORAGE:-local-lvm}" # rootfs storage
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" # where templates live
CT_NET="${CT_NET:-dhcp}" # "dhcp" or CIDR e.g. 192.168.1.50/24
CT_GW="${CT_GW:-}" # gateway for static IP
CT_UNPRIVILEGED="${CT_UNPRIVILEGED:-1}"
CT_FEATURES="${CT_FEATURES:-nesting=1,keyctl=1}" # nesting = required for Docker
CT_PASSWORD="${CT_PASSWORD:-}" # random if empty
# Stack / app settings
PUID="${PUID:-1000}"
PGID="${PGID:-1000}"
TZ_DEFAULT="$(cat /etc/timezone 2>/dev/null || echo Etc/UTC)"
STACK_TZ="${STACK_TZ:-$TZ_DEFAULT}"
DATA_MP="${DATA_MP:-/data}" # data path inside the CT
CONFIG_ROOT="${CONFIG_ROOT:-/opt/arrstack/config}" # app configs inside the CT
STACK_DIR="/opt/arrstack"
# Storage: how to provide ${DATA_MP} inside the CT
STORAGE_MODE="${STORAGE_MODE:-skip}" # disk | existing | skip
STORAGE_DISK="${STORAGE_DISK:-}" # /dev/sdX (STORAGE_MODE=disk)
STORAGE_EXISTING="${STORAGE_EXISTING:-}" # host path (STORAGE_MODE=existing)
MOUNT_BASE="${MOUNT_BASE:-/mnt/media}"
# VPN provisioning (off by default — file + .env block are always shipped)
VPN_ENABLED="${VPN_ENABLED:-0}"
VPN_SERVICE_PROVIDER="${VPN_SERVICE_PROVIDER:-mullvad}"
VPN_TYPE="${VPN_TYPE:-wireguard}"
WIREGUARD_PRIVATE_KEY="${WIREGUARD_PRIVATE_KEY:-}"
WIREGUARD_ADDRESSES="${WIREGUARD_ADDRESSES:-10.64.0.2/32}"
VPN_SERVER_COUNTRIES="${VPN_SERVER_COUNTRIES:-}"
LAN_SUBNET="${LAN_SUBNET:-192.168.0.0/16}"
# Jellyfin overlay (off by default — the file is always shipped)
JELLYFIN_ENABLED="${JELLYFIN_ENABLED:-0}"
JELLYFIN_PUBLISHED_URL="${JELLYFIN_PUBLISHED_URL:-}"
JELLYFIN_TRANSCODE_TMPFS_SIZE="${JELLYFIN_TRANSCODE_TMPFS_SIZE:-2g}"
ASSUME_YES="${ASSUME_YES:-0}"
usage() {
cat <<EOF
ArrStack — single-container Docker *arr installer for Proxmox VE
Usage: arrstack.sh [options]
-y, --yes non-interactive; use defaults/env (no whiptail)
--disk /dev/sdX format this disk ext4 and bind-mount it as the data dir
--existing PATH bind an existing host dir as the data dir (no format)
--vpn provision + enable the gluetun VPN overlay
--jellyfin enable the Jellyfin overlay (media server in-stack);
also raises the CT defaults to 8 cores / 8GB RAM
-h, --help show this help
Key env vars: CTID CT_HOSTNAME CT_DISK CT_CORES CT_RAM CT_SWAP CT_BRIDGE
CT_STORAGE TEMPLATE_STORAGE CT_NET CT_GW CT_PASSWORD
PUID PGID STACK_TZ DATA_MP MOUNT_BASE
STORAGE_MODE STORAGE_DISK STORAGE_EXISTING
VPN_ENABLED VPN_SERVICE_PROVIDER VPN_TYPE WIREGUARD_PRIVATE_KEY
JELLYFIN_ENABLED JELLYFIN_PUBLISHED_URL
CT_CORES_JELLYFIN CT_RAM_JELLYFIN (defaults used when --jellyfin)
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
-y|--yes) ASSUME_YES=1 ;;
--disk) STORAGE_MODE=disk; STORAGE_DISK="$2"; shift ;;
--existing) STORAGE_MODE=existing; STORAGE_EXISTING="$2"; shift ;;
--vpn) VPN_ENABLED=1 ;;
--jellyfin) JELLYFIN_ENABLED=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown option: $1 (try --help)" ;;
esac
shift
done
# --------------------------------------------------------------------------- #
# Preflight
# --------------------------------------------------------------------------- #
need_root
have pct && have pveam && have pvesh || die "This must run on a Proxmox VE host (pct/pveam not found)."
# --------------------------------------------------------------------------- #
# Interactive configuration (whiptail) — only with a real TTY
# --------------------------------------------------------------------------- #
interactive_config() {
[ "$ASSUME_YES" = "1" ] && return 0
[ -t 0 ] || return 0
have whiptail || return 0
if whiptail --title "ArrStack" --yesno \
"Create a Debian 12 LXC and install the Docker *arr stack?\n\nDefaults:\n hostname ${CT_HOSTNAME}, ${CT_CORES} cores, ${CT_RAM}MB RAM, ${CT_DISK}GB rootfs\n bridge ${CT_BRIDGE}, storage ${CT_STORAGE}, network ${CT_NET}\n timezone ${STACK_TZ}\n\nCustomise these settings?" \
18 72 --defaultno; then
CT_HOSTNAME="$(whiptail --inputbox "Hostname" 10 60 "$CT_HOSTNAME" 3>&1 1>&2 2>&3)" || exit 1
CT_CORES="$(whiptail --inputbox "CPU cores" 10 60 "$CT_CORES" 3>&1 1>&2 2>&3)" || exit 1
CT_RAM="$(whiptail --inputbox "RAM (MB)" 10 60 "$CT_RAM" 3>&1 1>&2 2>&3)" || exit 1
# Answered by hand — don't let the Jellyfin bump second-guess them.
CT_CORES_PINNED=1; CT_RAM_PINNED=1
CT_DISK="$(whiptail --inputbox "Root disk (GB)" 10 60 "$CT_DISK" 3>&1 1>&2 2>&3)" || exit 1
CT_BRIDGE="$(whiptail --inputbox "Network bridge" 10 60 "$CT_BRIDGE" 3>&1 1>&2 2>&3)" || exit 1
CT_STORAGE="$(whiptail --inputbox "rootfs storage" 10 60 "$CT_STORAGE" 3>&1 1>&2 2>&3)" || exit 1
STACK_TZ="$(whiptail --inputbox "Timezone" 10 60 "$STACK_TZ" 3>&1 1>&2 2>&3)" || exit 1
if whiptail --title "Network" --yesno "Use DHCP?\n\nNo = static IP" 10 60; then
CT_NET="dhcp"
else
CT_NET="$(whiptail --inputbox "Static IP in CIDR (e.g. 192.168.1.50/24)" 10 64 "192.168.1.50/24" 3>&1 1>&2 2>&3)" || exit 1
CT_GW="$(whiptail --inputbox "Gateway (e.g. 192.168.1.1)" 10 64 "${CT_GW:-192.168.1.1}" 3>&1 1>&2 2>&3)" || exit 1
fi
fi
# Storage
local schoice
schoice="$(whiptail --title "ArrStack — media storage" --menu \
"Where should the media live? (${DATA_MP} inside the CT)" 16 74 3 \
skip "Configure storage myself later (bind mount by hand)" \
disk "Format a blank disk as ext4 and bind-mount it" \
existing "Bind-mount an existing host directory" \
3>&1 1>&2 2>&3)" || exit 1
STORAGE_MODE="$schoice"
case "$STORAGE_MODE" in
disk) STORAGE_DISK="$(whiptail --inputbox "Disk to FORMAT (ERASES it), e.g. /dev/sdb\nLeave blank to pick interactively during setup." 11 64 "$STORAGE_DISK" 3>&1 1>&2 2>&3)" || exit 1 ;;
existing) STORAGE_EXISTING="$(whiptail --inputbox "Existing host directory to bind (e.g. /tank/media)" 10 64 "$STORAGE_EXISTING" 3>&1 1>&2 2>&3)" || exit 1
[ -n "$STORAGE_EXISTING" ] || die "No directory given." ;;
esac
# VPN
if whiptail --title "ArrStack — VPN" --yesno \
"Route qBittorrent through a VPN (gluetun)?\n\nYou'll need your provider + WireGuard key. You can also skip now and enable it later by editing .env." \
13 70 --defaultno; then
VPN_ENABLED=1
VPN_SERVICE_PROVIDER="$(whiptail --inputbox "VPN provider (mullvad, protonvpn, pia, nordvpn, …)" 10 64 "$VPN_SERVICE_PROVIDER" 3>&1 1>&2 2>&3)" || exit 1
WIREGUARD_PRIVATE_KEY="$(whiptail --inputbox "WireGuard private key (leave blank to fill in .env later)" 10 68 "" 3>&1 1>&2 2>&3)" || exit 1
WIREGUARD_ADDRESSES="$(whiptail --inputbox "WireGuard address (from your provider)" 10 64 "$WIREGUARD_ADDRESSES" 3>&1 1>&2 2>&3)" || exit 1
fi
# Jellyfin
local jf_note="\n\nThe CT will get ${CT_CORES_JELLYFIN} cores / ${CT_RAM_JELLYFIN}MB RAM instead of the usual ${CT_CORES}/${CT_RAM}."
[ "$CT_CORES_PINNED" = "1" ] && jf_note=""
if whiptail --title "ArrStack — Jellyfin" --yesno \
"Run Jellyfin inside this stack?\n\nIt reads the library straight off ${DATA_MP} — no network share to drop out. Say No if you already have a media server elsewhere.\n\nHardware transcoding needs the GPU passed into this CT; see the README.${jf_note}" \
16 70 --defaultno; then
JELLYFIN_ENABLED=1
fi
}
interactive_config
# Jellyfin transcoding is CPU- and RAM-hungry, and the /transcode tmpfs comes
# out of the CT's RAM — so raise the defaults. Anything the caller set
# explicitly (env var or whiptail) wins and is left alone.
jellyfin_sizing() {
[ "$JELLYFIN_ENABLED" = "1" ] || return 0
if [ "$CT_CORES_PINNED" = "0" ] && [ "$CT_CORES" -lt "$CT_CORES_JELLYFIN" ]; then
CT_CORES="$CT_CORES_JELLYFIN"
msg_info "Jellyfin enabled — sizing the CT at ${CT_CORES} cores (set CT_CORES= to override)"
fi
if [ "$CT_RAM_PINNED" = "0" ] && [ "$CT_RAM" -lt "$CT_RAM_JELLYFIN" ]; then
CT_RAM="$CT_RAM_JELLYFIN"
msg_info "Jellyfin enabled — sizing the CT at ${CT_RAM}MB RAM (set CT_RAM= to override)"
fi
# The transcode tmpfs lives in this RAM; too little and a 4K transcode can
# squeeze the CT toward OOM.
if [ "$CT_RAM" -lt 6144 ]; then
msg_warn "CT has ${CT_RAM}MB RAM and Jellyfin's /transcode tmpfs (${JELLYFIN_TRANSCODE_TMPFS_SIZE}) comes out of it."
msg_warn "Lower JELLYFIN_TRANSCODE_TMPFS_SIZE in .env, or give the CT more RAM, if transcodes are expected."
fi
}
jellyfin_sizing
# --------------------------------------------------------------------------- #
# Resolve CTID, password, template
# --------------------------------------------------------------------------- #
[ -n "$CTID" ] || CTID="$(pvesh get /cluster/nextid)"
[ -n "$CT_PASSWORD" ] || CT_PASSWORD="$(rand_alnum 16)"
pct status "$CTID" >/dev/null 2>&1 && die "CTID ${CTID} already exists. Set CTID= to choose another."
header "ArrStack — creating CT ${CTID} (${CT_HOSTNAME})"
msg_info "Refreshing template catalog"
pveam update >/dev/null 2>&1 || true
TEMPLATE="$(pveam available --section system 2>/dev/null | awk '/debian-12-standard/{print $2}' | sort -V | tail -n1)"
[ -n "$TEMPLATE" ] || die "No debian-12-standard template available via pveam."
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
msg_info "Downloading ${TEMPLATE} to ${TEMPLATE_STORAGE}"
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null
fi
TEMPLATE_REF="${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}"
msg_ok "Template ready: ${TEMPLATE}"
# Network config string
if [ "$CT_NET" = "dhcp" ]; then
NETCONF="name=eth0,bridge=${CT_BRIDGE},ip=dhcp"
else
NETCONF="name=eth0,bridge=${CT_BRIDGE},ip=${CT_NET}"
[ -n "$CT_GW" ] && NETCONF="${NETCONF},gw=${CT_GW}"
fi
# --------------------------------------------------------------------------- #
# Create the container (stopped) so storage can be attached before first boot.
# --------------------------------------------------------------------------- #
msg_info "Creating container"
pct create "$CTID" "$TEMPLATE_REF" \
--hostname "$CT_HOSTNAME" \
--cores "$CT_CORES" --memory "$CT_RAM" --swap "$CT_SWAP" \
--rootfs "${CT_STORAGE}:${CT_DISK}" \
--net0 "$NETCONF" \
--unprivileged "$CT_UNPRIVILEGED" \
--features "$CT_FEATURES" \
--ostype debian \
--password "$CT_PASSWORD" \
--onboot 1 \
--tags "arrstack" \
--description "ArrStack (Docker) — managed by github.com/${REPO}" >/dev/null
msg_ok "Container ${CTID} created"
# --------------------------------------------------------------------------- #
# Storage: format a disk / bind an existing dir, before first start.
# --------------------------------------------------------------------------- #
setup_storage() {
case "$STORAGE_MODE" in
skip)
msg_warn "No storage configured. Bind a data dir later, e.g.:"
printf ' scripts/setup-storage.sh --ctid %s --disk /dev/sdX\n' "$CTID"
printf ' or add a bind mount and set DATA_ROOT accordingly. Using CT-local %s for now.\n' "$DATA_MP" ;;
disk|existing)
local sscript="${SCRIPT_DIR}/scripts/setup-storage.sh"
if [ ! -f "$sscript" ]; then
sscript="$(mktemp)"; curl -fsSL "${RAW_BASE}/scripts/setup-storage.sh" -o "$sscript"; chmod +x "$sscript"
fi
local args=(--ctid "$CTID" --data-mp "$DATA_MP" --puid "$PUID" --pgid "$PGID" --mount-base "$MOUNT_BASE")
if [ "$STORAGE_MODE" = "disk" ]; then
[ -n "$STORAGE_DISK" ] && args+=(--disk "$STORAGE_DISK")
[ "$ASSUME_YES" = "1" ] && args+=(-y)
else
args+=(--existing "$STORAGE_EXISTING" -y)
fi
msg_info "Setting up storage (${STORAGE_MODE})"
bash "$sscript" "${args[@]}" ;;
esac
}
setup_storage
msg_info "Starting container"
pct start "$CTID"
msg_info "Waiting for network in the container"
NET_OK=0
for _ in $(seq 1 45); do
if pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1; then NET_OK=1; break; fi
sleep 2
done
[ "$NET_OK" = "1" ] || die "Container ${CTID} has no working network/DNS yet."
msg_ok "Container is online"
# --------------------------------------------------------------------------- #
# Install Docker inside the CT
# --------------------------------------------------------------------------- #
header "Installing Docker in CT ${CTID}"
pct exec "$CTID" -- bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq ca-certificates curl >/dev/null
if ! command -v docker >/dev/null 2>&1; then
curl -fsSL https://get.docker.com | sh >/dev/null
fi
systemctl enable --now docker >/dev/null 2>&1 || true
'
msg_ok "Docker installed: $(pct exec "$CTID" -- docker --version 2>/dev/null)"
# --------------------------------------------------------------------------- #
# Deliver the repo (compose files, caddy, scripts) into the CT
# --------------------------------------------------------------------------- #
pct exec "$CTID" -- mkdir -p "$STACK_DIR"
if [ -f "${SCRIPT_DIR}/docker-compose.yml" ]; then
msg_info "Copying stack into CT (local mode)"
TGZ="/tmp/arrstack-${CTID}.tgz"
tar -C "$SCRIPT_DIR" -czf "$TGZ" \
docker-compose.yml docker-compose.vpn.yml docker-compose.jellyfin.yml \
.env.example caddy lib scripts 2>/dev/null
pct push "$CTID" "$TGZ" "${STACK_DIR}/arrstack.tgz"
pct exec "$CTID" -- tar -C "$STACK_DIR" -xzf "${STACK_DIR}/arrstack.tgz"
pct exec "$CTID" -- rm -f "${STACK_DIR}/arrstack.tgz"
rm -f "$TGZ"
else
msg_info "Cloning ${GIT_URL} into CT (remote mode)"
pct exec "$CTID" -- bash -c \
"apt-get install -y -qq git >/dev/null && git clone --depth=1 -b '${BRANCH}' '${GIT_URL}.git' '${STACK_DIR}'"
fi
# --------------------------------------------------------------------------- #
# Generate .env inside the CT + pre-create the media layout
# --------------------------------------------------------------------------- #
msg_info "Writing ${STACK_DIR}/.env"
# Build the overlay list: base + whichever overlays were asked for. If none
# were, ship the line commented out as a hint.
COMPOSE_FILES="docker-compose.yml"
[ "$VPN_ENABLED" = "1" ] && COMPOSE_FILES="${COMPOSE_FILES}:docker-compose.vpn.yml"
[ "$JELLYFIN_ENABLED" = "1" ] && COMPOSE_FILES="${COMPOSE_FILES}:docker-compose.jellyfin.yml"
if [ "$COMPOSE_FILES" = "docker-compose.yml" ]; then
COMPOSE_FILE_LINE="# COMPOSE_FILE=docker-compose.yml:docker-compose.vpn.yml"
else
COMPOSE_FILE_LINE="COMPOSE_FILE=${COMPOSE_FILES}"
fi
pct exec "$CTID" -- bash -c "cat > '${STACK_DIR}/.env'" <<EOF
# Generated by arrstack.sh on the Proxmox host.
PUID=${PUID}
PGID=${PGID}
UMASK=002
TZ=${STACK_TZ}
DATA_ROOT=${DATA_MP}
CONFIG_ROOT=${CONFIG_ROOT}
FLARESOLVERR_LOG_LEVEL=info
JELLYSEERR_LOG_LEVEL=info
# --- Overlays (add/remove files here, then: docker compose up -d) ---
${COMPOSE_FILE_LINE}
# --- Jellyfin (docker-compose.jellyfin.yml) ---
JELLYFIN_PUBLISHED_URL=${JELLYFIN_PUBLISHED_URL}
JELLYFIN_TRANSCODE_TMPFS_SIZE=${JELLYFIN_TRANSCODE_TMPFS_SIZE}
# --- VPN (gluetun) ---
VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
VPN_TYPE=${VPN_TYPE}
WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
OPENVPN_USER=
OPENVPN_PASSWORD=
VPN_SERVER_COUNTRIES=${VPN_SERVER_COUNTRIES}
LAN_SUBNET=${LAN_SUBNET}
EOF
pct exec "$CTID" -- chmod 600 "${STACK_DIR}/.env"
msg_info "Creating media layout under ${DATA_MP}"
pct exec "$CTID" -- bash -c "
install -d -o ${PUID} -g ${PGID} \
'${DATA_MP}/media/movies' '${DATA_MP}/media/tv' '${DATA_MP}/media/music' \
'${DATA_MP}/torrents/incomplete' '${CONFIG_ROOT}'
# setgid so new files inherit the media group
find '${DATA_MP}' -type d -exec chmod 2775 {} + 2>/dev/null || true
"
if [ "$VPN_ENABLED" = "1" ] && [ -z "$WIREGUARD_PRIVATE_KEY" ]; then
msg_warn "VPN enabled but no WireGuard key given — qBittorrent will not have network"
msg_warn "until you set WIREGUARD_PRIVATE_KEY in ${STACK_DIR}/.env and re-run 'docker compose up -d'."
fi
# --------------------------------------------------------------------------- #
# Bring the stack up
# --------------------------------------------------------------------------- #
header "Starting the stack"
pct exec "$CTID" -- bash -c "cd '${STACK_DIR}' && docker compose pull -q && docker compose up -d"
msg_ok "Stack is up"
# --------------------------------------------------------------------------- #
# Final host-side summary
# --------------------------------------------------------------------------- #
IP="$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}')"
IP="${IP:-<ct-ip>}"
header "ArrStack ready"
msg_ok "CT ${CTID} (${CT_HOSTNAME}) at ${IP}"
printf ' root password : %s\n' "$CT_PASSWORD"
printf ' shell : pct enter %s (stack in %s)\n\n' "$CTID" "$STACK_DIR"
printf ' %-12s %s\n' "SERVICE" "URL"
printf ' %-12s %s\n' "-------" "---"
SERVICE_ROWS="sonarr 8989|radarr 7878|lidarr 8686|prowlarr 9696|bazarr 6767|qbittorrent 8080|jellyseerr 5055|flaresolverr 8191"
[ "$JELLYFIN_ENABLED" = "1" ] && SERVICE_ROWS="${SERVICE_ROWS}|jellyfin 8096"
(IFS='|'; for row in $SERVICE_ROWS; do
set -- $row; printf ' %-12s http://%s:%s\n' "$1" "$IP" "$2"
done)
echo
msg_info "Next: wire the apps together — see docs/SETUP.md in the repo."
[ "$JELLYFIN_ENABLED" = "1" ] && msg_info "Jellyfin: add libraries at /data/media/{movies,tv,music} (read-only mount). Hardware transcoding needs the GPU passed into CT ${CTID} — see the README."
[ "$VPN_ENABLED" = "1" ] && msg_info "VPN: qBittorrent is routed through gluetun. Verify with: pct exec ${CTID} -- docker exec gluetun wget -qO- https://ipinfo.io/ip"