Files
dvla-api/SERVER.md
Ovidiu U bf3aa57ef9 Add SERVER.md documentation for production VPS configuration
Documents Hetzner CX23 hosting setup, nginx/PHP-FPM stack, SQLite database configuration, and deployment architecture for single-app deployment
2026-05-15 14:00:44 +01:00

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 only
  • deploy (UID 1000) — owns the app directory, performs deployments
  • www-data — nginx + PHP-FPM runtime user; group-shared with deploy for file access

Note: This box runs one app. If a second app is ever added, the default PHP-FPM www pool MUST be split into per-app pools running as per-app system users, or the apps will be able to read each other's .env and 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
  • 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)

Application

  • Path: /var/www/dvla-api
  • Owner: deploy:www-data, directories have SGID bit (drwxrwsr-x) so new files inherit the www-data group 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, directory 770, both owned deploy:www-data
    • Directory must be writable by www-data so SQLite can create -journal / -wal / -shm siblings
  • 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=sync in .env or install the systemd unit below>
  • Scheduler: <TODO: cron entry for php artisan schedule:run if 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 deploy user instead of deploy-<appname>
  • Default www-data PHP-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.