Skip to content
SaaS4Builders
Getting Started

Deployment Guide

Production checklist and deployment strategies for SaaS4Builders on Forge, Ploi, or Docker-based infrastructure.

This guide covers everything you need to move SaaS4Builders from your local Docker environment to a production server. It assumes you are deploying a standard server-based setup — the recommended path uses Laravel Forge or Ploi, but Docker and other platforms are also covered.


Production Checklist

Work through this checklist before going live. Every item matters.

Application Settings

VariableDev ValueProduction Value
APP_ENVlocalproduction
APP_DEBUGtruefalse
APP_KEYauto-generatedKeep the generated key. Never share it.
APP_URLhttp://localhost:8000https://api.yourdomain.com
FRONTEND_URLhttp://localhost:3000https://app.yourdomain.com
APP_TIMEZONEUTCUTC (recommended)
APP_DEBUG=true in production exposes stack traces, environment variables, and database queries to users. Always set it to false.

Authentication and CORS

VariableProduction ValueNotes
SANCTUM_STATEFUL_DOMAINSapp.yourdomain.comMust exactly match your frontend domain (no protocol, include port if non-standard)
SESSION_DOMAIN.yourdomain.comPrefixed with . to cover subdomains. Required for CSRF cookies to be sent correctly.

CORS is configured in backend/config/cors.php:

backend/config/cors.php
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],

Setting FRONTEND_URL correctly in your backend .env automatically configures CORS. Only your frontend domain will be allowed to make cross-origin requests.

If SANCTUM_STATEFUL_DOMAINS does not exactly match your frontend domain, cookie-based authentication will silently fail. This is the most common production authentication issue.

Database

Use a managed PostgreSQL service instead of a Docker container:

VariableProduction Value
DB_CONNECTIONpgsql
DB_HOSTYour managed database hostname
DB_PORT5432 (or your provider's port)
DB_DATABASEYour production database name
DB_USERNAMEYour production database user
DB_PASSWORDA strong, unique password

Run migrations on first deploy:

php artisan migrate --force
Do not run --seed in production. The base seeders (roles, currencies, catalog) should be run once manually after the first migration. Demo seeders are automatically skipped when APP_ENV=production.

Cache, Sessions, and Queues

Use a managed Redis instance for all three:

VariableProduction Value
SESSION_DRIVERredis
CACHE_STOREredis
QUEUE_CONNECTIONredis
REDIS_HOSTYour managed Redis hostname
REDIS_PASSWORDYour Redis password
REDIS_PORT6379 (or your provider's port)

Stripe Billing

Switch from test keys to live keys:

VariableProduction Value
STRIPE_KEYpk_live_... (your Stripe publishable key)
STRIPE_SECRETsk_live_... (your Stripe secret key)
STRIPE_WEBHOOK_SECRETwhsec_... (from your Stripe webhook endpoint)
BILLING_MODEstripe_managed
BILLING_DEFAULT_CURRENCYYour default currency code (e.g., USD)

Register your webhook endpoint in the Stripe Dashboard:

  • URL: https://api.yourdomain.com/api/v1/stripe/webhook
  • Events to listen for: All invoice.*, customer.subscription.*, checkout.session.*, and charge.* events.

Mail

Replace Mailpit with a production mail provider:

MAIL_MAILER=mailgun
MAILGUN_DOMAIN=your-domain.com
MAILGUN_SECRET=your-mailgun-key

Update the sender address:

MAIL_FROM_ADDRESS=noreply@yourdomain.com
MAIL_FROM_NAME="Your App Name"

OAuth Providers

If you use Google or GitHub OAuth, register new OAuth applications with your production domains:

VariableProduction Value
GOOGLE_REDIRECT_URIhttps://api.yourdomain.com/api/v1/auth/oauth/google/callback
GITHUB_REDIRECT_URIhttps://api.yourdomain.com/api/v1/auth/oauth/github/callback

Create new OAuth apps in the Google Cloud Console and GitHub Developer Settings with your production callback URLs.

Broadcasting / Reverb

If you use real-time features (WebSocket):

VariableProduction Value
REVERB_SCHEMEhttps
REVERB_HOST_PUBLICws.yourdomain.com (or your API domain)
REVERB_PORT443 (if behind reverse proxy with SSL)

Alternatively, replace Reverb with a managed service like Pusher or Ably.

Web Push Notifications

Generate production VAPID keys:

php artisan webpush:vapid

Set VAPID_SUBJECT to your production URL:

VAPID_SUBJECT=https://yourdomain.com

Logging

VariableProduction Value
LOG_CHANNELstack
LOG_LEVELwarning or error

Avoid debug level in production — it generates excessive log volume and may expose sensitive data.


Cron and Scheduler

The Laravel scheduler requires a system cron job that runs every minute:

* * * * * cd /path-to-your-backend && php artisan schedule:run >> /dev/null 2>&1

On Forge or Ploi, the scheduler is configured through the UI — no manual cron setup needed.

Scheduled Tasks

The application schedules these tasks automatically:

CommandSchedulePurpose
billing:sync-invoicesDaily at 02:00Sync invoices from Stripe
billing:sync-chargesDaily at 03:00Sync charges from Stripe
onboarding:cleanup-staleDaily at 04:00Remove incomplete onboarding tenants

These tasks run without overlapping (a second instance will not start if the previous one is still running).


Queue Worker

The queue worker processes background jobs (emails, webhooks, billing sync). It must run continuously in production.

php artisan queue:work --sleep=3 --tries=3 --max-time=3600

Ensure QUEUE_CONNECTION=redis is set in your backend .env so the worker uses the Redis driver.

This command:

  • Processes jobs from the configured queue connection (Redis)
  • Waits 3 seconds between polling when idle
  • Retries failed jobs up to 3 times
  • Restarts automatically after 1 hour (prevents memory leaks)

Keeping the Worker Running

Use a process manager to ensure the worker restarts if it crashes:

[program:saas-queue-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path-to-your-backend/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/path-to-your-backend/storage/logs/worker.log
stopwaitsecs=3600

On Forge or Ploi, queue workers are managed through the dashboard — add a worker with the command above.

After each deployment, restart the queue worker to pick up new code:

php artisan queue:restart

Deployment with Forge or Ploi

Laravel Forge and Ploi are the recommended deployment platforms. They handle server provisioning, SSL certificates, queue workers, and scheduled tasks.

Setup Steps

  1. Provision a server — Ubuntu 24.04 with PHP 8.3+, PostgreSQL 16, Redis 7, Nginx.
  2. Create a site — Point it to your domain. Set the web directory to backend/public.
  3. Configure environment — Add all production .env variables through the platform's UI.
  4. Set up the database — Create a PostgreSQL database and user.
  5. Configure the queue worker — Add a worker with the command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600.
  6. Enable the scheduler — Enable the cron-based scheduler in the platform settings.
  7. Set up SSL — Use the platform's Let's Encrypt integration.

Deploy Script

Configure your deploy script to run on each push:

cd /home/forge/api.yourdomain.com

# Pull latest code
git pull origin main

# Install dependencies (no dev packages)
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev

# Run migrations
php artisan migrate --force

# Cache configuration for performance
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Restart the queue worker to pick up new code
php artisan queue:restart

Frontend Deployment

The Nuxt frontend is a separate deployment. Options:

  1. Static hosting — Run pnpm build and deploy the .output/ directory to Vercel, Netlify, or Cloudflare Pages.
  2. Node server — Run pnpm build and then node .output/server/index.mjs behind a reverse proxy.
  3. Same server — Deploy both backend and frontend on the same server with separate Nginx configurations.

Build the frontend for production:

cd frontend
pnpm install --frozen-lockfile
pnpm build

Docker Production Deployment

The included docker-compose.yml is designed for development. For production Docker deployments, create a separate docker-compose.production.yml with production-appropriate settings.

Key Differences from Development

ConcernDevelopmentProduction
FrontendNuxt dev server with HMRBuilt Nuxt app served via Node or Nginx
MailpitCaptures all emailsRemove — use real mail provider
DatabaseDocker containerManaged PostgreSQL service
RedisDocker containerManaged Redis service
VolumesBind mounts for live editingBuilt images with code baked in
SSLNot configuredRequired — use Traefik, Caddy, or cloud LB
ResourcesUnlimitedSet CPU/memory limits

Recommendations

  • Use managed database and Redis — Do not run PostgreSQL or Redis in Docker containers in production. Use your cloud provider's managed services (AWS RDS, DigitalOcean Managed Databases, etc.).
  • SSL termination — Use a reverse proxy (Traefik, Caddy) or cloud load balancer for HTTPS.
  • Container orchestration — For scaling, consider Docker Swarm or Kubernetes. For a single server, Docker Compose with production settings is sufficient.

Security Checklist

Before going live, verify every item:

  • APP_DEBUG is false
  • APP_ENV is production
  • APP_KEY is generated and stored securely (not committed to git)
  • HTTPS is enforced on all endpoints (API and frontend)
  • SANCTUM_STATEFUL_DOMAINS matches your production frontend domain exactly
  • FRONTEND_URL is set to your production frontend URL (controls CORS)
  • Database credentials are strong and unique (not the default secret)
  • Stripe is using live keys (not test keys)
  • Stripe webhook endpoint is registered with the correct production URL
  • OAuth redirect URIs use production domains
  • LOG_LEVEL is warning or error (not debug)
  • Redis has a password set (if accessible from outside the private network)
  • Queue worker is running and monitored (Supervisor or systemd)
  • Scheduler cron job is configured
  • VAPID keys are generated for production
  • MAIL_FROM_ADDRESS uses your actual domain

What's Next