Updates SERVER.md with deploy-dvla user separation, dedicated php8.4-fpm-dvla pool configuration, explicit directory permissions for Laravel storage/cache/database, and known production gaps
3.9 KiB
Server — dvla-api
Production host for the dvla-api Laravel application. Single VPS, single app.
This file is the source of truth for how the box is configured. Update it when
you change the box.
Host
- Provider / type: Hetzner Cloud CX23 (2 vCPU, 4 GB RAM, 80 GB disk)
- OS: Ubuntu 24.04 LTS (noble)
- Public IP: 37.27.203.46
- Hostname: ubuntu-4gb-dvla-api
- Swap: 2 GB swapfile at
/swapfile(mounted via/etc/fstab)
Users (UPDATED)
- deploy (UID 1000) — human admin, sudo, your SSH key
- deploy-dvla (UID 1001) — owns and runs dvla-api, no sudo, your SSH key
- www-data — nginx only, no group memberships in app users
PHP-FPM (UPDATED)
- Pool: /etc/php/8.4/fpm/pool.d/dvla-api.conf
- Runs as deploy-dvla
- Socket: /run/php/php8.4-fpm-dvla.sock
- pm.max_children = 3
- Default www.conf disabled (renamed to www.conf.disabled)
Filesystem perms
- /var/www/dvla-api: 2755 (traversable so nginx can reach public/)
- App subdirs: 2750 (deploy-dvla owner+group only)
- public/: 755/644 (world-readable, intentional)
- .env: 600
- storage/, bootstrap/cache/: 2770 (writable by deploy-dvla)
- database/: 2770, database.sqlite: 660
Known gaps
- No backups
- No HTTPS yet
- Token-based git auth (not SSH) — Pangolin doesn't forward SSH to Gitea o
Web stack
- nginx 1.24 — site config:
/etc/nginx/sites-available/dvla-api- Listens on
:80(HTTP only — TLS pending) server_name 37.27.203.46;- Document root:
/var/www/dvla-api/public
- Listens on
- PHP-FPM 8.4 — pool: default
www.conf- Runs as
www-data:www-data - Socket:
/run/php/php8.4-fpm.sock pm = dynamic,pm.max_children = 5(CX23-appropriate)
- Runs as
Application
- Path:
/var/www/dvla-api - Owner:
deploy:www-data, directories have SGID bit (drwxrwsr-x) so new files inherit thewww-datagroup automatically - Framework: Laravel 11 on PHP 8.4
- Git remote: SSH to Gitea via deploy key (see
DEPLOY.md) - Branch deployed:
main
Database — SQLite
- File:
/var/www/dvla-api/database/database.sqlite - Perms: file
660, directory770, both owneddeploy:www-data- Directory must be writable by
www-dataso SQLite can create-journal/-wal/-shmsiblings
- Directory must be writable by
- Journal mode: WAL (enabled with
sqlite3 database/database.sqlite "PRAGMA journal_mode=WAL;") - Backups: <TODO: document backup mechanism — see Backups section>
SESSION_DRIVER, CACHE_STORE, and QUEUE_CONNECTION are all database,
meaning sessions/cache/queue tables live in the same SQLite file as app
data. Acceptable for low traffic; revisit if write contention shows up
(database is locked errors in storage/logs/laravel.log).
Queue / scheduler
- Worker: <TODO: either set
QUEUE_CONNECTION=syncin.envor install the systemd unit below> - Scheduler: <TODO: cron entry for
php artisan schedule:runif any scheduled tasks exist>
Network / firewall
- UFW: <TODO: confirm enabled; document allowed ports>
- Open ports (verified via
ss -tlnp):- 22 — SSH
- 80 — HTTP (nginx)
- 53 — systemd-resolved, loopback only (not public)
- TLS: <TODO: not yet configured. Plan: domain → Let's Encrypt via certbot>
Backups
<TODO. SQLite makes this easy: a single file. Suggested approach:
nightly cron on deploy running sqlite3 database/database.sqlite ".backup /var/backups/dvla-api/db-$(date +\%F).sqlite" followed by offsite copy
(rsync to home server / S3 / Hetzner Storage Box).>
Known divergences from the multi-app pattern in project chats
This box was set up for a single app and intentionally simplified:
- Single
deployuser instead ofdeploy-<appname> - Default
www-dataPHP-FPM pool instead of a per-app pool - No Redis (cache/sessions/queue all on SQLite)
These are acceptable as long as this box hosts one app. Revisit the older project chats before adding a second app to this VPS.