feat(provision): idempotent setup for local dev and remote server

This commit is contained in:
Pontoporeia
2026-09-18 16:26:36 +02:00
parent d5c1dfb23d
commit 541470b9bb
6 changed files with 221 additions and 27 deletions
-1
View File
@@ -1 +0,0 @@
APP_KEY=M+4xc/c9/b4H+ScjO4cU82FzcZRO+xp5pzYNvJapUOQ=
+35 -17
View File
@@ -19,38 +19,56 @@ Deployment is orchestrated through the `justfile` (`deploy` group).
## One-time server setup
From a machine with `ssh` access to the `xamxam` host, run the full remote
provisioning once:
```bash
ssh xamxam
sudo mkdir -p /var/www/xamxam
sudo chown www-data:xamxam /var/www/xamxam
sudo chmod 775 /var/www/xamxam
exit
just provision-server
```
Then from local, deploy once and apply the nginx config + verify permissions:
This chains (each step is also runnable individually):
1. `scripts/provision-server-env.sh` — ensure the server has an `APP_KEY` in
`/var/www/xamxam/.env` (idempotent; see below).
2. `just deploy` — code + Composer deps + migrations + permissions.
3. `just deploy-nginx` — install + apply the nginx config and fix permissions.
4. `just deploy-backup`, `just deploy-cleanup-cron`, `just deploy-logrotate` —
install backup + cleanup cron jobs and log rotation.
It finishes by telling you what's left to do in `/admin/account` (set the admin
password — a fresh DB starts unauthenticated — and configure SMTP/PeerTube
credentials and Nextcloud sync).
If you'd rather do it step by step (e.g. you already provisioned nginx):
```bash
just deploy
just deploy-nginx
```
For a full initial rollout including backup + cleanup cron jobs:
For a full initial rollout *without* the env/nginx steps (backup + cleanup cron
only), when those are already handled:
```bash
just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron
just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron + deploy-logrotate
```
### First-time `APP_KEY` on a brand-new server
### Server `APP_KEY` — idempotent, never overwrites
`deploy-env` uploads the **local** `app/.env` only when the remote has none. Do
**not** rely on a committed `.env` — generate a fresh key per environment and put
it on the server before first `deploy`: create `app/.env` locally (see
[development.md](development.md#appkey-and-the-appenv-file)) so `deploy-env`
uploads it, or create `/var/www/xamxam/.env` on the server directly:
`just provision-server` (via `scripts/provision-server-env.sh`) ensures the
server has an `APP_KEY` in `/var/www/xamxam/.env`. It is **idempotent** and will
**never overwrite an existing key**:
```bash
ssh xamxam 'sudo -u www-data bash -c "echo APP_KEY=\$(php -r \"echo base64_encode(random_bytes(32));\") > /var/www/xamxam/.env; chmod 640 /var/www/xamxam/.env; chown www-data:xamxam /var/www/xamxam/.env"'
```
- If `/var/www/xamxam/.env` already has an `APP_KEY=…` → it is left untouched
and the script prints that the key already exists (so encrypted credentials
stay decryptable). It exits `0`.
- If the file exists but has no `APP_KEY` → a fresh key is appended, existing
lines untouched.
- If the file is absent → it is created with a fresh key.
Ownership (`www-data:xamxam`) and permissions (`640`) are normalised after any
write. `deploy-env` also refuses to overwrite a remote `.env` that already has
`APP_KEY`.
If you ever rotate `APP_KEY`, re-encrypt the SMTP password with
`just reencrypt-password <new_base64_key>` and push the new key via
+25 -9
View File
@@ -13,8 +13,23 @@ Setup, workflow, building assets, and testing for XAMXAM development.
## One-time setup
From the repo root, install dependencies (manually — there is no `just` recipe
for these):
From the repo root, run the provisioning script:
```bash
just provision
```
This is **idempotent** and safe to re-run. It:
1. Ensures `app/.env` has an `APP_KEY` — **never overwrites an existing key**
(it prints a message and keeps the current one so encrypted credentials stay
decryptable).
2. Installs Composer deps (`composer install`) and JS deps (`npm ci`).
3. Runs the DB schema + migrations (`just migrate`).
4. Checks the first-admin-password state and tells you if `/admin/` is still
unauthenticated.
If you prefer to run the steps manually, or `just` isn't installed yet:
```bash
composer install # PHP deps (vendor/)
@@ -26,8 +41,7 @@ just migrate # create/update the SQLite DB from schema + migration
— it still clones the old `php-live-reload` library and creates legacy
`admin/data/` directories. Live-reload now ships inside the app
(`app/public/live-reload.php`), and assets are built with rolldown/lightningcss,
not the live-reload watcher. Prefer the explicit `composer install` + `npm ci`
above.
not the live-reload watcher. Prefer `just provision`.
### APP_KEY and the app/.env file
@@ -40,17 +54,19 @@ password, PeerTube credentials, and Nextcloud WebDAV sync) requires an
APP_KEY=<base64-encoded 32 random bytes>
```
Generate one with:
Generate one manually with:
```bash
php -r 'echo "APP_KEY=" . base64_encode(random_bytes(32)) . PHP_EOL;' > app/.env
chmod 600 app/.env
```
`app/.env` must **never** be committed. If the file is missing, the public site
still runs but any path that reads encrypted credentials throws
`RuntimeException: APP_KEY not found`. On a fresh clone, create it before
relying on SMTP/PeerTube/Nextcloud features.
`app/.env` must **never** be committed. `just provision` only creates `app/.env`
(or appends `APP_KEY`) when the key is absent — if the value already exists it
is left untouched so existing encrypted credentials (SMTP password, PeerTube
OAuth, Nextcloud WebDAV) remain decryptable. If the file is missing, the public
site still runs but any path that reads encrypted credentials throws
`RuntimeException: APP_KEY not found`.
### First admin login
+27
View File
@@ -11,6 +11,13 @@ default:
setup:
@bash scripts/setup-dev.sh
# One-shot provisioning for a fresh clone: app/.env + APP_KEY (idempotent, never
# overwrites an existing key), composer deps, npm deps, DB schema + migrations,
# and a first-admin-password check. Prefer this over the stale `setup` recipe.
[group('dev')]
provision:
@bash scripts/provision.sh
# One-shot build of all frontend assets (run before `dev` if sources changed)
[group('dev')]
dev-build:
@@ -484,6 +491,26 @@ deploy-migrate-storage dry_run='' target_host='xamxam':
deploy-all-first: deploy deploy-backup deploy-cleanup-cron deploy-logrotate
# One-shot: full initial deploy including backup and cleanup cron jobs.
# One-shot remote provisioning for a fresh xamxam server.
# Chains: server .env/APP_KEY (idempotent, never overwrites an existing key) →
# full deploy → nginx → backup + cleanup + logrotate, then prints the
# first-admin and service-credential follow-ups.
[group('deploy')]
provision-server:
@bash scripts/provision-server-env.sh xamxam
@just deploy
@just deploy-nginx
@just deploy-backup
@just deploy-cleanup-cron
@just deploy-logrotate
@echo ""
@echo "✅ Server provisioning complete."
@echo ""
@echo "Next, from the server's /admin/account:"
@echo " 1. Set the admin password (fresh DB starts unauthenticated)"
@echo " 2. Configure SMTP + PeerTube credentials (stored encrypted in the DB)"
@echo " and verify Nextcloud sync in /admin"
# ============================================================================
# Testing
# ============================================================================
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Provision (or verify) the server-side .env / APP_KEY on the xamxam host.
#
# Behavior:
# - If remote /var/www/xamxam/.env already contains APP_KEY=... → DO NOT
# overwrite it. Print a clear message and exit 0.
# - If the file exists but has no APP_KEY → append a fresh key.
# - If the file does not exist → create it with a fresh key.
# Always fixes ownership (www-data:xamxam) and perms (640) after any write.
#
# Run from local via: just provision-server
# (uses the `xamxam` SSH host as defined in ~/.ssh/config / deploy recipes)
set -euo pipefail
HOST="${1:-xamxam}"
ENV_PATH="/var/www/xamxam/.env"
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m'
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
info() { printf "${CYAN}→${NC} %s\n" "$*"; }
die() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; }
info "Checking ${ENV_PATH} on ${HOST}…"
# Does the file exist AND already have an APP_KEY value?
if ssh "$HOST" "test -f '$ENV_PATH' && grep -qE '^\s*APP_KEY=\S+' '$ENV_PATH'"; then
ok "APP_KEY already present in ${ENV_PATH} on ${HOST} — NOT overwriting it."
warn "Keeping the existing key so encrypted credentials stay decryptable."
warn "If you intend to rotate the key, use: just reencrypt-password <new_base64_key>"
exit 0
fi
info "APP_KEY absent — generating one on the server…"
# Build the new key value once, server-side.
if ssh "$HOST" "command -v php >/dev/null 2>&1"; then
KEY="$(ssh "$HOST" "php -r 'echo base64_encode(random_bytes(32));'")"
else
die "php not found on ${HOST} — cannot generate APP_KEY remotely."
fi
# Append or create, always via root (app dir is not writable by www-data).
if ssh "$HOST" "test -f '$ENV_PATH'"; then
warn "${ENV_PATH} exists but has no APP_KEY — appending (existing lines untouched)."
ssh "$HOST" "{ printf '\nAPP_KEY=%s\n' '$KEY'; } | sudo tee -a '$ENV_PATH' >/dev/null"
ok "APP_KEY appended to ${ENV_PATH}."
else
ssh "$HOST" "echo 'APP_KEY=${KEY}' | sudo tee '$ENV_PATH' >/dev/null"
info "Created ${ENV_PATH} with a new APP_KEY."
fi
# Normalise ownership + perms regardless of which branch wrote.
ssh "$HOST" "sudo chown www-data:xamxam '$ENV_PATH' && sudo chmod 640 '$ENV_PATH'"
ok "Ownership www-data:xamxam, permissions 640."
ok "Server APP_KEY ready."
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# One-shot provisioning for a fresh XAMXAM clone (local dev).
#
# Steps:
# 1. Ensure app/.env exists with a valid APP_KEY (NEVER overwrites an existing key).
# 2. Install dependencies: composer install + npm ci.
# 3. Create/update the SQLite DB from schema + migrations (just migrate).
# 4. Report the first-admin status (and how to set a password, if unset).
#
# Idempotent: safe to re-run on an already-provisioned clone.
# Run via: just provision
set -euo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m'
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
info() { printf "${CYAN}→${NC} %s\n" "$*"; }
die() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; }
cd "$(cd "$(dirname "$0")/.." && pwd)"
ENV_FILE="app/.env"
# ── 1. APP_KEY / .env ──────────────────────────────────────────────────────────
info "Checking $ENV_FILE for APP_KEY…"
key_exists() {
[ -f "$ENV_FILE" ] && grep -qE '^\s*APP_KEY=\S+' "$ENV_FILE"
}
if [ -f "$ENV_FILE" ] && key_exists; then
ok "APP_KEY already present in $ENV_FILE — NOT overwriting it."
warn "Keeping the existing key so encrypted credentials stay decryptable."
elif [ -f "$ENV_FILE" ]; then
warn "$ENV_FILE exists but has no APP_KEY — appending one (existing lines untouched)."
printf '\nAPP_KEY=%s\n' "$(php -r 'echo base64_encode(random_bytes(32));')" >> "$ENV_FILE"
chmod 600 "$ENV_FILE"
ok "APP_KEY appended to $ENV_FILE."
elif command -v php >/dev/null 2>&1; then
printf 'APP_KEY=%s\n' "$(php -r 'echo base64_encode(random_bytes(32));')" > "$ENV_FILE"
chmod 600 "$ENV_FILE"
ok "Created $ENV_FILE with a new APP_KEY."
else
warn "PHP not found — skipping automatic .env creation."
warn "Create $ENV_FILE manually with: APP_KEY=<base64-encoded 32 random bytes>"
fi
# ── 2. Dependencies ─────────────────────────────────────────────────────────────
info "Installing Composer dependencies…"
command -v composer >/dev/null 2>&1 || die "composer not found on PATH. Install it first."
composer install
info "Installing npm dependencies…"
command -v npm >/dev/null 2>&1 || die "npm not found on PATH. Install it first."
npm ci
# ── 3. Database ─────────────────────────────────────────────────────────────────
info "Creating/updating the SQLite database…"
command -v just >/dev/null 2>&1 || die "just not found on PATH. Install it first (or run scripts/migrate.sh)."
just migrate
# ── 4. First admin password ─────────────────────────────────────────────────────
DB="app/storage/xamxam.db"
if [ -f "$DB" ] && command -v sqlite3 >/dev/null 2>&1; then
HASH_LEN="$(sqlite3 "$DB" "SELECT length(value) FROM site_settings WHERE key='admin_password_hash';" 2>/dev/null || true)"
if [ "$HASH_LEN" = "0" ] || [ -z "$HASH_LEN" ]; then
warn "No admin password is set yet — /admin/ is currently UNAUTHENTICATED (open)."
info "Set one now at http://127.0.0.1:8000/admin/account (or the production admin) to secure it."
else
ok "Admin password is already set."
fi
fi
echo ""
printf "${GREEN}✓${NC} Provisioning complete. Start the dev server with: ${CYAN}just dev${NC}\n"
echo " Public: http://127.0.0.1:8000/ Admin: http://127.0.0.1:8000/admin/"