Hosting SaaS Applications on CloudPloy
SaaS products have different hosting requirements from informational sites or internal tools. Your application needs to serve dozens or hundreds of independent customers, each with their own data - and it needs to do this reliably while you continuously deploy new features. CloudPloy's managed container hosting handles the deployment infrastructure so your team focuses on product development, not server administration.
This page covers architecture patterns for PHP and Laravel SaaS applications, how CloudPloy fits into SaaS deployment workflows, and practical considerations for growing from your first 10 customers to your first 1,000.
Multi-Tenant Architecture Patterns for Laravel SaaS
The most fundamental SaaS architecture decision is how to handle tenant data isolation. Laravel supports all three common approaches:
| Pattern | How It Works | Best For | Complexity |
|---|---|---|---|
| Single database, shared tables | All tenants' data in one database, filtered by a tenant_id column on every table | Early-stage SaaS, <1,000 tenants, simple data models | Low - standard Laravel queries with global scope |
| Single database, separate schemas | Each tenant gets their own MySQL/PostgreSQL schema or table prefix (tenant_a_posts, tenant_b_posts) | Medium-scale SaaS with compliance requirements | Medium - dynamic connection management |
| Separate database per tenant | Each customer has their own database, provisioned at onboarding | Enterprise SaaS, GDPR data residency, large data volumes per customer | High - database provisioning, migration management |
CloudPloy supports all three patterns. Each application container connects to your database(s) via environment variables - you provision and manage multi-tenant databases however your application requires.
Shared Database with Tenant Scope (Most Common)
The simplest approach: add tenant_id to every table and use a global Eloquent scope to automatically filter queries. The popular stancl/tenancy package handles this for Laravel:
composer require stancl/tenancy // config/tenancy.php
'tenant_model' => \App\Models\Tenant::class,
'id_generator' => \Stancl\Tenancy\UUIDGenerator::class,
// Central domain (your marketing site)
'central_domains' => [
'yoursaas.com',
'www.yoursaas.com',
], // Tenant model
class Tenant extends Model implements TenantWithDatabase
{
use HasDatabase, HasDomains;
}
// In a controller: switch to tenant context
tenancy()->initialize($tenant);
// All subsequent DB queries are scoped to this tenant's database Database-Per-Tenant with Dynamic Connections
For stronger isolation, provision a new database for each tenant at signup. Configure Laravel to switch database connections based on the incoming request's subdomain or custom domain:
// app/Http/Middleware/InitializeTenancy.php
public function handle(Request $request, Closure $next)
{
$host = $request->getHost();
$tenant = Tenant::where('domain', $host)
->orWhere('subdomain', explode('.', $host)[0])
->firstOrFail();
// Set dynamic database connection for this request
config([
'database.connections.tenant.database' => $tenant->database_name,
'database.connections.tenant.username' => $tenant->database_user,
'database.connections.tenant.password' => decrypt($tenant->database_password),
]);
DB::purge('tenant');
DB::reconnect('tenant');
app()->instance('tenant', $tenant);
return $next($request);
} Deployment Architecture for SaaS on CloudPloy
A typical Laravel SaaS deployment on CloudPloy uses a single application container serving all tenants. The deployment pipeline:
- Developer pushes to
mainbranch on GitHub - CloudPloy detects the push and starts a new container build from your Dockerfile
- New container runs
php artisan migrate --forceto apply database migrations across all tenant databases - CloudPloy health-checks the new container, then switches traffic atomically (blue-green)
- Old container drains over 30 seconds
- Total downtime: zero
Environment Variables for SaaS Configuration
Set these in App > Settings > Environment Variables:
# Core Laravel config
APP_ENV=production
APP_KEY=base64:your-app-key
APP_URL=https://yoursaas.com
APP_DEBUG=false
# Database (central database for tenant registry)
DB_CONNECTION=mysql
DB_HOST=your-db-host
DB_DATABASE=saas_central
DB_USERNAME=saas_user
DB_PASSWORD=your-db-password
# Queue (for background jobs - tenant provisioning, email, etc.)
QUEUE_CONNECTION=redis
REDIS_HOST=your-redis-host
REDIS_PASSWORD=your-redis-password
# Cache
CACHE_DRIVER=redis
SESSION_DRIVER=redis
# Logging (use stderr so CloudPloy captures it)
LOG_CHANNEL=stderr
LOG_LEVEL=error Deploy Commands for SaaS Applications
Configure in App > Settings > Deploy Commands. For a multi-tenant app, migrations must run on the central database and potentially all tenant databases:
# Run central migrations
php artisan migrate --force
# For stancl/tenancy: migrate all tenant databases
php artisan tenants:artisan "migrate --force"
# Clear caches
php artisan config:cache
php artisan route:cache
php artisan view:cache Custom Domains for SaaS Customers
Enterprise SaaS customers often require their own domain (app.theircorp.com) instead of your default subdomain (theircorp.yoursaas.com). This requires two things:
Wildcard Subdomain Setup
For subdomain-based tenancy (customer.yoursaas.com), add a wildcard domain in CloudPloy:
- Go to App > Settings > Domains
- Add
*.yoursaas.com - CloudPloy provisions a wildcard SSL certificate covering all subdomains
- In your DNS:
*.yoursaas.com A YOUR_SERVER_IP
Custom Domain Provisioning
For customer-owned custom domains:
- Customer creates a CNAME pointing
app.theircorp.comtotheircorp.yoursaas.com - Your application registers the custom domain via CloudPloy's API
- CloudPloy auto-provisions a Let's Encrypt certificate for the custom domain
CloudPloy's API endpoint for domain management allows your application to programmatically add new custom domains when customers configure them - no manual intervention needed per customer.
Queue Workers for SaaS Background Jobs
SaaS applications typically need background processing for: tenant provisioning (creating databases, running migrations), sending emails, processing uploads, generating reports, and scheduled maintenance tasks.
Configure dedicated queue workers in App > Workers:
# General queue worker (handles most jobs)
php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
# Tenant provisioning worker (separate queue to not block general jobs)
php artisan queue:work redis --queue=provisioning --sleep=3 --tries=5
# Laravel Horizon (replaces multiple queue:work commands)
php artisan horizon For tenant provisioning that takes 10-30 seconds (creating a database, running migrations, sending welcome email), queue the work and respond to the signup request immediately. Use Laravel's ShouldBeUnique interface on the provisioning job to prevent duplicate provisioning if a webhook fires twice.
Scaling a PHP SaaS Application
Vertical Scaling (Single Server)
Most PHP SaaS applications scale vertically for a long time. A 16GB RAM server with properly configured Redis caching and PHP-FPM process management handles thousands of concurrent users. Upgrade your server size in the CloudPloy dashboard - the process takes under 2 minutes with no downtime.
| Server Size | PHP-FPM Workers | Typical Tenant Count | Monthly Cost Ballpark |
|---|---|---|---|
| 4GB RAM, 2 vCPU | ~20 workers | Up to ~500 tenants (low usage) | $20-40/month |
| 8GB RAM, 4 vCPU | ~50 workers | 500-2,000 tenants | $40-80/month |
| 16GB RAM, 8 vCPU | ~100 workers | 2,000-10,000 tenants | $80-160/month |
| 32GB RAM, 16 vCPU | ~200 workers | 10,000+ tenants | $160-320/month |
These are rough estimates - actual capacity depends on what each PHP request does, how much work hits the database vs Redis cache, and the distribution of active vs inactive tenants.
Redis Caching Strategy for SaaS
Caching is critical for SaaS applications where many tenants may request similar data. Key cache prefix strategy to avoid cross-tenant data leaks:
// In AppServiceProvider::boot() or tenant middleware
Cache::setPrefix("tenant_{$tenant->id}:");
// Or use a dedicated Redis database per tenant (for strict isolation)
config(['cache.stores.redis.database' => $tenant->redis_database_index]); Using a consistent prefix per tenant means cache data is automatically isolated. Configure this in your tenancy middleware, not globally.
Database Connection Pooling
PHP-FPM creates a new database connection per request. With 50 active tenants and 10 concurrent requests each, that's potentially 500 simultaneous MySQL connections. Use ProxySQL connection pooling or set your MySQL max_connections appropriately:
# Check current connection count in App > Terminal
mysql -h $DB_HOST -u $DB_USERNAME -p$DB_PASSWORD -e "SHOW STATUS LIKE 'Threads_connected';"
# MySQL config for SaaS workloads (set via DB config or contact support)
max_connections = 500
wait_timeout = 60 # Close idle connections faster
interactive_timeout = 60 Per-Tenant Staging Environments
SaaS products that make significant database schema changes need careful staging. Recommended setup:
- Production app:
yoursaas.com/*.yoursaas.com- connected tomainbranch - Staging app:
staging.yoursaas.com/*.staging.yoursaas.com- connected tostagingbranch
The staging app uses a separate database with a copy of your production tenant registry (with anonymized data). When a new feature branch is ready, merge to staging - the staging environment auto-deploys and runs migrations against staging data. Test with representative tenants before merging to main.
Monitoring SaaS Applications
SaaS applications need application-level monitoring beyond basic uptime checks. Configure these in CloudPloy and your application:
Health Check Endpoint
// routes/web.php
Route::get('/health', function () {
// Check database connectivity
DB::select('SELECT 1');
// Check Redis connectivity
Cache::put('health_check', 1, 5);
// Check queue processing (is the queue worker running?)
$lastJob = DB::table('jobs')->orderBy('created_at', 'desc')->first();
$queueLag = $lastJob
? now()->diffInMinutes($lastJob->created_at)
: 0;
return response()->json([
'status' => 'ok',
'queue_lag_minutes' => $queueLag,
'timestamp' => now()->toIso8601String(),
]);
}); Application Metrics to Track
- Tenant provisioning success/failure rate and duration
- Failed background job count (check
failed_jobstable daily) - Database slow query log (enable in App > Database > Slow Query Log)
- Redis memory usage (high memory = object cache eviction, degraded performance)
- Response time p95/p99 per endpoint (available in App > Metrics)
Common SaaS Deployment Challenges
| Challenge | Cause | Solution |
|---|---|---|
| Migration takes too long during deploy | Large tenant count, complex schema change | Use zero-downtime migrations (add column, backfill, then drop old column in separate deploy) |
| Cache data leaks between tenants | Missing tenant prefix in cache keys | Set cache prefix in tenant middleware; audit all Cache::put() calls |
| Tenant provisioning fails silently | Job fails after max retries; no alert | Add failed() method to provisioning job; notify team via webhook or email |
| Slow response for large tenants | Missing database indexes, N+1 queries | Add tenant_id to all compound indexes; use Laravel Debugbar in staging |
| Custom domain SSL takes hours | DNS not propagated when certificate requested | Poll for DNS propagation before triggering CloudPloy domain add; retry with exponential backoff |
| Queue jobs pile up after deploy | Deploy restarts workers mid-job | Use --max-time=3600 flag; workers gracefully finish current job before stopping |
WordPress SaaS (WordPress Multisite)
Building a SaaS product on WordPress typically means WordPress Multisite - one WordPress installation serving multiple customer sites. CloudPloy supports WordPress Multisite with wildcard subdomain configuration.
Key requirements for WordPress Multisite on CloudPloy:
- Wildcard DNS:
*.yoursaas.com A YOUR_SERVER_IP - Wildcard domain in App > Settings > Domains:
*.yoursaas.com - CloudPloy provisions a wildcard SSL certificate automatically
wp-config.phpconstants:MULTISITE,SUBDOMAIN_INSTALL,DOMAIN_CURRENT_SITE- Nginx is configured automatically for subdomain multisite rewrites
For WordPress SaaS with custom domains per customer, use the Mercator or similar domain mapping plugin in addition to the standard Multisite setup.
Building a SaaS application and want to review your architecture before launch? Read the staging environment guide for pre-production testing, the Docker container guide for container configuration, or contact CloudPloy to discuss your SaaS deployment requirements.