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:
| App | Repository | Serves |
|---|---|---|
| PRD-000 | zaixos-company-platform | Company site, staff admin, engineering docs |
| PRD-001 | Dental Clinic Revenue Operating System | Clinic 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.
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 --> PRD0012. URL map — local to production
| Local (development) | Production | App | Purpose |
|---|---|---|---|
https://zaixos.com/ | https://zaixos.com/ | PRD-000 | Company marketing site |
https://zaixos.com/admin | https://zaixos.com/admin | PRD-000 | Company staff admin (Filament) |
https://zaixos.com/docs/ | https://zaixos.com/docs/ | PRD-000 | Engineering docs portal (static) |
https://platform.zaixos.com/platform/en | https://platform.zaixos.com/platform/en | PRD-001 | Clinic product marketing |
https://clinic.zaixos.com/en | https://clinic.zaixos.com/en | PRD-001 | Clinic product showcase |
https://clinic.zaixos.com/signup | https://clinic.zaixos.com/signup | PRD-001 | New clinic signup |
https://account.zaixos.com/admin/login | https://account.zaixos.com/admin/login | PRD-001 | Unified identity login |
{slug}.clinic.zaixos.com | {slug}.zaixos.com | PRD-001 | Per-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:
- User visits
https://clinic.zaixos.com/admin/login(or any tenant admin login). - Middleware
RedirectGuestAuthScreensToIdentityredirects to:https://account.zaixos.com/admin/login?redirect=https%3A%2F%2Fclinic.zaixos.com%2Fadmin%2Flogin - User authenticates on
account.*. - Session cookie is scoped to
.zaixos.com(set bySaasServiceProviderfromTENANCY_BASE_DOMAIN). - User is redirected back to the original URL.
Required production settings (PRD-001 .env):
TENANCY_BASE_DOMAIN=zaixos.com
SESSION_DOMAIN=.zaixos.com
APP_URL=https://account.zaixos.comAll 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
- Order Cloud VPS 10 with Ubuntu 24.04 (EU region).
- Note the server public IP from the Contabo panel (needed for DNS).
- SSH in as
rootwith the password from your order email. - Enable UFW firewall on the server: allow 22, 80, 443 only.
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable- Point
zaixos.comDNS to this IP (Section 5).
Server software stack
Install on the server:
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/composerVerify:
php -v # 8.2+
psql --version # 15+
redis-cli ping # PONG
nginx -v5. DNS records
Point all records to your Contabo server public IP (YOUR_SERVER_IP):
| Type | Name | Value |
|---|---|---|
| A | zaixos.com | YOUR_SERVER_IP |
| A | www | YOUR_SERVER_IP |
| A | account | YOUR_SERVER_IP |
| A | platform | YOUR_SERVER_IP |
| A | clinic | YOUR_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:
| App | Path |
|---|---|
| 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
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 buildPRD-001 production .env (minimum)
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.comDatabase, migrations, seeders
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 onlyCache config and permissions
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:linkQueue worker (Supervisor)
/etc/supervisor/conf.d/dental-clinic-worker.conf:
[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.logsudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start dental-clinic-worker:*Cron (scheduler)
sudo crontab -u www-data -eAdd:
* * * * * cd /var/www/dental-clinic-ros && php artisan schedule:run >> /dev/null 2>&1Validate PRD-001
php artisan pilot:validate-deployment --strictFull 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
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 buildPRD-000 production .env (minimum)
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>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:linkEngineering docs portal
Build and publish static docs to public/docs/:
cd zaixos-engineering-platform
npm run docs:sync && npm run docs:deploy && npm run docs:verifyThis copies the VitePress build into zaixos-company-platform/public/docs/ so https://zaixos.com/docs/ works.
9. Nginx configuration
SSL certificates
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.comFor 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:
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:
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:
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 nginxNote: 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:
| # | URL | Expected |
|---|---|---|
| 1 | https://zaixos.com/ | Company homepage 200 |
| 2 | https://zaixos.com/admin/login | Company admin login 200 |
| 3 | https://zaixos.com/docs/ | Docs portal loads |
| 4 | https://platform.zaixos.com/platform/en | Product marketing 200 |
| 5 | https://clinic.zaixos.com/en | Clinic showcase 200 |
| 6 | https://clinic.zaixos.com/signup | Signup form 200 |
| 7 | https://account.zaixos.com/admin/login | Unified login 200 |
| 8 | https://pilot-dental.zaixos.com/en | Tenant site 200 (after pilot seed) |
| 9 | https://account.zaixos.com/up | Health check 200 |
Quick health checks:
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 --strict11. Deployment order summary
- Provision Contabo Cloud VPS 10 (Ubuntu 24.04, EU, firewall, SSH).
- Point DNS records to server IP.
- Install stack (Nginx, PHP 8.3, Postgres, Redis, Supervisor).
- Deploy PRD-001 — migrate, seed, queue worker, cron, validate.
- Deploy PRD-000 — migrate, seed, build assets.
- Publish docs (
npm run docs:deploy). - Configure Nginx + SSL (Certbot).
- Run smoke test checklist (Section 10).
12. When to upgrade
| Signal | Action |
|---|---|
| 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 high | Move PostgreSQL to dedicated server or managed DB |
| Traffic spikes | Add second PRD-001 app server behind load balancer; keep DB central |
13. Related documentation
| Document | Scope |
|---|---|
| Local Operations | Development URLs on *.zaixos.com |
| Customer #1 manual test setup | Local setup, auth, cron, mail, keys |
| Customer #1 go-live runbook | Customer #1 production GO/NO-GO |
| Customer #1 smoke results | Latest smoke execution report |
| production-runbook-prd-001 | PRD-001 legacy operational runbook |
PRD-001 config/zaixos.php | Domain URL derivation |
PRD-001 docs/development/LOCAL_DEVELOPMENT_RUNTIME.md | Local runtime (frozen) |
Breadcrumbs: Home → Deployment → Production Deployment Guide