This project is deployed with docker compose.
For local development with hot reload, use docker-compose.dev.yaml instead of the production stack.
The stack includes:
vacation-app(application)postgres(database)nginx(reverse proxy)
Nginx config note:
- Edit
nginx/conf.d/default.conffor application routes and proxy rules. - The container generates the live server block from the templates and includes that file, so deploys now pick up changes there reliably.
Why old FE appears with docker compose up -d:
docker-compose.yamlis production-style and serves built frontend files from the app image.
Use the dev stack:
docker compose -f docker-compose.dev.yaml up -dOpen:
- Frontend (Vite HMR):
http://localhost:3001 - Backend API:
http://localhost:8080 - PostgreSQL (host-only):
127.0.0.1:5432
Dev logs:
docker compose -f docker-compose.dev.yaml logs -f vacation-app-dev frontend-dev postgresStop dev stack:
docker compose -f docker-compose.dev.yaml down- Docker and Docker Compose plugin installed
- DNS for your domain pointing to the server IP
- Ports
22(SSH) and443(HTTPS) open on the server - A server user with SSH key access (the workflow uses
rootby default)
The deployment workflow (.github/workflows/hetzner.yml) expects the following repository secrets:
| Secret | Purpose |
|---|---|
SERVER_IP |
IP address of the target server |
SSH_PRIVATE_KEY |
Private key for SSH authentication to the server |
TOKEN |
GitHub Personal Access Token with repo scope (used to clone the repository on the server) |
RESULT_TOKEN_SIGNING_KEY |
Strong secret key for signing optimization result tokens. Required in Production. |
DATABASE_SECURITY_HEALTHCHECK_ACCESS_TOKEN |
UUID that protects the database security report endpoint |
CF_ORIGIN_CERT |
TLS certificate content (e.g., Cloudflare Origin CA certificate PEM) |
CF_ORIGIN_KEY |
TLS private key content corresponding to the certificate |
For local development, create a .env file in the project root. It is intentionally not committed.
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<strong-password>
POSTGRES_DB=vacation_optimizer
DATABASE_SECURITY_HEALTHCHECK_ACCESS_TOKEN=<uuid>Generate the UUID with:
uuidgen | tr '[:upper:]' '[:lower:]'Without a valid UUID, the report endpoint stays disabled and returns 404.
In production, the deployment workflow recreates the .env file automatically from repository secrets.
The following files contain hard-coded domain references (currently longvacation.eu) that must be updated for your own domain before deploying:
.github/workflows/hetzner.yml— health check URLs and--resolvetargetsnginx/templates/default.ssl.conf—server_nameand certificate pathsnginx/templates/default.http.conf—server_namenginx/start-nginx.sh— certificate file paths
From the project root:
docker compose up -d --buildImportant behavior:
- If certificates are missing in
./certbot/conf, nginx serves HTTP only. - Once certificates exist, nginx automatically switches to HTTPS config on restart.
- PostgreSQL is not published publicly in the production stack.
- The application container is only reachable through
nginxinside the Compose network.
Cloudflare Origin Certificate is used. After creating one with a 10-year validity, copy the .pem and .key files to the server:
scp longvacation.eu.pem root@hetzner:/root/vacation-app/nginx/ssl
scp longvacation.eu.key root@hetzner:/root/vacation-app/nginx/sslThen restart nginx:
docker compose restart nginxCloudflare Origin Certificates are valid for 10 years and do not require automated renewal.
The application runs a read-only PostgreSQL role and connection audit at startup and then every 15 minutes. It checks login roles, elevated privileges, memberships in sensitive built-in PostgreSQL roles, and remote client connections. The endpoint only serves the last completed report; it never triggers database inspection itself.
Request it with the UUID stored in .env:
curl --fail --silent --show-error \
"https://longvacation.eu/api/internal/database-security?accessKey=${DATABASE_SECURITY_HEALTHCHECK_ACCESS_TOKEN}"Three invalid UUID attempts from one client IP start a 15-minute cooldown. Invalid requests always receive 404; a valid UUID during its own cooldown receives 429 with Retry-After. Nginx disables access logging for this endpoint so the query-string token is not placed in its access log. Avoid opening the URL in a browser or sharing it, since query strings can still end up in browser history.
Start:
docker compose up -dRebuild and start:
docker compose up -d --buildLogs:
docker compose logs -f vacation-app nginx postgresRemove postgres data volume (danger: deletes local DB data):
docker compose stop postgres; docker compose rm -f postgres; docker volume rm vacation-app_pgdataStop:
docker compose downStop and remove volumes (danger: deletes local DB data):
docker compose down -vUse this when you want a fully clean database after rewriting migrations or seed data.
Production-style stack:
docker compose down -v
docker volume rm vacationoptimizer_pgdata 2>/dev/null || true
docker compose up -d --buildLocal dev hot-reload stack:
docker compose -f docker-compose.dev.yaml down -v
docker volume rm vacationoptimizer_pgdata-dev 2>/dev/null || true
docker compose -f docker-compose.dev.yaml up -dIf you want to remove only the PostgreSQL container and volume:
docker compose stop postgres
docker compose rm -f postgres
docker volume rm vacationoptimizer_pgdata
docker compose up -d postgres