Runbook (VPS operations)
Host: HestiaCP server, domain krunc.com, document root /home/nerdy/web/krunc.com/public_html, PHP-FPM 8.3 runs as user nerdy, MariaDB database nerdy_krunc.
Deploy
cd /home/nerdy/web/krunc.com/public_html
git pull
composer dump-autoload -o
php bin/migrate # applies pending SQL migrations exactly once
php bin/lint && php bin/test
chown -R nerdy:nerdy . && chmod 600 .env
curl -fsS https://krunc.com/healthz && curl -fsS https://krunc.com/readyz
readyz returns 503 when the database is unreachable or storage/ is not writable.
Environment
All configuration lives in .env (chmod 600, never committed). .env.example lists every variable. Generate secrets with php bin/keygen. Rotating ENCRYPTION_KEY requires re-encrypting stored tokens — do not rotate casually.
Reverse proxies / Cloudflare
krunc.com is proxied by Cloudflare (orange cloud) in front of nginx → Apache → PHP-FPM. Client IPs are recovered in layers: nginx trusts CF-Connecting-IP from Cloudflare's published ranges (/etc/nginx/conf.d/cloudflare.inc), Apache's mod_remoteip trusts X-Real-IP from nginx, and the app (Request::clientIp()) honours CF-Connecting-IP / X-Real-IP / X-Forwarded-For only when the direct peer is loopback or listed in TRUSTED_PROXIES. A client that reaches the origin directly therefore cannot spoof its IP for rate limits or audit logs. Keep Cloudflare SSL mode on Full (strict) — the origin has a real Let's Encrypt certificate.
Worker & scheduler
The queue driver defaults to sync (jobs run inline). Scheduled work (live polling, scheduled posts, token refresh, offline detection, roll-ups) still needs the worker tick. Cron (installed for user nerdy):
* * * * * php bin/worker --once >> storage/logs/worker.log 2>&1
For higher throughput switch to QUEUE_DRIVER=database and run the worker as a long-lived process:
# /etc/systemd/system/creatoros-worker.service
[Unit]
Description=CreatorOS worker
After=network.target mariadb.service
[Service]
User=nerdy
WorkingDirectory=/home/nerdy/web/krunc.com/public_html
ExecStart=/usr/bin/php bin/worker
Restart=always
[Install]
WantedBy=multi-user.target
Logs
- App:
storage/logs/app.log(JSON lines, secrets redacted) - Mail (log driver):
storage/logs/mail.log - Worker:
storage/logs/worker.log - Web:
/var/log/apache2/domains/krunc.com.*.log
Backups
Nightly database dump with 14-day retention (add to root cron and sync the folder off-VPS):
0 3 * * * mysqldump --single-transaction nerdy_krunc | gzip > /home/nerdy/backups/krunc-$(date +\%F).sql.gz && find /home/nerdy/backups -name 'krunc-*.sql.gz' -mtime +14 -delete
Also sync storage/uploads (media) off-VPS. Hestia's own backups (v-backup-user nerdy) cover both.
Restore
gunzip -c krunc-YYYY-MM-DD.sql.gz | mysql nerdy_krunc
rsync -a backup/uploads/ storage/uploads/
php bin/migrate --status # confirm schema matches
Test a restore into a scratch database (v-add-database nerdy scratch ...) before relying on it.
Stripe webhooks
Endpoint: https://krunc.com/webhooks/stripe. Set STRIPE_WEBHOOK_SECRET from the Stripe dashboard. Locally: stripe listen --forward-to https://krunc.com/webhooks/stripe. Failed events show up in /admin/webhooks with a reprocess button.
Troubleshooting
- 500 on every page: check
storage/logs/app.logand Apache error log; usually.envunreadable bynerdyor a badENCRYPTION_KEY. - HTTP (port 80) shows a different site: a catch-all Apache vhost (
/etc/apache2/conf.d/custom-domains-catchall.conf,ServerAlias *) captures plain-HTTP requests on this host. Force-SSL is enabled forkrunc.com, so HTTP redirects to HTTPS which is served correctly. - Jobs stuck:
/admin/jobslists failed jobs; retry there orphp bin/worker --once. - Migrations: never edit an applied file; add a new numbered
.sqlunderdatabase/migrations.