Knowledge Portal · engineering documentation

Skip to content

Production Deployment Guide

Status: Canonical workspace runbook
Last updated: 2026-07-13
Audience: Developer / DevOps engineer
Production domain: zaixos.com
Hosting: Contabo Cloud VPS 10

This guide explains how to deploy the full ZAIXOS company stack on a single production server. It covers both Laravel applications, all public URLs, unified login, DNS, Nginx, and post-deploy verification.

For PRD-001-only deep operational steps (queue, mail, seeders, validation), see the Customer #1 Go-Live Runbook (federated from PRD-001).


1. Mental model — not one website

ZAIXOS production is two Laravel applications sharing a domain naming scheme:

AppRepositoryServes
PRD-000zaixos-company-platformCompany site, staff admin, engineering docs
PRD-001Dental Clinic Revenue Operating SystemClinic product, identity login, platform marketing, tenant clinics

Locally, Laravel Herd routes each *.zaixos.com hostname to the correct project. On a server, you recreate that with DNS + Nginx + two .env files.

mermaid
flowchart TB
    subgraph dns [DNS all point to server IP]
        APEX["zaixos.com"]
        ACC["account.zaixos.com"]
        PLAT["platform.zaixos.com"]
        CLINIC["clinic.zaixos.com"]
        TENANT["pilot-dental.zaixos.com"]
    end

    subgraph nginx [Nginx routes by hostname]
        N1["server block to PRD-000"]
        N2["server block to PRD-001"]
    end

    subgraph apps [Two Laravel apps]
        PRD000["zaixos-company-platform"]
        PRD001["Dental Clinic ROS"]
    end

    APEX --> N1 --> PRD000
    ACC --> N2
    PLAT --> N2
    CLINIC --> N2
    TENANT --> N2
    N2 --> PRD001

2. URL map — local to production

Local (development)ProductionAppPurpose
https://zaixos.com/https://zaixos.com/PRD-000Company marketing site
https://zaixos.com/adminhttps://zaixos.com/adminPRD-000Company staff admin (Filament)
https://zaixos.com/docs/https://zaixos.com/docs/PRD-000Engineering docs portal (static)
https://platform.zaixos.com/platform/enhttps://platform.zaixos.com/platform/enPRD-001Clinic product marketing
https://clinic.zaixos.com/enhttps://clinic.zaixos.com/enPRD-001Clinic product showcase
https://clinic.zaixos.com/signuphttps://clinic.zaixos.com/signupPRD-001New clinic signup
https://account.zaixos.com/admin/loginhttps://account.zaixos.com/admin/loginPRD-001Unified identity login
{slug}.clinic.zaixos.com{slug}.zaixos.comPRD-001Per-clinic tenant site + CRM

Tenant hostname note: Local dev uses nested hosts (demo.clinic.zaixos.com). Production Customer #1 uses flat hosts (pilot-dental.zaixos.com). Both formats are supported by PRD-001 tenancy resolution.

Domain configuration authority: PRD-001 config/zaixos.php and TENANCY_BASE_DOMAIN.


3. Unified login flow

All clinic admin logins converge on account.zaixos.com:

  1. User visits https://clinic.zaixos.com/admin/login (or any tenant admin login).
  2. Middleware RedirectGuestAuthScreensToIdentity redirects to: https://account.zaixos.com/admin/login?redirect=https%3A%2F%2Fclinic.zaixos.com%2Fadmin%2Flogin
  3. User authenticates on account.*.
  4. Session cookie is scoped to .zaixos.com (set by SaasServiceProvider from TENANCY_BASE_DOMAIN).
  5. User is redirected back to the original URL.

Required production settings (PRD-001 .env):

dotenv
TENANCY_BASE_DOMAIN=zaixos.com
SESSION_DOMAIN=.zaixos.com
APP_URL=https://account.zaixos.com

All subdomains must use HTTPS in production.


4. Contabo — start here

Yes — Contabo is a good place to begin. Order one server:

Contabo Cloud VPS 10 · 4 vCPU · 8 GB RAM · 75 GB NVMe · ~€4.40/month (12-month)

  • Region: European Union
  • OS: Ubuntu 24.04
  • Fits company + clinic + docs on one machine.

Provisioning checklist

  1. Order Cloud VPS 10 with Ubuntu 24.04 (EU region).
  2. Note the server public IP from the Contabo panel (needed for DNS).
  3. SSH in as root with the password from your order email.
  4. Enable UFW firewall on the server: allow 22, 80, 443 only.
bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
  1. Point zaixos.com DNS to this IP (Section 5).

Server software stack

Install on the server:

bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx postgresql postgresql-contrib redis-server supervisor cron \
  git unzip curl
sudo apt install -y php8.3-fpm php8.3-cli php8.3-pgsql php8.3-mbstring php8.3-xml \
  php8.3-curl php8.3-zip php8.3-bcmath php8.3-intl php8.3-redis
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

Verify:

bash
php -v          # 8.2+
psql --version  # 15+
redis-cli ping  # PONG
nginx -v

5. DNS records

Point all records to your Contabo server public IP (YOUR_SERVER_IP):

TypeNameValue
Azaixos.comYOUR_SERVER_IP
AwwwYOUR_SERVER_IP
AaccountYOUR_SERVER_IP
AplatformYOUR_SERVER_IP
AclinicYOUR_SERVER_IP
A*YOUR_SERVER_IP

The wildcard * record enables tenant clinics like pilot-dental.zaixos.com. If your DNS provider does not support wildcards, create per-clinic A records instead.

Allow DNS propagation (up to 24–48 hours; often faster).


6. Deploy paths

Use consistent paths on the server:

AppPath
PRD-001 (clinic)/var/www/dental-clinic-ros
PRD-000 (company)/var/www/zaixos-company-platform

7. Deploy PRD-001 — Clinic product

Repository: Dental Clinic Revenue Operating System

bash
cd /var/www
git clone <prd-001-repo-url> dental-clinic-ros
cd dental-clinic-ros

composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
npm ci && npm run build

PRD-001 production .env (minimum)

dotenv
APP_NAME="Dental Clinic ROS"
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:...                    # php artisan key:generate
APP_URL=https://account.zaixos.com

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=dental_clinic_ros
DB_USERNAME=dental_app
DB_PASSWORD=<strong-password>

SESSION_DRIVER=database
CACHE_STORE=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

TENANCY_ENABLED=true
TENANCY_BASE_DOMAIN=zaixos.com
TENANCY_ENFORCE_USER_CLINIC=true
TENANCY_PUBLIC_DEFAULT_CLINIC_FALLBACK=false

COMPANY_URL=https://zaixos.com
ACCOUNT_URL=https://account.zaixos.com
PLATFORM_URL=https://platform.zaixos.com
CLINIC_URL=https://clinic.zaixos.com

MAIL_MAILER=smtp
MAIL_HOST=smtp.yourprovider.com
MAIL_PORT=587
MAIL_USERNAME=<smtp-user>
MAIL_PASSWORD=<smtp-password>
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@zaixos.com

Database, migrations, seeders

bash
sudo -u postgres psql <<'SQL'
CREATE USER dental_app WITH PASSWORD 'REPLACE_STRONG_PASSWORD';
CREATE DATABASE dental_clinic_ros OWNER dental_app;
GRANT ALL PRIVILEGES ON DATABASE dental_clinic_ros TO dental_app;
SQL

php artisan migrate --force
php artisan db:seed --class=RolesAndPermissionsSeeder --force
php artisan db:seed --class=SaasPlanSeeder --force
php artisan db:seed --class=RegionSeeder --force
php artisan db:seed --class=PilotProductionSeeder --force   # Customer #1 pilot only

Cache config and permissions

bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
sudo chown -R www-data:www-data storage bootstrap/cache
php artisan storage:link

Queue worker (Supervisor)

/etc/supervisor/conf.d/dental-clinic-worker.conf:

ini
[program:dental-clinic-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/dental-clinic-ros/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --queue=default,ai
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/dental-clinic-ros/storage/logs/worker.log
bash
sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start dental-clinic-worker:*

Cron (scheduler)

bash
sudo crontab -u www-data -e

Add:

* * * * * cd /var/www/dental-clinic-ros && php artisan schedule:run >> /dev/null 2>&1

Validate PRD-001

bash
php artisan pilot:validate-deployment --strict

Full operational detail: Customer #1 go-live runbook · Manual test setup · legacy production-runbook-prd-001.


8. Deploy PRD-000 — Company platform

Repository: zaixos-company-platform

bash
cd /var/www
git clone <prd-000-repo-url> zaixos-company-platform
cd zaixos-company-platform

composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
npm ci && npm run build

PRD-000 production .env (minimum)

dotenv
APP_NAME="ZAIXOS Experience Platform"
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:...
APP_URL=https://zaixos.com
SESSION_DOMAIN=.zaixos.com

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=zaixos_company
DB_USERNAME=company_app
DB_PASSWORD=<strong-password>
bash
sudo -u postgres psql <<'SQL'
CREATE USER company_app WITH PASSWORD 'REPLACE_STRONG_PASSWORD';
CREATE DATABASE zaixos_company OWNER company_app;
GRANT ALL PRIVILEGES ON DATABASE zaixos_company TO company_app;
SQL

php artisan migrate --force
php artisan db:seed --class=App\\Modules\\Identity\\Database\\Seeders\\EngineeringAdminSeeder --force
php artisan config:cache && php artisan route:cache
sudo chown -R www-data:www-data storage bootstrap/cache
php artisan storage:link

Engineering docs portal

Build and publish static docs to public/docs/:

bash
cd zaixos-engineering-platform
npm run docs:sync && npm run docs:deploy && npm run docs:verify

This copies the VitePress build into zaixos-company-platform/public/docs/ so https://zaixos.com/docs/ works.


9. Nginx configuration

SSL certificates

bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d zaixos.com -d www.zaixos.com \
  -d account.zaixos.com -d platform.zaixos.com -d clinic.zaixos.com

For wildcard tenants, either add per-tenant certs or use a wildcard cert (*.zaixos.com) if your DNS provider supports DNS-01 challenge.

Server block 1 — Company (PRD-000)

/etc/nginx/sites-available/zaixos-company:

nginx
server {
    listen 443 ssl http2;
    server_name zaixos.com www.zaixos.com;
    root /var/www/zaixos-company-platform/public;

    ssl_certificate     /etc/letsencrypt/live/zaixos.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/zaixos.com/privkey.pem;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_param HTTPS on;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Server block 2 — Clinic product (PRD-001)

/etc/nginx/sites-available/zaixos-clinic:

nginx
server {
    listen 443 ssl http2;
    server_name account.zaixos.com platform.zaixos.com clinic.zaixos.com *.zaixos.com;
    root /var/www/dental-clinic-ros/public;

    ssl_certificate     /etc/letsencrypt/live/zaixos.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/zaixos.com/privkey.pem;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_param HTTPS on;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Enable and reload:

bash
sudo ln -s /etc/nginx/sites-available/zaixos-company /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/zaixos-clinic /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Note: Nginx server_name *.zaixos.com matches one label only (pilot-dental.zaixos.com). It does not match nested hosts like demo.clinic.zaixos.com — production uses flat tenant hosts.


10. Post-deploy smoke test

Run on the production host with real DNS:

#URLExpected
1https://zaixos.com/Company homepage 200
2https://zaixos.com/admin/loginCompany admin login 200
3https://zaixos.com/docs/Docs portal loads
4https://platform.zaixos.com/platform/enProduct marketing 200
5https://clinic.zaixos.com/enClinic showcase 200
6https://clinic.zaixos.com/signupSignup form 200
7https://account.zaixos.com/admin/loginUnified login 200
8https://pilot-dental.zaixos.com/enTenant site 200 (after pilot seed)
9https://account.zaixos.com/upHealth check 200

Quick health checks:

bash
curl -fsS -o /dev/null -w "%{http_code}" https://account.zaixos.com/up
curl -fsS -o /dev/null -w "%{http_code}" https://pilot-dental.zaixos.com/en
php artisan pilot:validate-deployment --strict

11. Deployment order summary

  1. Provision Contabo Cloud VPS 10 (Ubuntu 24.04, EU, firewall, SSH).
  2. Point DNS records to server IP.
  3. Install stack (Nginx, PHP 8.3, Postgres, Redis, Supervisor).
  4. Deploy PRD-001 — migrate, seed, queue worker, cron, validate.
  5. Deploy PRD-000 — migrate, seed, build assets.
  6. Publish docs (npm run docs:deploy).
  7. Configure Nginx + SSL (Certbot).
  8. Run smoke test checklist (Section 10).

12. When to upgrade

SignalAction
RAM consistently above 80%Upgrade to Cloud VPS 20, or split PRD-000 to a second server
Disk above 70%Add Contabo storage expansion or move backups off-server
DB load highMove PostgreSQL to dedicated server or managed DB
Traffic spikesAdd second PRD-001 app server behind load balancer; keep DB central

DocumentScope
Local OperationsDevelopment URLs on *.zaixos.com
Customer #1 manual test setupLocal setup, auth, cron, mail, keys
Customer #1 go-live runbookCustomer #1 production GO/NO-GO
Customer #1 smoke resultsLatest smoke execution report
production-runbook-prd-001PRD-001 legacy operational runbook
PRD-001 config/zaixos.phpDomain URL derivation
PRD-001 docs/development/LOCAL_DEVELOPMENT_RUNTIME.mdLocal runtime (frozen)

Breadcrumbs: HomeDeployment → Production Deployment Guide

ZAIXOS Knowledge Portal — public engineering docs at /docs · Staff operations at /admin