Documents Hetzner CX23 hosting setup, nginx/PHP-FPM stack, SQLite database configuration, and deployment architecture for single-app deployment
3.5 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
root— break-glass admin onlydeploy(UID 1000) — owns the app directory, performs deploymentswww-data— nginx + PHP-FPM runtime user; group-shared withdeployfor file access
Note: This box runs one app. If a second app is ever added, the default PHP-FPM
wwwpool MUST be split into per-app pools running as per-app system users, or the apps will be able to read each other's.envand SQLite files. See the earlier project chats for the multi-app layout.
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.