Auto-install Docker, add USB drive docs, improve whitelist UI

- lib/common.sh: require_docker now installs Docker CE + Compose plugin
  via get.docker.com instead of erroring out if Docker is missing.
  Also adds the calling user to the docker group automatically.

- README.md: fix 'tells you how to install Docker' → 'installs Docker
  automatically'; add full USB drive usage section (mount, fstab,
  DOCKER_DIR config, moving existing data, tips).

- services/minecraft.sh: replace single-source whitelist import with the
  multi-source UI from the updated setup-minecraft.sh — collects players
  from the current instance, saved backup files, and other servers' backups;
  assigns letters to each source so you can import by letter (all from that
  source) or by number (specific player).

https://claude.ai/code/session_017WJtGcE5jjerAQCUBWUE3H
This commit is contained in:
Claude
2026-06-04 01:37:58 +00:00
parent 5be84d85ea
commit 8aa1d4552d
3 changed files with 207 additions and 46 deletions
+71 -1
View File
@@ -35,7 +35,7 @@ sudo ./setup.sh --unattended base # non-interactive, use defaults
**First run:** **First run:**
1. Installs essential CLI packages (`git`, `curl`, `htop`, `ncdu`, `jq`, `glow`, …) 1. Installs essential CLI packages (`git`, `curl`, `htop`, `ncdu`, `jq`, `glow`, …)
2. Checks Docker is present (tells you how to install it if not) 2. Installs Docker CE and the Compose plugin automatically if not already present
3. Offers to set **site defaults** — timezone, base domain, Caddy Docker network — 3. Offers to set **site defaults** — timezone, base domain, Caddy Docker network —
so every service picks them up automatically instead of asking each time so every service picks them up automatically instead of asking each time
4. Offers to install Caddy (the reverse proxy most services use) 4. Offers to install Caddy (the reverse proxy most services use)
@@ -82,6 +82,76 @@ docker compose pull && docker compose up -d # update
docker compose down # stop docker compose down # stop
``` ```
## Using a USB drive for Docker data
Storing Docker volumes and service data on a large USB drive keeps your OS
disk free and makes migrating to a new machine trivial.
### 1 — Mount the USB drive
Find the device name:
```bash
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT
```
Format as ext4 if needed (skip if already formatted):
```bash
sudo mkfs.ext4 /dev/sdX1 # replace sdX1 with your partition
```
Create a permanent mount point and mount it:
```bash
sudo mkdir -p /mnt/docker-data
sudo mount /dev/sdX1 /mnt/docker-data
```
Make it mount automatically on boot by adding to `/etc/fstab`:
```bash
# Get the drive's UUID (stable across reboots)
sudo blkid /dev/sdX1
# Add a line like this to /etc/fstab:
UUID=your-uuid-here /mnt/docker-data ext4 defaults,nofail 0 2
```
The `nofail` option means the system still boots normally if the drive is absent.
### 2 — Point this setup at the USB drive
The wizard stores all Docker service folders under `~/docker/` by default.
To use the USB drive instead, set `DOCKER_DIR` before running setup:
```bash
export DOCKER_DIR=/mnt/docker-data/docker
sudo -E ./setup.sh
```
Or configure it as the permanent default by creating/editing `~/.config/ubuntu-post-install.conf`:
```bash
echo 'DOCKER_DIR=/mnt/docker-data/docker' >> ~/.config/ubuntu-post-install.conf
```
The wizard reads this file on every run, so you only need to set it once.
### 3 — Move existing Docker data (optional)
If you already have data under `~/docker/` and want to move it:
```bash
sudo systemctl stop docker
sudo cp -a ~/docker/. /mnt/docker-data/docker/
sudo mv ~/docker ~/docker.bak # keep as backup
export DOCKER_DIR=/mnt/docker-data/docker
sudo systemctl start docker
```
Verify everything works, then delete the backup: `sudo rm -rf ~/docker.bak`
### Tips
- Check free space on the USB drive: `df -h /mnt/docker-data`
- Check the drive's health: `sudo smartctl -a /dev/sdX` (install: `sudo apt install smartmontools`)
- For Minecraft world data especially, a fast USB 3.0 drive or SSD is recommended
— spinning drives can cause I/O lag during chunk generation
## Compatibility ## Compatibility
Tested on **Ubuntu 24.04 LTS** and **26.04 LTS**. Tested on **Ubuntu 24.04 LTS** and **26.04 LTS**.
+23 -1
View File
@@ -153,8 +153,30 @@ require_root() {
} }
require_docker() { require_docker() {
if command -v docker &>/dev/null; then
return 0
fi
log_info "Docker is not installed — installing now..."
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would install Docker CE and Docker Compose plugin via get.docker.com"
return 0
fi
# Official Docker convenience script — installs Docker CE + Compose plugin
if curl -fsSL https://get.docker.com | sh; then
if [ -n "$ACTUAL_USER" ] && [ "$ACTUAL_USER" != "root" ]; then
usermod -aG docker "$ACTUAL_USER" \
&& log_info "Added $ACTUAL_USER to the docker group (re-login or run 'newgrp docker' to activate)"
fi
log_success "Docker installed ($(docker --version 2>/dev/null))"
else
log_error "Docker installation failed. Try manually: curl -fsSL https://get.docker.com | sh"
return 1
fi
if ! command -v docker &>/dev/null; then if ! command -v docker &>/dev/null; then
log_error "Docker is not installed. Install Docker first (run: $0 docker)." log_error "Docker binary not found after install — something went wrong."
return 1 return 1
fi fi
} }
+113 -44
View File
@@ -145,61 +145,130 @@ print(snaps[0] if snaps else '')
[[ $WHITELIST =~ ^[Yy]$ ]] && WHITELIST_ENABLED=true || WHITELIST_ENABLED=false [[ $WHITELIST =~ ^[Yy]$ ]] && WHITELIST_ENABLED=true || WHITELIST_ENABLED=false
local WHITELIST_PLAYERS=() local WHITELIST_PLAYERS=()
declare -A WHITELIST_PRELOADED # name → uuid, already resolved from existing file declare -A WHITELIST_PRELOADED # name → uuid, already resolved from existing files
if [ "$WHITELIST_ENABLED" = true ] && [ "$UNATTENDED" != true ]; then if [ "$WHITELIST_ENABLED" = true ] && [ "$UNATTENDED" != true ]; then
echo "" echo ""
log_info "Whitelist Players" log_info "Whitelist Players"
# Import from existing whitelist.json when re-running against an existing instance # Collect whitelist sources: live instance data + any saved backup files
local _EXISTING_WL="$MC_DIR/data/whitelist.json" local -a _WL_SRC_FILES _WL_SRC_LABELS _WL_SRC_LETTERS
if [ -f "$_EXISTING_WL" ]; then local _WL_LETTERS=(a b c d e f g h i j k l m n o p q r s t u v w x y z)
local -a _EX_NAMES _EX_UUIDS
mapfile -t _EX_NAMES < <(python3 -c " if [ -f "$MC_DIR/data/whitelist.json" ]; then
_WL_SRC_FILES+=("$MC_DIR/data/whitelist.json")
_WL_SRC_LABELS+=("${MC_NAME} (current)")
fi
local _BACKUP_WL="$DOCKER_DIR/whitelist-${MC_NAME}.json"
if [ -f "$_BACKUP_WL" ]; then
_WL_SRC_FILES+=("$_BACKUP_WL")
_WL_SRC_LABELS+=("${MC_NAME}-backup")
fi
# Also pick up whitelist backups from other instances
local _wlf
while IFS= read -r _wlf; do
[ "$_wlf" = "$_BACKUP_WL" ] && continue
_WL_SRC_FILES+=("$_wlf")
_WL_SRC_LABELS+=("$(basename "$_wlf" .json | sed 's/^whitelist-//')")
done < <(compgen -G "$DOCKER_DIR/whitelist-*.json" 2>/dev/null | sort)
if [ ${#_WL_SRC_FILES[@]} -gt 0 ]; then
local _i
for _i in "${!_WL_SRC_FILES[@]}"; do
_WL_SRC_LETTERS+=("${_WL_LETTERS[$_i]}")
done
# Read all sources, dedup by lowercase name
local -a _WL_ROWS
mapfile -t _WL_ROWS < <(python3 - "${_WL_SRC_LETTERS[@]}" "---" "${_WL_SRC_FILES[@]}" << 'PYEOF'
import json, sys import json, sys
try: args = sys.argv[1:]
data = json.load(open(sys.argv[1])) sep = args.index('---')
for p in data: letters, files = args[:sep], args[sep+1:]
if p.get('name'): print(p['name']) seen = set()
except: pass for letter, path in zip(letters, files):
" "$_EXISTING_WL" 2>/dev/null) try:
mapfile -t _EX_UUIDS < <(python3 -c " for p in json.load(open(path)):
import json, sys n = p.get('name','').strip()
try: if n and n.lower() not in seen:
data = json.load(open(sys.argv[1])) seen.add(n.lower())
for p in data: print(f"{letter}\t{n}\t{p.get('uuid','')}")
if p.get('uuid'): print(p['uuid']) except: pass
except: pass PYEOF
" "$_EXISTING_WL" 2>/dev/null) )
if [ ${#_EX_NAMES[@]} -gt 0 ]; then
echo "" if [ ${#_WL_ROWS[@]} -gt 0 ]; then
echo " Existing whitelist found (${#_EX_NAMES[@]} player(s)):" local -a _EX_NAMES _EX_UUIDS _EX_SRC
local _i local _row _s _n _u
for _i in "${!_EX_NAMES[@]}"; do for _row in "${_WL_ROWS[@]}"; do
printf " %d) %s\n" "$((_i+1))" "${_EX_NAMES[$_i]}" IFS=$'\t' read -r _s _n _u <<< "$_row"
_EX_NAMES+=("$_n"); _EX_UUIDS+=("$_u"); _EX_SRC+=("$_s")
done done
echo "" echo ""
echo " Import from existing? Enter numbers (e.g. 1,3,4), 0=all, Enter=skip:" echo " Available whitelist players:"
local _NUM=1 _LAST_SRC="" _si _SRC_LABEL
for _i in "${!_EX_NAMES[@]}"; do
if [ "${_EX_SRC[$_i]}" != "$_LAST_SRC" ]; then
[ -n "$_LAST_SRC" ] && echo ""
for _si in "${!_WL_SRC_LETTERS[@]}"; do
[ "${_WL_SRC_LETTERS[$_si]}" = "${_EX_SRC[$_i]}" ] \
&& _SRC_LABEL="${_WL_SRC_LABELS[$_si]}" && break
done
echo -e " \033[1;33m── [${_EX_SRC[$_i]}] ${_SRC_LABEL}\033[0m"
_LAST_SRC="${_EX_SRC[$_i]}"
fi
printf " %2d) %s\n" "$_NUM" "${_EX_NAMES[$_i]}"
_NUM=$((_NUM+1))
done
local _EX_FIRST="${_WL_SRC_LETTERS[0]}"
local _EX_SECOND="${_WL_SRC_LETTERS[1]:-}"
local _EX_STR="$_EX_FIRST"
[ -n "$_EX_SECOND" ] && _EX_STR="${_EX_FIRST},3 or ${_EX_SECOND},1,4 or ${_EX_FIRST},${_EX_SECOND}"
echo ""
echo " Letter = all players from that source. Number = specific player."
echo " Combine freely with commas. 0 = everyone. Enter = skip."
echo " Example: ${_EX_STR} or 0"
local _WL_IMPORT="" local _WL_IMPORT=""
read -p " Selection: " _WL_IMPORT read -p " Selection: " _WL_IMPORT
if [ -n "$_WL_IMPORT" ]; then if [ -n "$_WL_IMPORT" ]; then
if [ "$_WL_IMPORT" = "0" ]; then local -a _TOKENS
for _i in "${!_EX_NAMES[@]}"; do IFS=',' read -ra _TOKENS <<< "$_WL_IMPORT"
WHITELIST_PRELOADED["${_EX_NAMES[$_i]}"]="${_EX_UUIDS[$_i]}" local _tok _idx _matched
done for _tok in "${_TOKENS[@]}"; do
log_success " Imported all ${#_EX_NAMES[@]} existing player(s)" _tok="${_tok// /}"
else if [ "$_tok" = "0" ]; then
local -a _SEL_NUMS for _i in "${!_EX_NAMES[@]}"; do
IFS=',' read -ra _SEL_NUMS <<< "$_WL_IMPORT" WHITELIST_PRELOADED["${_EX_NAMES[$_i]}"]="${_EX_UUIDS[$_i]}"
local _n done
for _n in "${_SEL_NUMS[@]}"; do log_success " Imported all ${#_EX_NAMES[@]} player(s)"; break
_n="${_n// /}" elif [[ "$_tok" =~ ^[0-9]+$ ]]; then
if [[ "$_n" =~ ^[0-9]+$ ]] && [ "$_n" -ge 1 ] && \ _idx=$((_tok-1))
[ "$_n" -le "${#_EX_NAMES[@]}" ]; then if [ "$_idx" -ge 0 ] && [ "$_idx" -lt "${#_EX_NAMES[@]}" ]; then
WHITELIST_PRELOADED["${_EX_NAMES[$((_n-1))]}"]="${_EX_UUIDS[$((_n-1))]}" WHITELIST_PRELOADED["${_EX_NAMES[$_idx]}"]="${_EX_UUIDS[$_idx]}"
log_info " Imported: ${_EX_NAMES[$((_n-1))]}" log_info " Imported: ${_EX_NAMES[$_idx]}"
fi fi
done elif [[ "$_tok" =~ ^[a-z]$ ]]; then
fi _matched=false
for _i in "${!_EX_NAMES[@]}"; do
if [ "${_EX_SRC[$_i]}" = "$_tok" ]; then
WHITELIST_PRELOADED["${_EX_NAMES[$_i]}"]="${_EX_UUIDS[$_i]}"
_matched=true
fi
done
if [ "$_matched" = true ]; then
for _si in "${!_WL_SRC_LETTERS[@]}"; do
[ "${_WL_SRC_LETTERS[$_si]}" = "$_tok" ] \
&& log_success " Imported all from [${_tok}] ${_WL_SRC_LABELS[$_si]}" && break
done
else
log_warning " [${_tok}] not found in the list above"
fi
else
log_warning " '${_tok}' not recognised — use a letter or player number"
fi
done
fi fi
fi fi
fi fi