mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
feat(provision): idempotent setup for local dev and remote server
This commit is contained in:
+35
-17
@@ -19,38 +19,56 @@ Deployment is orchestrated through the `justfile` (`deploy` group).
|
|||||||
|
|
||||||
## One-time server setup
|
## One-time server setup
|
||||||
|
|
||||||
|
From a machine with `ssh` access to the `xamxam` host, run the full remote
|
||||||
|
provisioning once:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh xamxam
|
just provision-server
|
||||||
sudo mkdir -p /var/www/xamxam
|
|
||||||
sudo chown www-data:xamxam /var/www/xamxam
|
|
||||||
sudo chmod 775 /var/www/xamxam
|
|
||||||
exit
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```bash
|
||||||
just deploy
|
just deploy
|
||||||
just deploy-nginx
|
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
|
```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
|
`just provision-server` (via `scripts/provision-server-env.sh`) ensures the
|
||||||
**not** rely on a committed `.env` — generate a fresh key per environment and put
|
server has an `APP_KEY` in `/var/www/xamxam/.env`. It is **idempotent** and will
|
||||||
it on the server before first `deploy`: create `app/.env` locally (see
|
**never overwrite an existing key**:
|
||||||
[development.md](development.md#appkey-and-the-appenv-file)) so `deploy-env`
|
|
||||||
uploads it, or create `/var/www/xamxam/.env` on the server directly:
|
|
||||||
|
|
||||||
```bash
|
- If `/var/www/xamxam/.env` already has an `APP_KEY=…` → it is left untouched
|
||||||
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"'
|
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
|
If you ever rotate `APP_KEY`, re-encrypt the SMTP password with
|
||||||
`just reencrypt-password <new_base64_key>` and push the new key via
|
`just reencrypt-password <new_base64_key>` and push the new key via
|
||||||
|
|||||||
+25
-9
@@ -13,8 +13,23 @@ Setup, workflow, building assets, and testing for XAMXAM development.
|
|||||||
|
|
||||||
## One-time setup
|
## One-time setup
|
||||||
|
|
||||||
From the repo root, install dependencies (manually — there is no `just` recipe
|
From the repo root, run the provisioning script:
|
||||||
for these):
|
|
||||||
|
```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
|
```bash
|
||||||
composer install # PHP deps (vendor/)
|
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
|
— it still clones the old `php-live-reload` library and creates legacy
|
||||||
`admin/data/` directories. Live-reload now ships inside the app
|
`admin/data/` directories. Live-reload now ships inside the app
|
||||||
(`app/public/live-reload.php`), and assets are built with rolldown/lightningcss,
|
(`app/public/live-reload.php`), and assets are built with rolldown/lightningcss,
|
||||||
not the live-reload watcher. Prefer the explicit `composer install` + `npm ci`
|
not the live-reload watcher. Prefer `just provision`.
|
||||||
above.
|
|
||||||
|
|
||||||
### APP_KEY and the app/.env file
|
### 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>
|
APP_KEY=<base64-encoded 32 random bytes>
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate one with:
|
Generate one manually with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php -r 'echo "APP_KEY=" . base64_encode(random_bytes(32)) . PHP_EOL;' > app/.env
|
php -r 'echo "APP_KEY=" . base64_encode(random_bytes(32)) . PHP_EOL;' > app/.env
|
||||||
chmod 600 app/.env
|
chmod 600 app/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
`app/.env` must **never** be committed. If the file is missing, the public site
|
`app/.env` must **never** be committed. `just provision` only creates `app/.env`
|
||||||
still runs but any path that reads encrypted credentials throws
|
(or appends `APP_KEY`) when the key is absent — if the value already exists it
|
||||||
`RuntimeException: APP_KEY not found`. On a fresh clone, create it before
|
is left untouched so existing encrypted credentials (SMTP password, PeerTube
|
||||||
relying on SMTP/PeerTube/Nextcloud features.
|
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
|
### First admin login
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ default:
|
|||||||
setup:
|
setup:
|
||||||
@bash scripts/setup-dev.sh
|
@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)
|
# One-shot build of all frontend assets (run before `dev` if sources changed)
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
dev-build:
|
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
|
deploy-all-first: deploy deploy-backup deploy-cleanup-cron deploy-logrotate
|
||||||
# One-shot: full initial deploy including backup and cleanup cron jobs.
|
# 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
|
# Testing
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
Executable
+58
@@ -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."
|
||||||
Executable
+76
@@ -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/"
|
||||||
Reference in New Issue
Block a user