From ca9d01e859b162cc9bdcccad5c6724d3bb4e8fe3 Mon Sep 17 00:00:00 2001 From: Chad Stovern Date: Fri, 31 Jul 2026 06:41:45 -0400 Subject: [PATCH] Add FreeBSD production deploy docs and scripts. Enable home-server hosting with the same user/rc.d/nginx/backup pattern as dothisweek, plus optional HOST bind for Jetty behind the reverse proxy. Co-authored-by: Cursor --- .env.example | 27 +++ .gitignore | 1 + DEPLOYMENT_FREEBSD.md | 300 +++++++++++++++++++++++++++++++ README.org | 9 +- scripts/backup-setup.sh | 92 ++++++++++ scripts/build-prod.sh | 94 ++++++++++ scripts/create-env.sh | 76 ++++++++ scripts/create-service.sh | 42 +++++ scripts/selfdestruct.rc.template | 85 +++++++++ scripts/setup-deploy-key.sh | 33 ++++ scripts/setup-nginx.sh | 75 ++++++++ scripts/start-app.sh | 38 ++++ scripts/update-restart.sh | 12 ++ src/self_destruct/core.clj | 23 ++- 14 files changed, 900 insertions(+), 7 deletions(-) create mode 100644 .env.example create mode 100644 DEPLOYMENT_FREEBSD.md create mode 100755 scripts/backup-setup.sh create mode 100755 scripts/build-prod.sh create mode 100755 scripts/create-env.sh create mode 100755 scripts/create-service.sh create mode 100644 scripts/selfdestruct.rc.template create mode 100755 scripts/setup-deploy-key.sh create mode 100755 scripts/setup-nginx.sh create mode 100755 scripts/start-app.sh create mode 100755 scripts/update-restart.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c4c6a99 --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +# Database (JDBC URL used by the app) +DATABASE_URL=jdbc:postgresql://localhost:5432/selfdestruct_prod?user=selfdestruct&password=your_secure_password_here + +# Also kept for pg_dump backups (must match DATABASE_URL) +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=selfdestruct_prod +DB_USER=selfdestruct +DB_PASSWORD=your_secure_password_here + +# Server +PORT=4003 +HOST=127.0.0.1 + +# Secrets (SESSION_COOKIE_KEY must be exactly 16 characters/bytes) +DATABASE_ENCRYPTION_KEY=change-me-to-a-long-random-secret +SESSION_COOKIE_KEY=changecookiekey1 + +# Workers (message expiry purge) +ENABLE_WORKERS=true +MESSAGE_EXPIRE_MINUTES=1440 +WORKER_DELAY_SECONDS=3600 + +# Logging +REPORTED_LOG_LEVEL=warn +LOG_APPENDER=println +# SENTRY_DSN= only needed when LOG_APPENDER=sentry diff --git a/.gitignore b/.gitignore index 7b500e5..7e99d42 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pom.xml pom.xml.asc profiles.clj +.env diff --git a/DEPLOYMENT_FREEBSD.md b/DEPLOYMENT_FREEBSD.md new file mode 100644 index 0000000..234dc00 --- /dev/null +++ b/DEPLOYMENT_FREEBSD.md @@ -0,0 +1,300 @@ +# self-destruct FreeBSD Deployment + +Deploy self-destruct on a FreeBSD home server using the same pattern as +DoThisWeek: dedicated app user, PostgreSQL, `.env`, Leiningen uberjar, `rc.d` +service, nginx reverse proxy, certbot, and daily DB backups. + +## Assumptions + +- PostgreSQL, nginx, certbot, firewall rules, and (optionally) Cloudflare DDNS + are already available on the server. +- `sudo` is available. +- The server has outbound HTTPS for GitHub, Maven/Clojars, Cloudflare, and + Let's Encrypt. +- App path is `/home/selfdestruct/self-destruct`. +- App user is `selfdestruct`. +- App port is `4003` (DoThisWeek uses `4002`). + +## 1. Install Java Runtime and Build Tools + +```sh +sudo pkg install openjdk21 git +``` + +Install Leiningen if it is not already on the system (e.g. `sudo pkg install +leiningen`, or place a `lein` script on `PATH`). + +Verify: + +```sh +/usr/local/openjdk21/bin/java -version +lein version +``` + +The deployment scripts force `JAVA_HOME=/usr/local/openjdk21` and put +`/usr/local/openjdk21/bin` first in `PATH` so FreeBSD's `javavm` wrapper does +not accidentally select an older Java runtime. + +## 2. Create App User and Clone Repo + +```sh +sudo pw user add selfdestruct -m -s /bin/sh -c "self-destruct Application" +``` + +If the repo is private, create a deploy key before cloning: + +```sh +sudo -u selfdestruct -H sh +mkdir -p ~/.ssh +chmod 700 ~/.ssh +ssh-keygen -t ed25519 -C "selfdestruct-server@$(hostname)" -f ~/.ssh/id_ed25519 -N "" +cat ~/.ssh/id_ed25519.pub +exit +``` + +Add the printed public key as a read-only deploy key in GitHub +(Repository Settings → Deploy keys), then test and clone: + +```sh +sudo -u selfdestruct ssh -T git@github.com +sudo -u selfdestruct git clone git@github.com:chadhs/self-destruct.git /home/selfdestruct/self-destruct +``` + +After the clone, you can regenerate or reprint the key with +`./scripts/setup-deploy-key.sh` as the `selfdestruct` user. + +## 3. Create PostgreSQL Database + +```sh +sudo -u postgres createuser selfdestruct +sudo -u postgres createdb -O selfdestruct selfdestruct_prod +sudo -u postgres psql -c "ALTER USER selfdestruct WITH PASSWORD 'YOUR_SECURE_PASSWORD';" +``` + +Verify the app user can connect: + +```sh +psql -U selfdestruct -h localhost selfdestruct_prod +``` + +If needed, update `pg_hba.conf` to allow `md5` or `scram-sha-256` +authentication for localhost connections, matching the other deployed apps. + +## 4. Create Production Environment + +Run this as the app user from the repo root: + +```sh +sudo -u selfdestruct sh -c 'cd /home/selfdestruct/self-destruct && ./scripts/create-env.sh' +``` + +The script prompts for: + +- Database password for the `selfdestruct` PostgreSQL user +- Domain name +- App port (default `4003`) + +It writes `/home/selfdestruct/self-destruct/.env` with mode `600`, including +generated `SESSION_COOKIE_KEY` (16 hex chars) and `DATABASE_ENCRYPTION_KEY`. + +See [`.env.example`](.env.example) for the full variable list. + +## 5. Build and Migrate + +Run the production build as the app user: + +```sh +sudo -u selfdestruct sh -c 'cd /home/selfdestruct/self-destruct && ./scripts/build-prod.sh' +``` + +This: + +- Fetches dependencies with `lein deps` +- Builds `target/self-destruct.jar` with `lein uberjar` +- Runs migrations with `java -jar target/self-destruct.jar --migrate` + +Optional local server test from the FreeBSD host: + +```sh +sudo -u selfdestruct sh -c 'cd /home/selfdestruct/self-destruct && ./scripts/start-app.sh' +``` + +From another shell on the server: + +```sh +curl -s http://127.0.0.1:4003/ | head -20 +``` + +Do not test with `http://SERVER_IP:4003`. Production `.env` sets `HOST=127.0.0.1`, +so external browser traffic should go through nginx. + +## 6. Install and Start rc.d Service + +Run as root: + +```sh +sudo /home/selfdestruct/self-destruct/scripts/create-service.sh +sudo sysrc selfdestruct_enable="YES" +sudo service selfdestruct start +``` + +Verify: + +```sh +sudo service selfdestruct status +curl -s http://127.0.0.1:4003/ | head -20 +sudo tail -f /var/log/selfdestruct.log +``` + +## 7. Configure Nginx + +Run as root: + +```sh +sudo /home/selfdestruct/self-destruct/scripts/setup-nginx.sh +``` + +Enter your domain when prompted. The script creates a site config that proxies +the apex and `www` hostnames to `127.0.0.1:4003`. + +Test and reload nginx: + +```sh +sudo service nginx configtest +sudo service nginx reload +``` + +If nginx is not already enabled: + +```sh +sudo sysrc nginx_enable="YES" +sudo service nginx start +``` + +## 8. Configure DNS and Cloudflare DDNS + +In Cloudflare (or your DNS provider): + +- Create an A record for `@` pointing to the server public IP +- Prefer a CNAME for `www` pointing at the apex hostname +- Keep records DNS-only unless you intentionally want a CDN proxy + +If you already run a shared Cloudflare DDNS script on this host, add another +zone entry for this domain (same pattern as DoThisWeek / other apps). Example: + +```sh +sudo sysrc cloudflare_ddns_zoneN_id="YOUR_ZONE_ID" +sudo sysrc cloudflare_ddns_zoneN_record_id="YOUR_RECORD_ID" +sudo sysrc cloudflare_ddns_zoneN_name="your.domain.example" +``` + +Then update the DDNS script to call `process_zone` for the new zone and verify: + +```sh +sudo /usr/local/bin/cloudflare-ddns.sh +tail /var/log/cloudflare-ddns.log +``` + +## 9. Configure HTTPS + +Once DNS points at the server: + +```sh +dig your.domain.example +dig www.your.domain.example +sudo certbot --nginx -d your.domain.example -d www.your.domain.example +``` + +Verify HTTPS: + +```sh +curl -I https://your.domain.example +``` + +Certbot renewal should already be configured if other apps use it. Verify: + +```sh +sudo certbot renew --dry-run +``` + +## 10. Configure Database Backups + +Run as root: + +```sh +sudo /home/selfdestruct/self-destruct/scripts/backup-setup.sh +``` + +This creates: + +- Backup directory: `/home/selfdestruct/backups/` +- Backup script: `/home/selfdestruct/backup-db.sh` +- Cron job: daily at 3:45 AM +- Log file: `/var/log/selfdestruct-backup.log` +- 30-day backup retention + +The generated backup script loads `.env` and uses `DB_PASSWORD` for `pg_dump`. + +Test manually: + +```sh +sudo -u selfdestruct /home/selfdestruct/backup-db.sh +ls -la /home/selfdestruct/backups/ +tail /var/log/selfdestruct-backup.log +``` + +## Future Deployments + +Run as root: + +```sh +sudo /home/selfdestruct/self-destruct/scripts/update-restart.sh +``` + +This runs `git pull`, rebuilds the uberjar, runs migrations, and restarts the +service. + +Schema changes should be backward-compatible with the currently running app, +because `update-restart.sh` runs migrations before restarting the service. + +## Troubleshooting + +If the build fails with an error like: + +```text +has been compiled by a more recent version of the Java Runtime +this version of the Java Runtime only recognizes class file versions up to 52.0 +``` + +the build is running under Java 8. Confirm OpenJDK 21 is installed: + +```sh +/usr/local/openjdk21/bin/java -version +``` + +Then pull the latest scripts and rerun `./scripts/build-prod.sh`. The production +scripts explicitly use `/usr/local/openjdk21/bin/java`. + +Health check (once HTTPS is up): + +```sh +curl -s https://your.domain.example/health +``` + +## Quick Reference + +| Item | Value | +|------|-------| +| App port | 4003 | +| DB name | selfdestruct_prod | +| DB user | selfdestruct | +| System user | selfdestruct | +| App directory | /home/selfdestruct/self-destruct | +| Uberjar | target/self-destruct.jar | +| Service name | selfdestruct | +| App log | /var/log/selfdestruct.log | +| Backup log | /var/log/selfdestruct-backup.log | +| Backup time | 3:45 AM daily | +| Backup retention | 30 days | +| Build | `lein uberjar` | +| Migrate | `java -jar target/self-destruct.jar --migrate` | diff --git a/README.org b/README.org index b139a04..44adaea 100644 --- a/README.org +++ b/README.org @@ -81,12 +81,15 @@ - Migratus handles schema changes; files live in ~resources/migrations~. - Create a migration with ~lein migratus create ~, then edit the ~up~ / ~down~ SQL. - Apply migrations with ~lein migrate~ (runs the app ~--migrate~ CLI) or - ~java -jar self-destruct.jar --migrate~ (production / Heroku release phase). + ~java -jar target/self-destruct.jar --migrate~ in production. ** Production Setup - Required environment variables: + Primary target is a FreeBSD home server. See [[file:DEPLOYMENT_FREEBSD.md][DEPLOYMENT_FREEBSD.md]] + for install, ~rc.d~, nginx, certbot, and backup scripts under ~scripts/~. + + Required environment variables (also listed in [[file:.env.example][.env.example]]): - ~DATABASE_URL~ - ~DATABASE_ENCRYPTION_KEY~ @@ -95,12 +98,14 @@ Optional: + - ~HOST~ (e.g. ~127.0.0.1~ so Jetty only accepts local/nginx traffic) - ~REPORTED_LOG_LEVEL~ (defaults to ~warn~; min level for Sentry when enabled) - ~LOG_APPENDER~ (~println~ or ~sentry~) - ~SENTRY_DSN~ (required only when ~LOG_APPENDER=sentry~) - ~ENABLE_WORKERS~ (defaults to enabled; set ~false~ to disable background workers) - ~MESSAGE_EXPIRE_MINUTES~ (defaults to ~1440~ / 24 hours) - ~WORKER_DELAY_SECONDS~ (defaults to ~3600~ / 1 hour) + - ~SECURE_DEFAULTS~ (set ~true~ to use Ring ~secure-site-defaults~) ** License diff --git a/scripts/backup-setup.sh b/scripts/backup-setup.sh new file mode 100755 index 0000000..c106cd1 --- /dev/null +++ b/scripts/backup-setup.sh @@ -0,0 +1,92 @@ +#!/bin/sh + +echo "Setting up database backups..." + +# Check if running as root +if [ "$(id -u)" != "0" ]; then + echo "Error: This script must be run as root (use sudo)" + exit 1 +fi + +BACKUP_DIR="/home/selfdestruct/backups" +mkdir -p ${BACKUP_DIR} +chown selfdestruct:selfdestruct ${BACKUP_DIR} +chmod 750 ${BACKUP_DIR} + +cat > /home/selfdestruct/backup-db.sh << 'EOF' +#!/bin/sh + +# Configuration +DB_NAME="selfdestruct_prod" +DB_USER="selfdestruct" +ENV_FILE="/home/selfdestruct/self-destruct/.env" +BACKUP_DIR="/home/selfdestruct/backups" +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="${BACKUP_DIR}/selfdestruct_backup_${DATE}.sql" + +if [ -f "${ENV_FILE}" ]; then + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|'#'*) continue ;; + *'='*) + key="${line%%=*}" + value="${line#*=}" + if echo "$key" | grep -q '^[A-Za-z_][A-Za-z0-9_]*$'; then + export "$key=$value" + fi + ;; + esac + done < "${ENV_FILE}" +fi + +if [ -n "${DB_PASSWORD}" ]; then + export PGPASSWORD="${DB_PASSWORD}" +fi + +echo "Creating backup: ${BACKUP_FILE}" +pg_dump -U ${DB_USER} -h localhost ${DB_NAME} > ${BACKUP_FILE} + +if [ $? -eq 0 ]; then + echo "Backup created successfully: ${BACKUP_FILE}" + + gzip ${BACKUP_FILE} + echo "Backup compressed: ${BACKUP_FILE}.gz" + + find ${BACKUP_DIR} -name "selfdestruct_backup_*.sql.gz" -mtime +30 -delete + echo "Cleaned up backups older than 30 days" +else + echo "Backup failed" + exit 1 +fi +EOF + +chmod +x /home/selfdestruct/backup-db.sh +chown selfdestruct:selfdestruct /home/selfdestruct/backup-db.sh + +echo "Setting up daily backup cron job..." +CRON_LINE="45 3 * * * /home/selfdestruct/backup-db.sh >> /var/log/selfdestruct-backup.log 2>&1" + +if ! crontab -u selfdestruct -l 2>/dev/null | grep -q "backup-db.sh"; then + (crontab -u selfdestruct -l 2>/dev/null; echo "${CRON_LINE}") | crontab -u selfdestruct - + echo "Daily backup cron job added for user 'selfdestruct'" +else + echo "Daily backup cron job already exists for user 'selfdestruct'" +fi + +touch /var/log/selfdestruct-backup.log +chown selfdestruct:selfdestruct /var/log/selfdestruct-backup.log +chmod 644 /var/log/selfdestruct-backup.log + +echo "" +echo "Database backup system configured:" +echo " Backup directory: ${BACKUP_DIR}" +echo " Backup script: /home/selfdestruct/backup-db.sh" +echo " Schedule: Daily at 3:45 AM" +echo " Log file: /var/log/selfdestruct-backup.log" +echo " Retention: 30 days" +echo "" +echo "To run a manual backup:" +echo " sudo -u selfdestruct /home/selfdestruct/backup-db.sh" +echo "" +echo "To view backup logs:" +echo " tail -f /var/log/selfdestruct-backup.log" diff --git a/scripts/build-prod.sh b/scripts/build-prod.sh new file mode 100755 index 0000000..ab488fd --- /dev/null +++ b/scripts/build-prod.sh @@ -0,0 +1,94 @@ +#!/bin/sh + +echo "Building self-destruct for production..." + +# Ensure Java 21 is on PATH. FreeBSD's /usr/local/bin/java can be the javavm +# wrapper, so put OpenJDK 21 first. +export JAVA_HOME="${JAVA_HOME:-/usr/local/openjdk21}" +export PATH="${JAVA_HOME}/bin:/usr/local/bin:$PATH" + +if [ ! -x "${JAVA_HOME}/bin/java" ]; then + echo "Error: Java 21 not found at ${JAVA_HOME}/bin/java" + echo "Install it with: sudo pkg install openjdk21" + exit 1 +fi + +if ! "${JAVA_HOME}/bin/java" -version 2>&1 | grep -q 'version "21'; then + echo "Error: ${JAVA_HOME}/bin/java is not Java 21" + "${JAVA_HOME}/bin/java" -version + exit 1 +fi + +if [ ! -f "project.clj" ]; then + echo "Error: Please run this script from the self-destruct project root directory" + exit 1 +fi + +if [ ! -f ".env" ]; then + echo "Error: .env file not found. Please run scripts/create-env.sh first" + exit 1 +fi + +if ! command -v lein >/dev/null 2>&1; then + echo "Error: lein not found on PATH" + echo "Install Leiningen (e.g. sudo pkg install leiningen) or place lein in PATH" + exit 1 +fi + +echo "Loading environment variables..." +while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|'#'*) continue ;; + *'='*) + key="${line%%=*}" + value="${line#*=}" + if echo "$key" | grep -q '^[A-Za-z_][A-Za-z0-9_]*$'; then + export "$key=$value" + echo " Loaded: $key" + else + echo " Skipped invalid key: $key" + fi + ;; + *) + echo " Skipped invalid line: $line" + ;; + esac +done < .env + +echo "Fetching Clojure dependencies..." +lein deps +if [ $? -ne 0 ]; then + echo "Error: Failed to fetch dependencies" + exit 1 +fi + +echo "Building uberjar..." +lein uberjar +if [ $? -ne 0 ]; then + echo "Error: Failed to build uberjar" + exit 1 +fi + +if [ ! -f "target/self-destruct.jar" ]; then + echo "Error: target/self-destruct.jar was not created" + exit 1 +fi + +echo "Running database migrations..." +"${JAVA_HOME}/bin/java" -jar target/self-destruct.jar --migrate +if [ $? -ne 0 ]; then + echo "Error: Failed to run database migrations" + exit 1 +fi + +echo "Production build completed successfully!" +echo "" +echo "Build summary:" +echo " - Dependencies fetched" +echo " - Uberjar built: target/self-destruct.jar" +echo " - Database migrations applied" +echo "" +echo "Next steps:" +echo " 1. Restart the service: sudo service selfdestruct restart" +echo " 2. Check status: sudo service selfdestruct status" +echo " 3. Monitor logs: sudo tail -f /var/log/selfdestruct.log" diff --git a/scripts/create-env.sh b/scripts/create-env.sh new file mode 100755 index 0000000..dcfd38d --- /dev/null +++ b/scripts/create-env.sh @@ -0,0 +1,76 @@ +#!/bin/sh + +echo "Creating production environment file..." + +# Check if we're in the right directory +if [ ! -f "project.clj" ]; then + echo "Error: Please run this script from the self-destruct project root directory" + exit 1 +fi + +echo "Please provide the following information:" + +printf "Database password for 'selfdestruct' user: " +stty -echo +read DB_PASSWORD +stty echo +echo + +printf "Domain name (e.g., selfdestruct.example.com): " +read DOMAIN + +printf "App port [4003]: " +read PORT +PORT=${PORT:-4003} + +# Generate secrets +echo "Generating secrets..." +SESSION_COOKIE_KEY=$(openssl rand -hex 8) +DATABASE_ENCRYPTION_KEY=$(openssl rand -hex 32) + +DB_NAME="selfdestruct_prod" +DB_USER="selfdestruct" +DB_HOST="localhost" +DB_PORT="5432" +DATABASE_URL="jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?user=${DB_USER}&password=${DB_PASSWORD}" + +cat > .env << EOF +# Database Configuration +DATABASE_URL=${DATABASE_URL} +DB_HOST=${DB_HOST} +DB_PORT=${DB_PORT} +DB_NAME=${DB_NAME} +DB_USER=${DB_USER} +DB_PASSWORD=${DB_PASSWORD} + +# Server Configuration +PORT=${PORT} +HOST=127.0.0.1 + +# Secrets +DATABASE_ENCRYPTION_KEY=${DATABASE_ENCRYPTION_KEY} +SESSION_COOKIE_KEY=${SESSION_COOKIE_KEY} + +# Workers +ENABLE_WORKERS=true +MESSAGE_EXPIRE_MINUTES=1440 +WORKER_DELAY_SECONDS=3600 + +# Logging +REPORTED_LOG_LEVEL=warn +LOG_APPENDER=println + +# Optional: Ring secure-site-defaults when terminating TLS at nginx +# SECURE_DEFAULTS=true + +# Optional reference (not read by the app) +APP_BASE_URL=https://${DOMAIN} +EOF + +chmod 600 .env + +echo "Environment file created: .env" +echo "File permissions set to 600 (owner read/write only)" +echo "" +echo "Generated SESSION_COOKIE_KEY and DATABASE_ENCRYPTION_KEY." +echo "You can edit .env manually to adjust logging or worker settings." diff --git a/scripts/create-service.sh b/scripts/create-service.sh new file mode 100755 index 0000000..5a33ea6 --- /dev/null +++ b/scripts/create-service.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +echo "Creating FreeBSD rc.d service script..." + +# Check if running as root +if [ "$(id -u)" != "0" ]; then + echo "Error: This script must be run as root (use sudo)" + exit 1 +fi + +APP_PATH="/home/selfdestruct/self-destruct" + +if [ ! -d "$APP_PATH" ]; then + echo "Error: Application directory not found: $APP_PATH" + echo " Make sure the self-destruct application is deployed first" + exit 1 +fi + +echo "Creating service script..." + +if [ -f "$APP_PATH/scripts/selfdestruct.rc.template" ]; then + cp "$APP_PATH/scripts/selfdestruct.rc.template" /usr/local/etc/rc.d/selfdestruct +else + echo "Error: rc.d template not found: $APP_PATH/scripts/selfdestruct.rc.template" + exit 1 +fi + +chmod +x /usr/local/etc/rc.d/selfdestruct + +touch /var/log/selfdestruct.log +chown selfdestruct:selfdestruct /var/log/selfdestruct.log +chmod 644 /var/log/selfdestruct.log + +echo "Service script created: /usr/local/etc/rc.d/selfdestruct" +echo "Log file created: /var/log/selfdestruct.log" +echo "" +echo "To enable and start the service:" +echo " sysrc selfdestruct_enable=\"YES\"" +echo " service selfdestruct start" +echo "" +echo "To check service status:" +echo " service selfdestruct status" diff --git a/scripts/selfdestruct.rc.template b/scripts/selfdestruct.rc.template new file mode 100644 index 0000000..e156563 --- /dev/null +++ b/scripts/selfdestruct.rc.template @@ -0,0 +1,85 @@ +#!/bin/sh +# +# PROVIDE: selfdestruct +# REQUIRE: LOGIN postgresql +# KEYWORD: shutdown +# +# Add the following lines to /etc/rc.conf to enable selfdestruct: +# selfdestruct_enable="YES" +# + +. /etc/rc.subr + +name="selfdestruct" +rcvar="selfdestruct_enable" + +user="selfdestruct" +group="selfdestruct" + +selfdestruct_chdir="/home/selfdestruct/self-destruct" +selfdestruct_env="LANG=en_US.UTF-8" + +pidfile="/var/run/selfdestruct.pid" +logfile="/var/log/selfdestruct.log" + +start_cmd="selfdestruct_start" +stop_cmd="selfdestruct_stop" +status_cmd="selfdestruct_status" + +selfdestruct_start() +{ + echo "Starting ${name}." + + if [ ! -f /home/selfdestruct/self-destruct/scripts/start-app.sh ]; then + echo "Error: start-app.sh script not found" + return 1 + fi + + su ${user} -c "cd /home/selfdestruct/self-destruct && nohup ./scripts/start-app.sh > ${logfile} 2>&1 &" + + sleep 5 + + PID=$(pgrep -f "java.*self-destruct.jar") + if [ -n "$PID" ]; then + echo "$PID" > ${pidfile} + echo "${name} started with PID $PID." + else + echo "Failed to start ${name}." + return 1 + fi +} + +selfdestruct_stop() +{ + if [ -f ${pidfile} ]; then + PID=$(cat ${pidfile}) + echo "Stopping ${name} (PID: $PID)." + kill $PID + rm -f ${pidfile} + else + echo "${name} is not running (no pidfile found)." + PID=$(pgrep -f "java.*self-destruct.jar") + if [ -n "$PID" ]; then + echo "Found orphaned ${name} process (PID: $PID), killing it." + kill $PID + fi + fi +} + +selfdestruct_status() +{ + if [ -f ${pidfile} ]; then + PID=$(cat ${pidfile}) + if kill -0 $PID 2>/dev/null; then + echo "${name} is running as pid $PID." + else + echo "${name} is not running (stale pidfile)." + rm -f ${pidfile} + fi + else + echo "${name} is not running." + fi +} + +load_rc_config $name +run_rc_command "$1" diff --git a/scripts/setup-deploy-key.sh b/scripts/setup-deploy-key.sh new file mode 100755 index 0000000..4500049 --- /dev/null +++ b/scripts/setup-deploy-key.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +echo "Setting up SSH deploy key for repository access..." + +# This script should be run as the selfdestruct user +if [ "$(whoami)" != "selfdestruct" ]; then + echo "Error: This script must be run as the selfdestruct user" + echo " Run: sudo -u selfdestruct -H sh" + echo " Then: ./scripts/setup-deploy-key.sh" + exit 1 +fi + +mkdir -p ~/.ssh +chmod 700 ~/.ssh + +if [ -f ~/.ssh/id_ed25519 ]; then + echo "SSH key already exists at ~/.ssh/id_ed25519" +else + ssh-keygen -t ed25519 -C "selfdestruct-server@$(hostname)" -f ~/.ssh/id_ed25519 -N "" + echo "SSH key generated." +fi + +echo "" +echo "Add this public key as a read-only deploy key in GitHub:" +echo " Settings > Deploy keys (or repository Settings > Deploy keys)" +echo "" +cat ~/.ssh/id_ed25519.pub +echo "" +echo "After adding the key, test the connection:" +echo " ssh -T git@github.com" +echo "" +echo "Then clone the repository:" +echo " git clone git@github.com:chadhs/self-destruct.git /home/selfdestruct/self-destruct" diff --git a/scripts/setup-nginx.sh b/scripts/setup-nginx.sh new file mode 100755 index 0000000..0f2c4ca --- /dev/null +++ b/scripts/setup-nginx.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +echo "Setting up Nginx configuration..." + +# Check if running as root +if [ "$(id -u)" != "0" ]; then + echo "Error: This script must be run as root (use sudo)" + exit 1 +fi + +echo "Please provide the following information:" +printf "Domain name (e.g., selfdestruct.example.com): " +read DOMAIN + +if [ -z "$DOMAIN" ]; then + echo "Error: Domain name is required" + exit 1 +fi + +WWW_DOMAIN="www.${DOMAIN}" + +echo "Creating Nginx site configuration..." +mkdir -p /usr/local/etc/nginx/sites-available + +cat > /usr/local/etc/nginx/sites-available/selfdestruct << EOF +server { + listen 80; + server_name ${DOMAIN} ${WWW_DOMAIN}; + + # Serve Let's Encrypt challenges directly + location /.well-known/acme-challenge/ { + root /usr/local/www/nginx; + } + + # Proxy everything else to self-destruct + location / { + proxy_pass http://127.0.0.1:4003; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_cache_bypass \$http_upgrade; + } +} +EOF + +mkdir -p /usr/local/etc/nginx/sites-enabled +mkdir -p /usr/local/www/nginx/.well-known/acme-challenge + +chown -R www:www /usr/local/www/nginx +chmod -R 755 /usr/local/www/nginx + +ln -sf /usr/local/etc/nginx/sites-available/selfdestruct /usr/local/etc/nginx/sites-enabled/ + +if ! grep -q "include.*sites-enabled" /usr/local/etc/nginx/nginx.conf; then + echo "Adding sites-enabled include to nginx.conf..." + sed -i '' '/http {/a\ + include /usr/local/etc/nginx/sites-enabled/*;' /usr/local/etc/nginx/nginx.conf +else + echo "sites-enabled already included in nginx.conf" +fi + +echo "Nginx configuration created for domains: $DOMAIN and $WWW_DOMAIN" +echo "Site configuration: /usr/local/etc/nginx/sites-available/selfdestruct" +echo "Site enabled: /usr/local/etc/nginx/sites-enabled/selfdestruct" +echo "" +echo "To test and reload Nginx:" +echo " service nginx configtest" +echo " service nginx reload" +echo "" +echo "After DNS is pointed at this server, set up SSL with certbot:" +echo " sudo certbot --nginx -d $DOMAIN -d $WWW_DOMAIN" diff --git a/scripts/start-app.sh b/scripts/start-app.sh new file mode 100755 index 0000000..6cd70ee --- /dev/null +++ b/scripts/start-app.sh @@ -0,0 +1,38 @@ +#!/bin/sh +cd /home/selfdestruct/self-destruct || exit 1 + +# Load environment variables +if [ -f .env ]; then + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|'#'*) continue ;; + *'='*) + key="${line%%=*}" + value="${line#*=}" + if echo "$key" | grep -q '^[A-Za-z_][A-Za-z0-9_]*$'; then + export "$key=$value" + fi + ;; + esac + done < .env +fi + +export LANG=en_US.UTF-8 +export JAVA_HOME="${JAVA_HOME:-/usr/local/openjdk21}" +export PATH="${JAVA_HOME}/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" + +if [ ! -x "${JAVA_HOME}/bin/java" ]; then + echo "Error: Java 21 not found at ${JAVA_HOME}/bin/java" + exit 1 +fi + +if ! "${JAVA_HOME}/bin/java" -version 2>&1 | grep -q 'version "21'; then + echo "Error: ${JAVA_HOME}/bin/java is not Java 21" + "${JAVA_HOME}/bin/java" -version + exit 1 +fi + +PORT="${PORT:-4003}" + +# HOST from .env is read by the app via environ (bind to 127.0.0.1 in prod) +exec "${JAVA_HOME}/bin/java" -jar /home/selfdestruct/self-destruct/target/self-destruct.jar --port "${PORT}" diff --git a/scripts/update-restart.sh b/scripts/update-restart.sh new file mode 100755 index 0000000..3fc3398 --- /dev/null +++ b/scripts/update-restart.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +## update +sudo -u selfdestruct sh -c 'cd /home/selfdestruct/self-destruct && git pull' +if [ $? -ne 0 ]; then + echo "Error: git pull failed, aborting deployment" + exit 1 +fi +## build and restart if successful +sudo -u selfdestruct sh -c 'cd /home/selfdestruct/self-destruct && ./scripts/build-prod.sh' && sudo service selfdestruct stop && sudo service selfdestruct start +## clean exit +exit 0 diff --git a/src/self_destruct/core.clj b/src/self_destruct/core.clj index 772ca7f..d523542 100644 --- a/src/self_destruct/core.clj +++ b/src/self_destruct/core.clj @@ -2,7 +2,8 @@ (:require [self-destruct.config :as config] [self-destruct.route :as route] [self-destruct.worker :as worker]) - (:require [clojure.tools.cli :refer [parse-opts]] + (:require [clojure.string :as str] + [clojure.tools.cli :refer [parse-opts]] [environ.core :as environ] [ring.adapter.jetty :as jetty] [ring.middleware.defaults :refer :all] @@ -81,6 +82,16 @@ nil)) +(defn- jetty-options + "Build Jetty options from CLI port and optional HOST env (e.g. 127.0.0.1)." + [port] + (let [host (environ/env :host) + opts {:port (Integer/valueOf port)}] + (if (and host (not (str/blank? host))) + (assoc opts :host host) + opts))) + + ;; main application entry point (defn -main [& args] (let [parsed (parse-opts args cli-options) @@ -93,9 +104,11 @@ (timbre/info "running init tasks") ;; workers only; logging already configured above (worker/launch-workers) - (timbre/info (str "starting the app on port " port "...")) - (jetty/run-jetty app - {:port (Integer/valueOf port)}))))) + (timbre/info (str "starting the app on port " port + (when-let [host (environ/env :host)] + (str " host " host)) + "...")) + (jetty/run-jetty app (jetty-options port)))))) ;; development mode main application entry point @@ -111,4 +124,4 @@ (worker/launch-workers) (timbre/info (str "DEV: starting the app on port " port "...")) (jetty/run-jetty (wrap-reload #'app) - {:port (Integer/valueOf port)}))))) + (jetty-options port))))))