Hosting & Deployment
A production deploy has two pieces: the Next.js frontend and the Directus stack. They can live on the same host or on separate infrastructure.
Option 1: single VPS (Docker Compose)
The repo's root docker-compose.yml runs the full stack. On first boot, the directus-init sidecar provisions collections and seeds EN/FR/ES content automatically: same zero-command flow as local dev.
# On the server, first time only
git clone <your-fork>
cd saasforge-agency
cp .env.example .env
# Edit secrets: change Postgres password, Directus admin, keys, etc.
docker compose up -d --build
Put Caddy, Nginx, or Cloudflare Tunnel in front to terminate TLS:
yourdomain.com→ port 3000 (Next.js frontend)cms.yourdomain.com→ port 8055 (Directus)
Option 2: Vercel frontend + self-hosted Directus
- Deploy Directus on a VPS, Directus Cloud, Railway, Fly.io, or any Docker host. Persist the Postgres volume.
- On first boot, SSH in and run
docker compose run --rm directus-initonce to bootstrap + seed. (Or include the init service in your compose; it's a no-op on subsequent boots thanks to idempotency guards.) - Deploy the frontend to Vercel from the
frontend/directory. Set environment variables in the Vercel project:NEXT_PUBLIC_DIRECTUS_URL= your public Directus URL (e.g.https://cms.yourdomain.com)NEXT_PUBLIC_SITE_URL= your public site URL (for canonical tags, sitemap, hreflang)NEXT_PUBLIC_DEFAULT_LOCALE=enRESEND_API_KEY(optional)
- CORS: set
CORS_ORIGINin the Directus env to your Vercel domain, otherwise browser-based requests get blocked.
Required env vars in production
Directus side (in compose .env):
DIRECTUS_KEY,DIRECTUS_SECRET: generate real UUIDs, don't ship the examplesDIRECTUS_ADMIN_EMAIL,DIRECTUS_ADMIN_PASSWORDPOSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB
Frontend side (Vercel or host):
NEXT_PUBLIC_DIRECTUS_URLNEXT_PUBLIC_SITE_URLNEXT_PUBLIC_DEFAULT_LOCALE=enRESEND_API_KEY(if contact-form email delivery is wanted)NEXT_PUBLIC_GTM_ID/NEXT_PUBLIC_GA_ID(if analytics)NEXT_PUBLIC_BOOKING_URL(optional Calendly/Cal.com link)
Backups
Two things must be backed up:
- Postgres: where every collection + content lives. Use
pg_dump:docker compose exec database pg_dump -U directus directus | gzip > backup-$(date +%F).sql.gz - Directus uploads: the
directus_uploadsDocker volume holds files uploaded through the admin (logos, hero images). These are real user data and not reproducible from code.docker run --rm \ -v saasforge-agency_directus_uploads:/data \ -v $(pwd):/backup alpine \ tar czf /backup/uploads-$(date +%F).tgz -C /data .
The collections schema itself does not need backing up: it's re-created by bootstrap.mjs from code.
What happens if you wipe volumes
Running docker compose down -v wipes both the Postgres data and uploads. On the next docker compose up, directus-init re-runs bootstrap + seed, giving you a fresh CMS with the same seeded EN/FR/ES content. Only user-uploaded files are unrecoverable without a backup.
Next steps
- Implementation Status: what ships vs. BYO.