Docker Setup
SaaS4Builders runs entirely in Docker. The docker-compose.yml at the repository root defines 9 services that together provide the complete development environment: application server, frontend dev server, database, cache, email testing, background jobs, task scheduling, and WebSocket support.
This page explains what each service does, how they connect, and how to customize the setup.
Services Overview
| Service | Container | Image | Exposed Port | Purpose |
|---|---|---|---|---|
| php | saas-php | php:8.3-fpm-alpine (custom) | — (internal 9000) | Laravel application via PHP-FPM |
| nginx | saas-nginx | nginx:alpine | 8000:80 | Reverse proxy for the API |
| node | saas-node | node:22-slim (custom, pnpm 10.26.2) | 3000:3000 | Nuxt 4 dev server with SSR and HMR |
| postgres | saas-postgres | postgres:16-alpine | 5432:5432 | PostgreSQL database |
| redis | saas-redis | redis:7-alpine | 6379:6379 | Cache, sessions, and queues |
| mailpit | saas-mailpit | axllent/mailpit | 8025:8025, 1025:1025 | Email testing (SMTP + web UI) |
| queue | saas-queue | Same as php | — | Background job worker |
| scheduler | saas-scheduler | Same as php | — | Laravel task scheduler |
| reverb | saas-reverb | Same as php | 8080:8080 | WebSocket server (Laravel Reverb) |
The php, queue, scheduler, and reverb containers all use the same Dockerfile (docker/php/Dockerfile), but run different commands. The php container runs PHP-FPM, while the others run specific artisan commands.
How Services Connect
All services share a single Docker bridge network called saas-network. This allows containers to communicate by service name.
PHP-FPM and Nginx
Nginx listens on port 80 inside its container (mapped to host port 8000). It proxies PHP requests to the php container on port 9000 via FastCGI. Static assets are served directly by Nginx from the mounted backend/ volume.
Browser → localhost:8000 → Nginx (saas-nginx:80) → PHP-FPM (saas-php:9000)
Node and the Backend API
The Node container needs to reach the backend API for two different contexts:
- Server-side rendering (SSR): When Nuxt renders pages on the server, it calls the API using the internal Docker network. The environment variable
NUXT_API_BASE_URLis set tohttp://nginx:80. - Client-side requests: When the browser makes API calls, it uses the host-accessible URL. The environment variable
NUXT_PUBLIC_API_BASE_URLis set tohttp://localhost:8000.
SSR (server): Nuxt → http://nginx:80/api/v1/...
Client (browser): Browser → http://localhost:8000/api/v1/...
docker-compose.yml as environment variables for the Node container. They override any values in frontend/.env. Note that NUXT_PUBLIC_API_BASE_URL is hardcoded as http://localhost:8000 in docker-compose.yml — if you change NGINX_PORT, you must also update this value in docker-compose.yml. This dual-URL pattern is necessary because the Docker internal network (nginx:80) is not accessible from the browser.Queue Worker
The queue container runs a long-lived process that picks up and executes queued jobs:
php artisan queue:work --sleep=3 --tries=3 --max-time=3600
It processes jobs from the Redis queue, retrying up to 3 times on failure, and restarts automatically after 1 hour to prevent memory leaks.
Task Scheduler
The scheduler container runs a loop that triggers the Laravel scheduler every 60 seconds:
while true; do php artisan schedule:run --verbose --no-interaction & sleep 60; done
The scheduled tasks include:
| Task | Schedule | Purpose |
|---|---|---|
billing:sync-invoices | Daily at 02:00 | Sync invoices from Stripe |
billing:sync-charges | Daily at 03:00 | Sync charges from Stripe |
onboarding:cleanup-stale | Daily at 04:00 | Remove incomplete onboarding tenants |
Reverb (WebSocket)
The Reverb container runs the Laravel Reverb WebSocket server:
php artisan reverb:start --host=0.0.0.0 --port=8080
The frontend connects to Reverb using the NUXT_PUBLIC_REVERB_* environment variables. In development, this is ws://localhost:8080.
Health Checks
PostgreSQL and Redis have built-in health checks. The PHP, queue, scheduler, and reverb containers wait for these services to be healthy before starting:
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
This prevents connection errors during startup.
Volumes
| Volume | Type | Mount Point | Purpose |
|---|---|---|---|
postgres_data | Named volume | /var/lib/postgresql/data | Persistent database storage across container restarts |
redis_data | Named volume | /data | Persistent Redis data |
node_modules | Named volume | /app/node_modules | Node dependencies (isolated from host) |
The node_modules volume is a named volume rather than a bind mount. This prevents conflicts between your host OS and the container's Linux environment — particularly important for native modules like better-sqlite3 that must be compiled for the target platform.
docker compose exec node pnpm add <package>, or use make shell-node to open a shell first.Bind Mounts
In addition to named volumes, the development setup uses bind mounts for live code editing:
| Host Path | Container Path | Service |
|---|---|---|
./backend | /var/www/html | php, queue, scheduler, reverb |
./frontend | /app | node |
Changes to files in backend/ and frontend/ on your host are immediately visible inside the containers.
Useful Make Commands
| Command | Action |
|---|---|
make up | Start all containers in the background |
make down | Stop all containers |
make restart | Restart all containers |
make logs | Follow logs from all containers |
make logs-php | Follow PHP and Nginx logs |
make logs-node | Follow Node container logs |
make logs-reverb | Follow Reverb WebSocket logs |
make shell-php | Open a shell in the PHP container |
make shell-node | Open a shell in the Node container |
make status | Show container status |
Running Commands Inside Containers
You can run any command inside a container with docker compose exec:
# Backend (PHP/Laravel)
docker compose exec php php artisan migrate
docker compose exec php composer require some/package
docker compose exec php php artisan test --filter=BillingTest
# Frontend (Node/Nuxt)
docker compose exec node pnpm add @vueuse/core
docker compose exec node pnpm test
docker compose exec node pnpm build
Or open an interactive shell:
make shell-php # sh inside the PHP container
make shell-node # sh inside the Node container
Customizing Ports
All exposed ports are configurable through the root .env file:
| Variable | Default | Service |
|---|---|---|
NGINX_PORT | 8000 | Backend API (Nginx) |
NUXT_PORT | 3000 | Frontend (Nuxt dev server) |
DB_PORT | 5432 | PostgreSQL |
REDIS_PORT | 6379 | Redis |
MAILPIT_UI_PORT | 8025 | Mailpit web UI |
MAILPIT_SMTP_PORT | 1025 | Mailpit SMTP server |
REVERB_PORT | 8080 | Reverb WebSocket server |
HMR_PORT | 24679 | Nuxt hot module replacement |
After changing a port, restart the containers:
make restart
NGINX_PORT, you must also update NUXT_PUBLIC_API_BASE_URL in docker-compose.yml and SANCTUM_STATEFUL_DOMAINS in backend/.env to match the new port.Troubleshooting
Port Conflicts
Symptom: A container fails to start with "port is already allocated".
Fix: Identify which port is conflicting, then change it in the root .env:
# Find what's using port 5432
lsof -i :5432
# or on Linux
ss -tlnp | grep 5432
Update the corresponding variable in .env and restart.
Permission Issues on Linux
Symptom: PHP cannot write to storage/ or bootstrap/cache/.
Cause: The PHP container runs as user www with UID/GID 1000. If your host user has a different UID, file ownership conflicts occur.
Fix: Check your host user's UID with id -u. If it is not 1000, update docker/php/Dockerfile to match:
RUN addgroup -g <your-uid> -S www && \
adduser -u <your-uid> -S www -G www
Replace <your-uid> with the output of id -u, then rebuild the container:
docker compose build --no-cache php
make restart
Apple Silicon (M1/M2/M3/M4)
The Docker setup works natively on Apple Silicon. All images use Alpine or Slim variants that support ARM64 architecture. No Rosetta emulation is needed.
Windows (WSL2)
Docker Desktop for Windows requires WSL2 as its backend. For best performance:
- Clone the repository inside the WSL2 filesystem (e.g.,
~/projects/), not on the Windows filesystem (/mnt/c/). - Run all
makecommands from a WSL2 terminal. - Access the application at
http://localhost:3000from your Windows browser — Docker ports are automatically forwarded.
Rebuilding Containers
If you modify a Dockerfile (e.g., to add a PHP extension), rebuild the affected containers:
docker compose build --no-cache php
docker compose up -d
To rebuild everything from scratch:
docker compose build --no-cache
make restart
Resetting Everything
If you need a completely clean slate — removing all containers, volumes, and data:
make clean # Removes containers and volumes
make install # Rebuilds everything from scratch
make clean destroys all data in the PostgreSQL and Redis volumes. Your database will be wiped.What's Next
- Environment Configuration — Configure Stripe keys, mail, OAuth, and other services.
- Installation — Go back to the setup steps if you haven't installed yet.