diff --git a/TODO.md b/TODO.md index 2fff21a..261a9a1 100644 --- a/TODO.md +++ b/TODO.md @@ -40,6 +40,10 @@ ## Admin / Server +- [x] Create `scripts/setup-server.sh` (one-time server setup: group, ownership, setgid 2775 on dirs) +- [x] Add `just setup-server` recipe (rsync + run setup-server.sh on remote) +- [x] Exclude `.claude` and `.pi` from rsync deploy +- [x] Update `docs/SERVER_SETUP.md` with correct permissions rationale and troubleshooting - [ ] Add server status view in admin panel (nginx + php-fpm health, site HTTP check) - [ ] Add server log viewer in admin panel (tail nginx error/access logs via SSH or log endpoint) - [ ] Add nginx config deploy flow to admin panel (upload `scripts/deploy-server.sh`, run remotely) diff --git a/docs/SERVER_SETUP.md b/docs/SERVER_SETUP.md index 00ec6e6..8716f46 100644 --- a/docs/SERVER_SETUP.md +++ b/docs/SERVER_SETUP.md @@ -1,18 +1,42 @@ # Server Setup -## One-time setup on server +## One-time setup (before first deploy) + +Run the setup script on the server. It creates `/var/www/posterg`, sets the +correct ownership/permissions, and adds the deploy user to the `posterg` group: ```bash -ssh posterg -sudo mkdir -p /var/www/posterg -sudo chown www-data:posterg /var/www/posterg -sudo chmod 775 /var/www/posterg -exit +just setup-server ``` -## Deploying the application +What the script does: +- Creates the `posterg` group if it doesn't exist +- Adds both the SSH user (read from `~/.ssh/config` via `ssh -G posterg`) and `www-data` to `posterg` +- Creates `/var/www/posterg` owned by `www-data:posterg` +- Sets all directories to **2775** (`rwxrws r-x`) — the setgid bit ensures + new files/dirs inherit the `posterg` group, which is required for + `rsync --chown=www-data:posterg` to succeed +- Sets files to **664** +- Sets `storage/` to **2775**, database files to **660** -Files are pushed via rsync — there is no repo on the server. +> **Important:** After running `setup-server`, log out and back in on the server +> (or run `newgrp posterg`) so the new group membership is active before deploying. + +### Why setgid (2775) on directories? + +rsync uses `--chown=www-data:posterg` to set ownership on transferred files. +For this to work, the receiving process (running as `padlock`) must have write +permission on every target directory. Without the setgid bit: +- Newly created subdirectories inherit `padlock`'s primary group +- `www-data` (nginx/php-fpm) can't write to them → 403 errors +- `padlock` can't write to dirs owned by `www-data` → rsync Permission denied + +With `2775 + group=posterg`: +- Both `padlock` and `www-data` are in `posterg` → both can write +- New subdirs automatically get `posterg` as their group +- rsync can create files and directories without errors + +## Deploying the application ```bash # Push all app files @@ -24,7 +48,8 @@ just deploy-db ## Applying the nginx config -The config is in `nginx/posterg.conf`. Upload it and run the deploy script on the server: +The config is in `nginx/posterg.conf`. Upload it and run the deploy script on +the server: ```bash rsync -v nginx/posterg.conf posterg:/tmp/posterg.conf @@ -32,8 +57,8 @@ ssh posterg "sudo bash /var/www/posterg/scripts/deploy-server.sh" ssh posterg "sudo systemctl reload nginx" ``` -`scripts/deploy-server.sh` fixes ownership/permissions and installs the nginx config -from `/tmp/posterg.conf`. It must be run as root. +`scripts/deploy-server.sh` fixes ownership/permissions and installs the nginx +config from `/tmp/posterg.conf`. It must be run as root. ## Managing admin users @@ -41,24 +66,51 @@ from `/tmp/posterg.conf`. It must be run as root. ssh posterg "sudo bash /var/www/posterg/scripts/manage-admin-users.sh" ``` -This is an interactive menu for adding, changing, and deleting htpasswd entries -at `/etc/nginx/.htpasswd-posterg`. +Interactive menu for adding, changing, and deleting htpasswd entries at +`/etc/nginx/.htpasswd-posterg`. ## Troubleshooting -### Nginx 403 Forbidden +### rsync: Permission denied on mkdir or mkstemp + +The remote directory permissions are wrong. Run: + +```bash +just setup-server +``` + +Then log out/in on the server and retry `just deploy`. + +If you need to fix it manually (replace `youruser` with your remote username): + +```bash +ssh posterg +sudo DEPLOY_USER=youruser bash /tmp/setup-server.sh +``` + +Or directly: + ```bash ssh posterg sudo chown -R www-data:posterg /var/www/posterg -sudo find /var/www/posterg -type d -exec chmod 755 {} \; -sudo find /var/www/posterg -type f -exec chmod 644 {} \; -sudo chmod 775 /var/www/posterg/storage +sudo find /var/www/posterg -type d -exec chmod 2775 {} \; +sudo find /var/www/posterg -type f -exec chmod 664 {} \; +sudo usermod -aG posterg youruser +``` + +### Nginx 403 Forbidden + +```bash +ssh posterg +sudo find /var/www/posterg -type d -exec chmod 2775 {} \; +sudo find /var/www/posterg -type f -exec chmod 664 {} \; sudo chmod 660 /var/www/posterg/storage/*.db ``` ### Database permission error + ```bash ssh posterg -sudo chown www-data:posterg /var/www/posterg/storage/test.db -sudo chmod 660 /var/www/posterg/storage/test.db +sudo chown www-data:posterg /var/www/posterg/storage/posterg.db +sudo chmod 660 /var/www/posterg/storage/posterg.db ``` diff --git a/justfile b/justfile index 41cf837..cc20b1a 100644 --- a/justfile +++ b/justfile @@ -37,6 +37,8 @@ deploy: --exclude '*.md' \ --exclude '.git*' \ --exclude '.jj' \ + --exclude '.claude' \ + --exclude '.pi' \ --exclude '.DS_Store' \ --exclude 'storage/backup_*' \ --exclude 'storage/fixtures' \ @@ -48,12 +50,12 @@ deploy: --exclude 'var/cache/*' \ --exclude 'var/logs/*' \ ./ posterg:/var/www/posterg/ - ssh posterg "cd /var/www/posterg && \ - mkdir -p var/{cache,logs,tmp} && \ - chown -R www-data:posterg . && \ - chmod -R 755 . && \ - chmod -R 775 var/ storage/ && \ - chmod 660 storage/*.db 2>/dev/null || true" + ssh posterg "mkdir -p /var/www/posterg/var/{cache,logs,tmp}" + +[group('deploy')] +setup-server: + rsync -v scripts/setup-server.sh posterg:/tmp/setup-server.sh + ssh posterg "sudo DEPLOY_USER=$(ssh -G posterg | awk '/^user / {print $2}') bash /tmp/setup-server.sh" [group('deploy')] deploy-db: diff --git a/scripts/setup-server.sh b/scripts/setup-server.sh new file mode 100755 index 0000000..78c7bd3 --- /dev/null +++ b/scripts/setup-server.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# One-time server setup for Post-ERG +# Run this before the first deploy (or after a permission reset). +# +# Usage: ssh posterg "sudo bash /tmp/setup-server.sh" +# Or: just setup-server +# +# What it does: +# 1. Creates /var/www/posterg with correct ownership and permissions +# 2. Ensures the deploy user is in the posterg group +# 3. Sets sticky group bit (setgid) on all directories so new files +# inherit the posterg group — required for rsync --chown to work + +set -e + +# ── Config ──────────────────────────────────────────────────────────────────── +# DEPLOY_USER is passed explicitly by the justfile (read from ~/.ssh/config via +# `ssh -G posterg`). Falls back to $SUDO_USER if run manually with sudo. +DEPLOY_USER="${DEPLOY_USER:-${SUDO_USER}}" +[ -n "$DEPLOY_USER" ] || die "DEPLOY_USER is not set. Pass it explicitly: sudo DEPLOY_USER=youruser bash $0" +APP_DIR="/var/www/posterg" +APP_GROUP="posterg" +WEB_USER="www-data" +# ───────────────────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +ok() { echo -e "${GREEN}✓${NC} $*"; } +warn() { echo -e "${YELLOW}!${NC} $*"; } +die() { echo -e "${RED}✗${NC} $*" >&2; exit 1; } + +[ "$EUID" -eq 0 ] || die "Run as root (sudo)" + +echo "🔧 Post-ERG Server Setup" +echo "========================" +echo "" + +# ── 1. Create posterg group ─────────────────────────────────────────────────── +if ! getent group "$APP_GROUP" >/dev/null; then + groupadd "$APP_GROUP" + ok "Created group: $APP_GROUP" +else + ok "Group already exists: $APP_GROUP" +fi + +# ── 2. Add deploy user and web user to group ────────────────────────────────── +for user in "$DEPLOY_USER" "$WEB_USER"; do + if id "$user" &>/dev/null; then + if ! id -nG "$user" | grep -qw "$APP_GROUP"; then + usermod -aG "$APP_GROUP" "$user" + ok "Added $user to $APP_GROUP" + else + ok "$user already in $APP_GROUP" + fi + else + warn "User $user not found — skipping" + fi +done + +# ── 3. Create app directory ─────────────────────────────────────────────────── +mkdir -p "$APP_DIR" +ok "Ensured $APP_DIR exists" + +# ── 4. Set ownership ────────────────────────────────────────────────────────── +chown -R "$WEB_USER:$APP_GROUP" "$APP_DIR" +ok "Ownership: $WEB_USER:$APP_GROUP on $APP_DIR" + +# ── 5. Set directory permissions with setgid ────────────────────────────────── +# 2775 = rwxrwsr-x +# - owner (www-data) and group (posterg) can read/write/execute +# - setgid bit ensures new files/dirs inherit the posterg group +# - this is what allows rsync --chown=www-data:posterg to succeed +find "$APP_DIR" -type d -exec chmod 2775 {} \; +ok "Directories: 2775 (setgid) on $APP_DIR/**" + +# ── 6. Set file permissions ─────────────────────────────────────────────────── +find "$APP_DIR" -type f -exec chmod 664 {} \; +ok "Files: 664 on $APP_DIR/**" + +# ── 7. Tighten storage ─────────────────────────────────────────────────────── +if [ -d "$APP_DIR/storage" ]; then + chmod 2775 "$APP_DIR/storage" + find "$APP_DIR/storage" -name "*.db" -exec chmod 660 {} \; + ok "Storage: 2775, databases: 660" +fi + +echo "" +echo -e "${GREEN}✓ Setup complete.${NC}" +echo "" +echo "Next steps:" +echo " 1. Log out and back in as '$DEPLOY_USER' so group membership takes effect" +echo " (or run: newgrp $APP_GROUP)" +echo " 2. Run: just deploy" +echo "" +warn "If this is a fresh server, also run after first deploy:" +echo " just deploy-db # push initial database" +echo " just deploy-nginx # apply nginx config"