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

  1. Deploy Directus on a VPS, Directus Cloud, Railway, Fly.io, or any Docker host. Persist the Postgres volume.
  2. On first boot, SSH in and run docker compose run --rm directus-init once to bootstrap + seed. (Or include the init service in your compose; it's a no-op on subsequent boots thanks to idempotency guards.)
  3. 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=en
    • RESEND_API_KEY (optional)
  4. CORS: set CORS_ORIGIN in 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 examples
  • DIRECTUS_ADMIN_EMAIL, DIRECTUS_ADMIN_PASSWORD
  • POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB

Frontend side (Vercel or host):

  • NEXT_PUBLIC_DIRECTUS_URL
  • NEXT_PUBLIC_SITE_URL
  • NEXT_PUBLIC_DEFAULT_LOCALE=en
  • RESEND_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:

  1. Postgres: where every collection + content lives. Use pg_dump:
    docker compose exec database pg_dump -U directus directus | gzip > backup-$(date +%F).sql.gz
    
  2. Directus uploads: the directus_uploads Docker 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