Skip to content

Commit e8dc062

Browse files
committed
Improve installer flexibility and preflight reliability
1 parent cde5356 commit e8dc062

9 files changed

Lines changed: 445 additions & 70 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ Requires OrbStack (or Docker Desktop) and Plex already installed. Handles everyt
6464
curl -fsSL https://raw.githubusercontent.com/liamvibecodes/mac-media-stack/main/bootstrap.sh | bash
6565
```
6666

67+
Optional flags when running from a local clone:
68+
69+
```bash
70+
bash bootstrap.sh --media-dir /Volumes/T9/Media --install-dir ~/mac-media-stack --non-interactive
71+
```
72+
6773
<details>
6874
<summary>See it in action</summary>
6975
<br>
@@ -79,6 +85,7 @@ git clone https://github.com/liamvibecodes/mac-media-stack.git
7985
cd mac-media-stack
8086
bash scripts/setup.sh # creates folders, generates .env
8187
# edit .env and add your VPN keys
88+
bash scripts/doctor.sh # preflight validation before first boot
8289
docker compose up -d # start everything
8390
docker compose --profile autoupdate up -d watchtower # optional auto-updates
8491
bash scripts/configure.sh # auto-configure all services
@@ -96,6 +103,7 @@ By default, Seerr is bound to `127.0.0.1` for safer local-only access. Set `SEER
96103
| Script | Purpose |
97104
|--------|---------|
98105
| `scripts/setup.sh` | Creates folder structure and .env file |
106+
| `scripts/doctor.sh` | Runs preflight checks (runtime, env, compose, ports) |
99107
| `scripts/configure.sh` | Auto-configures all service connections |
100108
| `scripts/health-check.sh` | Checks if everything is running correctly |
101109
| `scripts/auto-heal.sh` | Hourly self-healer (restarts VPN/containers if down) |

SETUP.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ curl -fsSL https://raw.githubusercontent.com/liamvibecodes/mac-media-stack/main/
2121

2222
It will prompt you for VPN keys and walk you through the Seerr login. If you'd rather do each step yourself, continue with the manual guide below.
2323

24+
You can also run it locally with custom paths:
25+
26+
```bash
27+
bash bootstrap.sh --media-dir /Volumes/T9/Media --install-dir ~/mac-media-stack
28+
```
29+
2430
---
2531

2632
## What You Need
@@ -133,13 +139,19 @@ WIREGUARD_ADDRESSES=10.2.0.2/32
133139

134140
## Step 6: Start the Stack
135141

142+
Run preflight checks before first startup:
143+
136144
```bash
137-
docker compose up -d
145+
bash scripts/doctor.sh
138146
```
139147

140-
This will download everything it needs (about 2-3 GB, may take a few minutes on the first run). You'll see each service being created.
148+
Then start services:
149+
150+
```bash
151+
docker compose up -d
152+
```
141153

142-
When it's done, wait about 30 seconds for everything to start up, then run the health check:
154+
This will download everything it needs (about 2-3 GB, may take a few minutes on the first run). You'll see each service being created. After it starts, run the health check:
143155

144156
```bash
145157
bash scripts/health-check.sh
@@ -179,7 +191,7 @@ The script will:
179191
- Connect everything together (Prowlarr, Radarr, Sonarr, Seerr)
180192
- Ask you to sign in to Seerr with Plex (one browser click)
181193

182-
At the end it will print your qBittorrent password. Save it somewhere just in case, but you shouldn't need it for normal use.
194+
At the end it will print your qBittorrent password and save credentials/API keys to `~/Media/state/first-run-credentials.txt` (mode `600`).
183195

184196
---
185197
## Step 9: Install Auto-Healer (Optional but Recommended)

bootstrap.sh

Lines changed: 127 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,64 @@ YELLOW='\033[1;33m'
1010
CYAN='\033[0;36m'
1111
NC='\033[0m'
1212

13+
MEDIA_DIR="$HOME/Media"
14+
INSTALL_DIR="$HOME/mac-media-stack"
15+
NON_INTERACTIVE=false
16+
17+
usage() {
18+
cat <<EOF
19+
Usage: bash bootstrap.sh [OPTIONS]
20+
21+
Options:
22+
--media-dir DIR Media root path (default: ~/Media)
23+
--install-dir DIR Repo install directory (default: ~/mac-media-stack)
24+
--non-interactive Skip interactive prompts (manual Seerr wiring required)
25+
--help Show this help message
26+
27+
Examples:
28+
bash bootstrap.sh
29+
bash bootstrap.sh --media-dir /Volumes/T9/Media
30+
bash bootstrap.sh --media-dir /Volumes/T9/Media --non-interactive
31+
EOF
32+
}
33+
34+
while [[ $# -gt 0 ]]; do
35+
case "$1" in
36+
--media-dir)
37+
if [[ $# -lt 2 || "$2" == --* ]]; then
38+
echo "Missing value for --media-dir"
39+
exit 1
40+
fi
41+
MEDIA_DIR="$2"
42+
shift 2
43+
;;
44+
--install-dir)
45+
if [[ $# -lt 2 || "$2" == --* ]]; then
46+
echo "Missing value for --install-dir"
47+
exit 1
48+
fi
49+
INSTALL_DIR="$2"
50+
shift 2
51+
;;
52+
--non-interactive)
53+
NON_INTERACTIVE=true
54+
shift
55+
;;
56+
--help|-h)
57+
usage
58+
exit 0
59+
;;
60+
*)
61+
echo "Unknown option: $1"
62+
usage
63+
exit 1
64+
;;
65+
esac
66+
done
67+
68+
MEDIA_DIR="${MEDIA_DIR/#\~/$HOME}"
69+
INSTALL_DIR="${INSTALL_DIR/#\~/$HOME}"
70+
1371
echo ""
1472
echo "=============================="
1573
echo " Mac Media Stack Installer"
@@ -51,6 +109,26 @@ detect_running_runtime() {
51109
fi
52110
}
53111

112+
wait_for_service() {
113+
local name="$1"
114+
local url="$2"
115+
local max_attempts="${3:-45}"
116+
local attempt=0
117+
118+
while [[ $attempt -lt $max_attempts ]]; do
119+
status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$url" 2>/dev/null || true)
120+
if [[ "$status" =~ ^(200|301|302|401|403)$ ]]; then
121+
echo -e " ${GREEN}OK${NC} $name is reachable"
122+
return 0
123+
fi
124+
sleep 2
125+
attempt=$((attempt + 1))
126+
done
127+
128+
echo -e " ${YELLOW}WARN${NC} $name is not reachable yet (continuing anyway)"
129+
return 1
130+
}
131+
54132
INSTALLED_RUNTIME=$(detect_installed_runtime)
55133

56134
if ! docker info &>/dev/null; then
@@ -85,10 +163,12 @@ if ! command -v git &>/dev/null; then
85163
exit 1
86164
fi
87165

166+
echo ""
167+
echo "Install dir: $INSTALL_DIR"
168+
echo "Media dir: $MEDIA_DIR"
88169
echo ""
89170

90171
# Clone
91-
INSTALL_DIR="$HOME/mac-media-stack"
92172
if [[ -d "$INSTALL_DIR" ]]; then
93173
echo -e "${YELLOW}Note:${NC} $INSTALL_DIR already exists. Pulling latest..."
94174
if ! git -C "$INSTALL_DIR" pull --ff-only; then
@@ -107,46 +187,69 @@ echo ""
107187

108188
# Setup
109189
echo -e "${CYAN}Running setup...${NC}"
110-
bash scripts/setup.sh
190+
bash scripts/setup.sh --media-dir "$MEDIA_DIR"
111191

112192
echo ""
113193

114194
# VPN keys
115195
if grep -q "your_wireguard_private_key_here" .env 2>/dev/null; then
116-
echo -e "${CYAN}VPN Configuration${NC}"
117-
echo ""
118-
echo "You need your ProtonVPN WireGuard credentials."
119-
echo "If someone gave you a private key and address, enter them now."
120-
echo ""
121-
read -s -p " WireGuard Private Key: " vpn_key
122-
echo ""
123-
read -p " WireGuard Address (e.g. 10.2.0.2/32): " vpn_addr
124-
125-
if [[ -n "$vpn_key" && -n "$vpn_addr" ]]; then
126-
sed -i '' "s|WIREGUARD_PRIVATE_KEY=.*|WIREGUARD_PRIVATE_KEY=$vpn_key|" .env
127-
sed -i '' "s|WIREGUARD_ADDRESSES=.*|WIREGUARD_ADDRESSES=$vpn_addr|" .env
128-
echo -e " ${GREEN}VPN keys saved${NC}"
196+
if [[ "$NON_INTERACTIVE" == true ]]; then
197+
echo -e "${YELLOW}WARN${NC} Non-interactive mode: VPN placeholders still present in .env"
198+
echo " Update WIREGUARD_PRIVATE_KEY and WIREGUARD_ADDRESSES before using the stack."
129199
else
130-
echo -e " ${YELLOW}Skipped.${NC} Edit .env manually before starting."
131-
echo " Run: open -a TextEdit $INSTALL_DIR/.env"
200+
echo -e "${CYAN}VPN Configuration${NC}"
201+
echo ""
202+
echo "You need your ProtonVPN WireGuard credentials."
203+
echo "If someone gave you a private key and address, enter them now."
204+
echo ""
205+
read -s -p " WireGuard Private Key: " vpn_key
206+
echo ""
207+
read -p " WireGuard Address (e.g. 10.2.0.2/32): " vpn_addr
208+
209+
if [[ -n "$vpn_key" && -n "$vpn_addr" ]]; then
210+
sed -i '' "s|WIREGUARD_PRIVATE_KEY=.*|WIREGUARD_PRIVATE_KEY=$vpn_key|" .env
211+
sed -i '' "s|WIREGUARD_ADDRESSES=.*|WIREGUARD_ADDRESSES=$vpn_addr|" .env
212+
echo -e " ${GREEN}VPN keys saved${NC}"
213+
else
214+
echo -e " ${YELLOW}Skipped.${NC} Edit .env manually before starting."
215+
echo " Run: open -a TextEdit $INSTALL_DIR/.env"
216+
fi
132217
fi
133218
fi
134219

135220
echo ""
136221

222+
# Preflight
223+
echo -e "${CYAN}Running preflight checks...${NC}"
224+
if ! bash scripts/doctor.sh --media-dir "$MEDIA_DIR"; then
225+
echo ""
226+
echo -e "${RED}Preflight checks failed.${NC} Fix the FAIL items above, then re-run bootstrap."
227+
exit 1
228+
fi
229+
230+
echo ""
231+
137232
# Start stack
138233
echo -e "${CYAN}Starting media stack...${NC}"
139234
echo " (First run downloads ~2-3 GB, this may take a few minutes)"
140235
echo ""
141236
docker compose up -d
142237

143238
echo ""
144-
echo "Waiting 30 seconds for services to initialize..."
145-
sleep 30
239+
echo "Waiting for core services..."
240+
wait_for_service "qBittorrent" "http://localhost:8080" || true
241+
wait_for_service "Prowlarr" "http://localhost:9696" || true
242+
wait_for_service "Radarr" "http://localhost:7878" || true
243+
wait_for_service "Sonarr" "http://localhost:8989" || true
244+
wait_for_service "Seerr" "http://localhost:5055" || true
146245

147246
# Configure
148247
echo ""
149-
bash scripts/configure.sh
248+
if [[ "$NON_INTERACTIVE" == true ]]; then
249+
bash scripts/configure.sh --non-interactive
250+
else
251+
bash scripts/configure.sh
252+
fi
150253

151254
# Auto-heal
152255
echo ""
@@ -161,7 +264,9 @@ echo ""
161264
echo " Seerr (browse/request): http://localhost:5055"
162265
echo " Plex (watch): http://localhost:32400/web"
163266
echo ""
267+
echo " Media location: $MEDIA_DIR"
268+
echo ""
164269
echo " Next: Set up Plex libraries (Settings > Libraries > Add)"
165-
echo " - Movies: ~/Media/Movies"
166-
echo " - TV Shows: ~/Media/TV Shows"
270+
echo " - Movies: $MEDIA_DIR/Movies"
271+
echo " - TV Shows: $MEDIA_DIR/TV Shows"
167272
echo ""

scripts/auto-heal.sh

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,25 @@ fi
2626
HEALED=0
2727

2828
# Check VPN tunnel
29-
vpn_ip=$(docker exec gluetun sh -c 'wget -qO- --timeout=5 https://ipinfo.io/ip' 2>/dev/null)
30-
local_ip=$(curl -s --max-time 5 https://ipinfo.io/ip 2>/dev/null)
29+
vpn_ip=$(docker exec gluetun sh -lc 'cat /tmp/gluetun/ip 2>/dev/null || true' 2>/dev/null)
30+
vpn_iface=$(docker exec gluetun sh -lc 'ls /sys/class/net 2>/dev/null | grep -E "^(tun|wg)[0-9]+$" | head -1' 2>/dev/null)
31+
vpn_health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' gluetun 2>/dev/null || true)
3132

32-
if [[ -z "$vpn_ip" ]]; then
33-
log "WARN: VPN not responding. Restarting gluetun..."
33+
if [[ -z "$vpn_ip" || -z "$vpn_iface" || "$vpn_health" == "unhealthy" ]]; then
34+
log "WARN: VPN status unhealthy (health=${vpn_health:-unknown}, ip=${vpn_ip:-none}, iface=${vpn_iface:-none}). Restarting gluetun..."
3435
docker restart gluetun >> "$LOG" 2>&1
3536
sleep 15
36-
vpn_ip=$(docker exec gluetun sh -c 'wget -qO- --timeout=5 https://ipinfo.io/ip' 2>/dev/null)
37-
if [[ -n "$vpn_ip" && "$vpn_ip" != "$local_ip" ]]; then
38-
log "OK: VPN recovered (IP: $vpn_ip)"
37+
vpn_ip=$(docker exec gluetun sh -lc 'cat /tmp/gluetun/ip 2>/dev/null || true' 2>/dev/null)
38+
vpn_iface=$(docker exec gluetun sh -lc 'ls /sys/class/net 2>/dev/null | grep -E "^(tun|wg)[0-9]+$" | head -1' 2>/dev/null)
39+
vpn_health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' gluetun 2>/dev/null || true)
40+
if [[ -n "$vpn_ip" && -n "$vpn_iface" && "$vpn_health" != "unhealthy" ]]; then
41+
log "OK: VPN recovered (IP: $vpn_ip, iface=$vpn_iface, health=${vpn_health:-unknown})"
3942
((HEALED++))
4043
else
41-
log "ERROR: VPN still down after restart"
44+
log "ERROR: VPN still down after restart (health=${vpn_health:-unknown}, ip=${vpn_ip:-none}, iface=${vpn_iface:-none})"
4245
fi
43-
elif [[ "$vpn_ip" == "$local_ip" ]]; then
44-
log "WARN: VPN IP matches real IP. Tunnel not working. Restarting gluetun..."
45-
docker restart gluetun >> "$LOG" 2>&1
46-
sleep 15
47-
((HEALED++))
4846
else
49-
log "OK: VPN active (IP: $vpn_ip)"
47+
log "OK: VPN active (IP: $vpn_ip, iface=$vpn_iface, health=${vpn_health:-unknown})"
5048
fi
5149

5250
# Check core containers are running

0 commit comments

Comments
 (0)