Skip to content
SaaS4Builders
Getting Started

Docker Setup

Understand the Docker services, networks, and volumes that power the SaaS4Builders development environment.

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

ServiceContainerImageExposed PortPurpose
phpsaas-phpphp:8.3-fpm-alpine (custom)— (internal 9000)Laravel application via PHP-FPM
nginxsaas-nginxnginx:alpine8000:80Reverse proxy for the API
nodesaas-nodenode:22-slim (custom, pnpm 10.26.2)3000:3000Nuxt 4 dev server with SSR and HMR
postgressaas-postgrespostgres:16-alpine5432:5432PostgreSQL database
redissaas-redisredis:7-alpine6379:6379Cache, sessions, and queues
mailpitsaas-mailpitaxllent/mailpit8025:8025, 1025:1025Email testing (SMTP + web UI)
queuesaas-queueSame as phpBackground job worker
schedulersaas-schedulerSame as phpLaravel task scheduler
reverbsaas-reverbSame as php8080:8080WebSocket 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_URL is set to http://nginx:80.
  • Client-side requests: When the browser makes API calls, it uses the host-accessible URL. The environment variable NUXT_PUBLIC_API_BASE_URL is set to http://localhost:8000.
SSR (server):  Nuxt → http://nginx:80/api/v1/...
Client (browser): Browser → http://localhost:8000/api/v1/...
These two URLs are set in 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:

TaskSchedulePurpose
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

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

VolumeTypeMount PointPurpose
postgres_dataNamed volume/var/lib/postgresql/dataPersistent database storage across container restarts
redis_dataNamed volume/dataPersistent Redis data
node_modulesNamed volume/app/node_modulesNode 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.

If you install a new npm package on your host, it will not appear inside the container. Always install packages inside the container: 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 PathContainer PathService
./backend/var/www/htmlphp, queue, scheduler, reverb
./frontend/appnode

Changes to files in backend/ and frontend/ on your host are immediately visible inside the containers.


Useful Make Commands

CommandAction
make upStart all containers in the background
make downStop all containers
make restartRestart all containers
make logsFollow logs from all containers
make logs-phpFollow PHP and Nginx logs
make logs-nodeFollow Node container logs
make logs-reverbFollow Reverb WebSocket logs
make shell-phpOpen a shell in the PHP container
make shell-nodeOpen a shell in the Node container
make statusShow 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:

VariableDefaultService
NGINX_PORT8000Backend API (Nginx)
NUXT_PORT3000Frontend (Nuxt dev server)
DB_PORT5432PostgreSQL
REDIS_PORT6379Redis
MAILPIT_UI_PORT8025Mailpit web UI
MAILPIT_SMTP_PORT1025Mailpit SMTP server
REVERB_PORT8080Reverb WebSocket server
HMR_PORT24679Nuxt hot module replacement

After changing a port, restart the containers:

make restart
If you change 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:

docker/php/Dockerfile
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:

  1. Clone the repository inside the WSL2 filesystem (e.g., ~/projects/), not on the Windows filesystem (/mnt/c/).
  2. Run all make commands from a WSL2 terminal.
  3. Access the application at http://localhost:3000 from 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