Dev log · Backend
Building a Multi-Tenant SaaS Backend with Laravel & Docker
10 min read
One of my backend projects is a multi-tenant SaaS API built with Laravel and deployed with Docker. The goal: multiple clients on one deployment, each with isolated data, without running a separate server per customer.
This post explains the architecture choices — not Laravel basics, but the decisions that make multi-tenancy work in production.
Why multi-tenant?
Separate deployments per client don’t scale operationally. You want one codebase, one pipeline, shared monitoring — but hard boundaries so Client A never sees Client B’s data. For a B2B SaaS dashboard and API, that boundary is non-negotiable.
Tenant isolation strategy
I used a shared database, shared schema model with a tenant_id column on tenant-scoped tables. It’s the right trade-off for this project’s scale:
- Cheaper and simpler to operate than database-per-tenant
- Easier migrations across all tenants at once
- Still secure when every query is scoped through middleware
How tenancy is resolved
- Request arrives with subdomain, header (X-Tenant-ID), or JWT claim
- Tenant middleware resolves the tenant record and binds it to the request context
- Global Eloquent scope automatically applies where tenant_id = ? on scoped models
- Jobs and queued workers re-hydrate tenant context before handling tasks
Docker setup
- PHP-FPM — Laravel application container
- Nginx — reverse proxy, static assets, rate limiting
- MySQL — primary datastore
- Redis — cache, sessions, queue backend
docker-compose wires services for local dev. Production uses the same images with environment-specific .env files — no “works on my machine” surprises.
Security checklist
- Never trust client-supplied tenant_id in the request body — derive from auth token only
- Encrypt sensitive tenant config at rest
- Per-tenant rate limits to prevent noisy-neighbor abuse
- Audit log table for admin actions
When to choose a different model
If you have enterprise clients demanding physical isolation, or strict compliance requiring separate databases, move to database-per-tenant or schema-per-tenant. Start simple unless regulation forces complexity.