- extras/restore_kopia_backup.sh: interactive restore tool — lists all snapshot sources, pick source + snapshot, choose inspect (restore to /tmp) or restore-in-place (moves current data aside as .pre-restore-DATE for easy rollback, auto-stops/starts the associated Docker service) - services/backup.sh: copy restore script to ~/docker/backup/ during install; update summary to show restore command - CLAUDE.md: add note to update README services table when adding a service; add Authelia section documenting which services have built-in auth, the import-authelia Caddy pattern for no-auth services, and remember_me_duration config for kiosk/persistent sessions https://claude.ai/code/session_019XgsQ13XKm4Zj3cNsDNwHj
8.6 KiB
CLAUDE.md — ubuntu-post-install contributor guide
Context for adding or modifying services. Read this before touching any service file so the result matches what's already here.
How the system works
setup.sh sources lib/common.sh then globs every services/*.sh file.
Each service file self-registers and defines its install function. Nothing
in setup.sh needs to change when you add a service — just add the file.
The wizard groups services by category (from register_service), shows a
checklist per group, and calls install_<name>() for each selected item.
--list, --dry-run, and --unattended all work automatically.
Adding a service — the three-step rule
- Create
services/<name>.sh(kebab-case filename) - Call
register_serviceat the top of the file - Define
install_<name>()(hyphens → underscores in function name)
That's it. The menu picks it up on the next run.
Also update the Services table in README.md — add the service name to the
appropriate group row so the README stays current.
Minimal Docker service template
#!/bin/bash
# services/my-tool.sh — One-line description.
# Part of the modular post-install system (sourced by setup.sh).
register_service my-tool utilities "What it does (My Tool)" 8080
install_my_tool() {
require_docker || return 1
local DIR="$DOCKER_DIR/my-tool"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $DIR with docker-compose.yml"
return 0
fi
mkdir -p "$DIR"
ensure_docker_dir_ownership "$DIR"
cd "$DIR" || return 1
cat > docker-compose.yml << 'EOF'
name: my-tool
services:
my-tool:
image: vendor/my-tool:latest
container_name: my-tool
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./data:/data
EOF
configure_caddy_for_service "My Tool" "8080" "my-tool"
write_readme "$DIR" << 'MD'
# My Tool
Brief description.
## Manage
```bash
docker compose up -d
docker compose down
docker compose logs -f
docker compose pull && docker compose up -d
MD
local START=""
prompt_yn "Start My Tool now? (y/n):" "y" START
if [ "$START" = "y" ] || [ "$START" = "Y" ]; then
docker compose up -d \
&& log_success "My Tool started" \
|| log_warning "Start failed — check: docker compose logs"
fi
}
## register_service signature
```bash
register_service <name> <group> "<description>" [port]
name— kebab-case, matches the filename and theinstall_functiongroup— one of the categories below; determines which menu it appears indescription— shown in--listand the menu checklistport— optional; informational only (not used by the framework)
Available globals
| Variable | Value |
|---|---|
DOCKER_DIR |
~/docker — parent for all Docker service directories |
ACTUAL_USER |
The non-root user that invoked sudo |
ACTUAL_HOME |
Home directory of ACTUAL_USER |
SITE_TZ |
Timezone from site config, e.g. America/New_York |
SITE_DOMAIN |
Base domain from site config, e.g. example.com |
SITE_CADDY_NET |
Docker network name for Caddy (default: caddy_net) |
DRY_RUN |
true/false — set by --dry-run flag |
UNATTENDED |
true/false — set by --unattended flag |
Available helpers (lib/common.sh)
Logging
log_info "message" # blue [INFO]
log_success "message" # green [OK]
log_warning "message" # yellow [WARN]
log_error "message" # red [ERROR]
Prompts — honor UNATTENDED automatically
prompt_yn "Question? (y/n):" "default_y_or_n" VARNAME
prompt_text "Question? [default]:" "default" VARNAME
When UNATTENDED=true both functions skip the prompt and use the default.
Pre-flight
require_root # exits with an error if not running as root
require_docker # installs Docker CE + Compose plugin if missing, then returns
Execution and ownership
run_cmd COMMAND [args...] # no-ops in DRY_RUN, executes otherwise
ensure_docker_dir_ownership DIR... # chown -R ACTUAL_USER:ACTUAL_USER (skips in DRY_RUN)
generate_password [length] # alphanumeric random string, default 32 chars
pip_user_install PACKAGE... # pip3 --user with --break-system-packages on 24.04+
Caddy reverse proxy
configure_caddy_for_service "Display Name" "PORT" "default-subdomain" ["extra-block"]
Prompts the user for a domain, appends a site block to the Caddyfile, and
reloads Caddy. No-ops silently if Caddy isn't installed. The fourth argument
is an optional string inserted verbatim inside the Caddy site block (use it
for import authelia or custom matchers).
README generation
write_readme "$DIR" << 'MD'
# Title
Content
MD
Writes $DIR/README.md (creates the directory if needed). No-ops in DRY_RUN.
Every Docker service should call this so ~/docker/<name>/README.md is
self-documenting on the deployed box.
Categories
| Group | Purpose |
|---|---|
base |
CLI packages installed on every box |
homelab |
Core infrastructure — reverse proxy, auth, intrusion prevention |
utilities |
Self-hosted web apps — budget, DNS, files, monitoring, VPN, etc. |
media |
Media servers, photo backup, disc ripping |
cameras |
NVR and camera tooling (Frigate) |
gaming |
Game servers, cloud gaming (Wolf), emulation |
extras |
Non-Docker tools and scripts |
backup |
Backup solutions |
Authelia SSO — which services need it
Some services have their own login screens; others have none and need Caddy to gate them via Authelia.
Has built-in auth — no Authelia needed:
emby, jellyfin, audiobookshelf, immich, mealie, actualbudget,
homeassistant, portainer, meshcentral, traccar, uptimekuma,
filebrowser, wg-easy, ntfy (configurable)
No built-in auth — should be protected:
magicmirror, wolf-pair, js99er, sky-cam
For services without built-in auth, prompt the user before calling
configure_caddy_for_service and pass import authelia as the extra block
if Authelia is installed and the user wants SSO protection:
local EXTRA_BLOCK=""
if [ -d "$DOCKER_DIR/authelia" ]; then
local _use_auth=""
prompt_yn "Protect MagicMirror with Authelia SSO? (y/n):" "y" _use_auth
[[ "$_use_auth" =~ ^[Yy]$ ]] && EXTRA_BLOCK=" import authelia"
fi
configure_caddy_for_service "MagicMirror" "8081" "mirror" "$EXTRA_BLOCK"
Authelia "stay logged in" / kiosk mode:
Edit ~/docker/authelia/config/configuration.yml and set a long
remember_me_duration. Users then check "Remember me" once on login and
the session persists through reboots (Redis stores the session in a volume):
session:
secret: 'your-existing-secret'
remember_me_duration: 1y # add or update this line
expiration: 1h
inactivity: 5m
cookies:
- domain: 'example.com'
authelia_url: 'https://auth.example.com'
After editing: docker compose -f ~/docker/authelia/docker-compose.yml restart
Non-Docker services
Not everything is a container. For apt-based or git-clone–based services,
skip require_docker and the Docker helpers. See services/base.sh (apt
packages + Charm repo) and services/crowdsec.sh (official apt repo) as
reference patterns.
For non-Docker services the default is_installed check in setup.sh
looks for $DOCKER_DIR/$name, which won't exist. Add a case to the
is_installed() function in setup.sh so the [installed] marker appears
correctly in the menu:
# In setup.sh → is_installed()
my-tool) command -v my-tool >/dev/null 2>&1 ;;
Docker services use the default case and don't need an entry.
DRY_RUN convention
Every install_* function must check $DRY_RUN before touching the
filesystem, installing packages, or starting containers. The pattern is:
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would do X"
echo "[DRY-RUN] Would do Y"
return 0
fi
Put the check early — after any pure-display output (banners, info text) but before the first write.
.env files and secrets
Generate passwords with generate_password (never hardcode them).
Write secrets to .env files in the service directory, owned by
ACTUAL_USER, permissions 600. Document every variable with a comment
in the .env heredoc so the user knows what to change later.
Caddy network wiring
Services that need to reach Caddy (or each other) over Docker networking
should join the $SITE_CADDY_NET network. Add to docker-compose.yml:
networks:
caddy_net:
external: true
name: ${CADDY_NET:-caddy_net}
And read the network name from .env using CADDY_NET=$SITE_CADDY_NET.