T
Tenanto
Documentation / Deployment

Deployment

Updated Jan 25, 2026

Deployment Guide

This document provides step-by-step instructions for deploying Tenanto to production environments.


Table of Contents

  1. Prerequisites
  2. Docker Production Deployment
  3. Environment Configuration
  4. Database Setup
  5. Application Deployment
  6. Web Server Configuration
  7. Queue Workers
  8. Scheduler
  9. SSL/TLS Configuration
  10. Monitoring
  11. Scaling
  12. Staging Environment
  13. Troubleshooting

Prerequisites

Server Requirements

Component Minimum Recommended
PHP 8.4+ 8.4
PostgreSQL 15+ 16
Redis 7+ 7
RAM 2 GB 4 GB+
CPU 2 cores 4+ cores
Storage 20 GB 50 GB+ SSD

Required PHP Extensions

bcmath
ctype
curl
dom
fileinfo
gd
intl
json
mbstring
openssl
pcntl
pdo
pdo_pgsql
redis
tokenizer
xml
zip

Install Dependencies

# Ubuntu/Debian
sudo apt update
sudo apt install -y \
    php8.4-fpm php8.4-cli php8.4-pgsql php8.4-redis \
    php8.4-bcmath php8.4-intl php8.4-gd php8.4-xml \
    php8.4-curl php8.4-zip php8.4-mbstring \
    nginx postgresql-16 redis-server supervisor

# Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

# Install Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

Docker Production Deployment

For containerized deployments, Tenanto includes production-ready Docker configurations:

Production Files

File Purpose
docker-compose.prod.yml Production Docker Compose without exposed DB/Redis ports
docker/php/php-prod.ini Hardened PHP configuration for production

Quick Start (Docker)

# Copy production compose file
cp docker-compose.prod.yml docker-compose.override.yml

# Or run directly with production config
docker compose -f docker-compose.prod.yml up -d

# Run migrations
docker compose exec app php artisan migrate --force

# Optimize for production
docker compose exec app php artisan optimize

Security Features

The production Docker configuration includes:

Environment Variables

Ensure these are set in your .env for production Docker:

APP_ENV=production
APP_DEBUG=false
DB_HOST=db
REDIS_HOST=redis
REDIS_PASSWORD=your-secure-redis-password

Environment Configuration

1. Create Application Directory

sudo mkdir -p /var/www/tenanto
sudo chown -R www-data:www-data /var/www/tenanto

2. Clone Repository

cd /var/www/tenanto
# Clone from your repository
git clone https://your-git-host.com/your-org/tenanto.git .

3. Configure Environment

cp .env.example .env

Critical Production Settings:

# Application
APP_NAME="Tenanto"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com

# Security
APP_KEY=  # Generate with: php artisan key:generate

# Database
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=tenanto
DB_USERNAME=tenanto
DB_PASSWORD=your-secure-password

# Cache & Session
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

# Redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

# Mail (example: SMTP)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME="Tenanto"

# Stripe (Production Keys!)
STRIPE_KEY=pk_live_xxx
STRIPE_SECRET=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

# Plan Price IDs (Production)
STRIPE_PRICE_BASIC=price_xxx
STRIPE_PRICE_PRO=price_xxx
STRIPE_PRICE_ENTERPRISE=price_xxx

# Tenancy
TENANCY_MODE=subdomain
TENANT_CENTRAL_DOMAINS=yourdomain.com,admin.yourdomain.com

# Logging
LOG_CHANNEL=daily
LOG_LEVEL=warning

4. Install Dependencies

# PHP dependencies (no dev packages)
composer install --optimize-autoloader --no-dev

# Node.js dependencies and build
npm ci
npm run build

# Generate key
php artisan key:generate

5. Set Permissions

sudo chown -R www-data:www-data /var/www/tenanto
sudo chmod -R 755 /var/www/tenanto
sudo chmod -R 775 /var/www/tenanto/storage
sudo chmod -R 775 /var/www/tenanto/bootstrap/cache

Database Setup

1. Create Database and User

sudo -u postgres psql
CREATE USER tenanto WITH PASSWORD 'your-secure-password';
CREATE DATABASE tenanto OWNER tenanto;
GRANT ALL PRIVILEGES ON DATABASE tenanto TO tenanto;
\q

2. Configure PostgreSQL SSL (Recommended)

For production environments, enable SSL connections to PostgreSQL:

# Generate or obtain SSL certificates for PostgreSQL
# Option 1: Self-signed (for internal use)
openssl req -new -x509 -days 365 -nodes -text \
    -out /etc/postgresql/16/main/server.crt \
    -keyout /etc/postgresql/16/main/server.key \
    -subj "/CN=postgres-server"

sudo chown postgres:postgres /etc/postgresql/16/main/server.{crt,key}
sudo chmod 600 /etc/postgresql/16/main/server.key

Edit /etc/postgresql/16/main/postgresql.conf:

ssl = on
ssl_cert_file = '/etc/postgresql/16/main/server.crt'
ssl_key_file = '/etc/postgresql/16/main/server.key'

Edit /etc/postgresql/16/main/pg_hba.conf to require SSL:

# Require SSL for all connections
hostssl all all 0.0.0.0/0 scram-sha-256

Update Laravel .env for SSL connection:

DB_SSLMODE=require
# Or for certificate verification:
# DB_SSLMODE=verify-full
# DB_SSLROOTCERT=/path/to/ca-certificate.crt

Restart PostgreSQL:

sudo systemctl restart postgresql

3. Run Migrations

php artisan migrate --force

3. Seed Initial Data (Optional)

# Create system admin
php artisan db:seed --class=RoleAndPermissionSeeder

# Or seed demo data
php artisan db:seed

4. Create System Admin

php artisan tinker
$admin = \App\Models\User::create([
    'name' => 'System Admin',
    'email' => '[email protected]',
    'password' => bcrypt('secure-password'),
    'email_verified_at' => now(),
    'tenant_id' => null, // System admin has no tenant
]);
$admin->assignRole('system-admin');
exit

Application Deployment

1. Optimize for Production

# Cache configuration
php artisan config:cache

# Cache routes
php artisan route:cache

# Cache views
php artisan view:cache

# Cache events
php artisan event:cache

# Optimize autoloader
composer dump-autoload --optimize

2. Storage Link

php artisan storage:link

3. Health Check

curl http://localhost/health
# Should return: {"status":"ok"}

Web Server Configuration

Nginx Configuration

Create /etc/nginx/sites-available/tenanto:

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com *.yourdomain.com;
    return 301 https://$host$request_uri;
}

# Main HTTPS server
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name yourdomain.com *.yourdomain.com;
    root /var/www/tenanto/public;
    index index.php;

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_stapling on;
    ssl_stapling_verify on;

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Content Security Policy (adjust based on requirements)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.stripe.com wss:; frame-src https://js.stripe.com https://hooks.stripe.com; object-src 'none'; base-uri 'self'; form-action 'self';" always;

    # Permissions Policy
    add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(self), usb=()" always;

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript
               application/xml application/rss+xml application/atom+xml image/svg+xml;

    # Max upload size
    client_max_body_size 100M;

    # Logging
    access_log /var/log/nginx/tenanto.access.log;
    error_log /var/log/nginx/tenanto.error.log;

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

    # PHP-FPM
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_read_timeout 300;
    }

    # Deny access to sensitive files
    location ~ /\. {
        deny all;
    }

    location ~ ^/(\.env|composer\.json|composer\.lock|package\.json) {
        deny all;
    }

    # Static assets caching
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/tenanto /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Queue Workers (Laravel Horizon)

Tenanto uses Laravel Horizon for queue management, providing a dashboard, job metrics, and automatic worker scaling.

Supervisor Configuration

Create /etc/supervisor/conf.d/tenanto-horizon.conf:

[program:tenanto-horizon]
process_name=%(program_name)s
command=php /var/www/tenanto/artisan horizon
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/tenanto/storage/logs/horizon.log
stopwaitsecs=3600

Start Horizon:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start tenanto-horizon

Horizon Dashboard

Access the Horizon dashboard at /horizon (e.g., https://admin.yourdomain.com/horizon).

Access Control: Only system administrators (users without tenant with super-admin role) can access the dashboard in production.

Horizon Configuration

Key settings in config/horizon.php:

'environments' => [
    'production' => [
        'supervisor-1' => [
            'maxProcesses' => 10,      // Adjust based on server resources
            'balanceMaxShift' => 1,
            'balanceCooldown' => 3,
        ],
    ],
],

Queue Priorities

Tenanto uses prioritized queues:

Queue Priority Used For
high 1st Stripe webhooks, critical billing
default 2nd Emails, notifications
low 3rd Reports, cleanup jobs

Deploying Horizon Updates

After deployment, restart Horizon to pick up code changes:

php artisan horizon:terminate
# Supervisor will auto-restart Horizon

Scheduler

Add to crontab (crontab -e as www-data):

* * * * * cd /var/www/tenanto && php artisan schedule:run >> /dev/null 2>&1

Or create a systemd timer:

/etc/systemd/system/tenanto-scheduler.service:

[Unit]
Description=Tenanto Scheduler
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/tenanto
ExecStart=/usr/bin/php artisan schedule:work
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable tenanto-scheduler
sudo systemctl start tenanto-scheduler

SSL/TLS Configuration

Let's Encrypt with Certbot

# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Get wildcard certificate
sudo certbot certonly --manual \
    -d yourdomain.com \
    -d '*.yourdomain.com' \
    --preferred-challenges dns

# Auto-renewal
sudo certbot renew --dry-run

Certificate Auto-Renewal

Add to crontab:

0 0 1 * * certbot renew --quiet && systemctl reload nginx

Monitoring

Health Check Endpoints

Sentry Integration

# .env
SENTRY_LARAVEL_DSN=https://[email protected]/xxx
composer require sentry/sentry-laravel
php artisan sentry:publish

Log Aggregation

Configure in .env:

LOG_CHANNEL=daily
LOG_LEVEL=warning
LOG_DEPRECATIONS_CHANNEL=null

For centralized logging, consider:


Scaling

Horizontal Scaling

  1. Load Balancer - Use nginx, HAProxy, or cloud LB
  2. Session Storage - Redis (already configured)
  3. Cache - Redis cluster
  4. Database - PostgreSQL replication
  5. File Storage - S3 or similar object storage

Database Optimization

-- Useful indexes for tenant isolation
CREATE INDEX CONCURRENTLY idx_projects_tenant_archived
    ON projects (tenant_id, is_archived) WHERE deleted_at IS NULL;

CREATE INDEX CONCURRENTLY idx_tasks_tenant_project_status
    ON tasks (tenant_id, project_id, status) WHERE deleted_at IS NULL;

PHP-FPM Tuning

/etc/php/8.4/fpm/pool.d/www.conf:

pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500

OPcache Settings

/etc/php/8.4/fpm/conf.d/10-opcache.ini:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.save_comments=1

Deployment Checklist

Before Deployment

During Deployment

# 1. Enable maintenance mode
php artisan down --secret="your-secret-token"

# 2. Pull latest code
git pull origin main

# 3. Install dependencies
composer install --optimize-autoloader --no-dev
npm ci && npm run build

# 4. Run migrations
php artisan migrate --force

# 5. Clear and rebuild caches
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# 6. Restart Horizon (queue workers)
php artisan horizon:terminate
# Supervisor will auto-restart Horizon

# 7. Disable maintenance mode
php artisan up

After Deployment


Rollback Procedure

If deployment fails:

# 1. Enable maintenance mode
php artisan down

# 2. Revert code
git checkout HEAD~1

# 3. Reinstall dependencies
composer install --optimize-autoloader --no-dev

# 4. Rollback migrations (if necessary)
php artisan migrate:rollback --step=1

# 5. Clear caches
php artisan cache:clear
php artisan config:cache
php artisan route:cache

# 6. Restart workers
sudo supervisorctl restart tenanto-worker:*

# 7. Disable maintenance mode
php artisan up

Backup Strategy

Database Backup

# Daily backup script with encryption
#!/bin/bash
set -e

BACKUP_DIR="/var/backups/tenanto"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="db_$TIMESTAMP.sql.gz"
ENCRYPTED_FILE="db_$TIMESTAMP.sql.gz.gpg"

# Create backup directory if not exists
mkdir -p "$BACKUP_DIR"

# Dump and compress database
pg_dump -U tenanto tenanto | gzip > "$BACKUP_DIR/$BACKUP_FILE"

# Encrypt backup for off-site storage (optional but recommended)
# First, import your GPG key: gpg --import backup-key.pub
GPG_RECIPIENT="${GPG_BACKUP_KEY:[email protected]}"
if gpg --list-keys "$GPG_RECIPIENT" > /dev/null 2>&1; then
    gpg --encrypt --recipient "$GPG_RECIPIENT" \
        --output "$BACKUP_DIR/$ENCRYPTED_FILE" \
        "$BACKUP_DIR/$BACKUP_FILE"

    # Remove unencrypted backup after successful encryption
    rm "$BACKUP_DIR/$BACKUP_FILE"

    echo "Encrypted backup created: $ENCRYPTED_FILE"
else
    echo "Warning: GPG key not found. Backup stored unencrypted."
fi

# Keep only last 7 days
find $BACKUP_DIR -name "db_*.sql.gz*" -mtime +7 -delete

# Optionally upload to S3 or remote storage
# aws s3 cp "$BACKUP_DIR/$ENCRYPTED_FILE" s3://your-backup-bucket/tenanto/

Add to crontab:

0 3 * * * /var/www/tenanto/scripts/backup.sh

Decrypting Backups

# To restore from encrypted backup
gpg --decrypt db_20250101_030000.sql.gz.gpg | gunzip | psql -U tenanto tenanto

File Backup

# Backup uploads with encryption
#!/bin/bash
BACKUP_DIR="/var/backups/tenanto/files"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# Sync files
rsync -avz /var/www/tenanto/storage/app/public/ "$BACKUP_DIR/"

# Create encrypted archive for off-site storage
tar -czf - -C /var/www/tenanto/storage/app public | \
    gpg --encrypt --recipient "${GPG_BACKUP_KEY:[email protected]}" \
    > "/var/backups/tenanto/files_$TIMESTAMP.tar.gz.gpg"

Staging Environment

A staging environment mirrors production and is essential for testing deployments before they go live.

Staging Server Setup

Set up a separate server (or container) that mirrors production:

# Create staging directory
sudo mkdir -p /var/www/tenanto-staging
sudo chown -R www-data:www-data /var/www/tenanto-staging

# Clone repository
cd /var/www/tenanto-staging
# Clone from your repository
git clone https://your-git-host.com/your-org/tenanto.git .
git checkout develop  # Or specific release branch

Staging Environment Configuration

Create .env for staging:

# Application
APP_NAME="Tenanto Staging"
APP_ENV=staging
APP_DEBUG=true  # Allowed in staging for debugging
APP_URL=https://staging.yourdomain.com

# Database (separate from production!)
DB_DATABASE=tenanto_staging
DB_USERNAME=tenanto_staging
DB_PASSWORD=staging-password

# Cache prefix to avoid conflicts
CACHE_PREFIX=staging_
REDIS_PREFIX=tenanto_staging_

# Stripe TEST keys (never use production keys!)
STRIPE_KEY=pk_test_xxx
STRIPE_SECRET=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_test_xxx

# Test price IDs
STRIPE_PRICE_BASIC=price_test_basic
STRIPE_PRICE_PRO=price_test_pro
STRIPE_PRICE_ENTERPRISE=price_test_enterprise

# Tenancy
TENANCY_MODE=subdomain
TENANT_CENTRAL_DOMAINS=staging.yourdomain.com,admin.staging.yourdomain.com

# Email (use sandbox/testing service)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=mailtrap-username
MAIL_PASSWORD=mailtrap-password

# Logging (more verbose for staging)
LOG_CHANNEL=daily
LOG_LEVEL=debug

DNS Configuration for Staging

Configure DNS records:

# A records
staging.yourdomain.com        -> staging-server-ip
*.staging.yourdomain.com      -> staging-server-ip

# For tenant subdomains
tenant1.staging.yourdomain.com
tenant2.staging.yourdomain.com

Nginx Configuration for Staging

Create /etc/nginx/sites-available/tenanto-staging:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name staging.yourdomain.com *.staging.yourdomain.com;
    root /var/www/tenanto-staging/public;
    index index.php;

    # SSL (can use separate staging certificate)
    ssl_certificate /etc/letsencrypt/live/staging.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/staging.yourdomain.com/privkey.pem;

    # Basic auth to protect staging (optional but recommended)
    auth_basic "Staging Environment";
    auth_basic_user_file /etc/nginx/.htpasswd-staging;

    # Disable basic auth for webhooks
    location /stripe/webhook {
        auth_basic off;
        try_files $uri $uri/ /index.php?$query_string;
    }

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

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~ /\. {
        deny all;
    }
}

Create staging password:

sudo htpasswd -c /etc/nginx/.htpasswd-staging staging-user

Staging Database Setup

sudo -u postgres psql
CREATE USER tenanto_staging WITH PASSWORD 'staging-password';
CREATE DATABASE tenanto_staging OWNER tenanto_staging;
GRANT ALL PRIVILEGES ON DATABASE tenanto_staging TO tenanto_staging;
\q

Continuous Deployment to Staging

GitLab CI Configuration

Add to .gitlab-ci.yml:

stages:
  - test
  - deploy-staging
  - deploy-production

deploy_staging:
  stage: deploy-staging
  script:
    - ssh deploy@staging-server "cd /var/www/tenanto-staging && git pull origin develop"
    - ssh deploy@staging-server "cd /var/www/tenanto-staging && composer install --optimize-autoloader"
    - ssh deploy@staging-server "cd /var/www/tenanto-staging && npm ci && npm run build"
    - ssh deploy@staging-server "cd /var/www/tenanto-staging && php artisan migrate --force"
    - ssh deploy@staging-server "cd /var/www/tenanto-staging && php artisan config:cache"
    - ssh deploy@staging-server "cd /var/www/tenanto-staging && php artisan route:cache"
    - ssh deploy@staging-server "cd /var/www/tenanto-staging && php artisan view:cache"
    - ssh deploy@staging-server "sudo supervisorctl restart tenanto-staging-worker:*"
  only:
    - develop
  environment:
    name: staging
    url: https://staging.yourdomain.com

Manual Deployment Script

Create scripts/deploy-staging.sh:

#!/bin/bash
set -e

STAGING_DIR="/var/www/tenanto-staging"
BRANCH="${1:-develop}"

echo "Deploying branch: $BRANCH to staging..."

cd $STAGING_DIR

# Pull latest code
git fetch origin
git checkout $BRANCH
git pull origin $BRANCH

# Install dependencies
composer install --optimize-autoloader
npm ci && npm run build

# Run migrations
php artisan migrate --force

# Clear and rebuild caches
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Restart Horizon
php artisan horizon:terminate

echo "Staging deployment complete!"

Staging Horizon Configuration

Create /etc/supervisor/conf.d/tenanto-staging-horizon.conf:

[program:tenanto-staging-horizon]
process_name=%(program_name)s
command=php /var/www/tenanto-staging/artisan horizon
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/tenanto-staging/storage/logs/horizon.log
stopwaitsecs=3600

Staging Horizon dashboard: https://admin.staging.yourdomain.com/horizon

Testing in Staging

Pre-Production Checklist

Before promoting to production, verify in staging:

Automated Smoke Tests

Run smoke tests against staging:

# Health checks
curl -f https://staging.yourdomain.com/health
curl -f https://staging.yourdomain.com/ready

# API health
curl -f https://staging.yourdomain.com/api/v1/health

Data Seeding for Staging

Create staging-specific seeders:

# Seed staging with test data
php artisan db:seed --class=DemoDataSeeder

# Create test tenant
php artisan tenant:create "Test Company" test

# Create demo tenants
php artisan demo:create [email protected]

Promoting Staging to Production

Once staging is verified:

# 1. Tag the release
git tag -a v1.x.x -m "Release v1.x.x"
git push origin v1.x.x

# 2. Deploy to production
# (Follow the standard deployment checklist)

Staging Environment Isolation

Critical Security Notes:

  1. Never share databases - Staging must have its own database
  2. Never use production API keys - Always use Stripe test keys
  3. Protect staging access - Use basic auth or IP whitelisting
  4. Separate Redis instances - Use different Redis databases or prefixes
  5. Isolated file storage - Don't share storage with production

Staging Maintenance

# Reset staging database (start fresh)
php artisan migrate:fresh --seed

# Sync production data to staging (sanitized)
pg_dump -U tenanto tenanto | psql -U tenanto_staging tenanto_staging
# IMPORTANT: Run data sanitization script after sync!

# Clear staging data
php artisan cache:clear
php artisan queue:clear

Troubleshooting

Common Issues

500 Error

tail -f /var/www/tenanto/storage/logs/laravel.log
tail -f /var/log/nginx/tenanto.error.log

Permission Denied

sudo chown -R www-data:www-data /var/www/tenanto/storage
sudo chmod -R 775 /var/www/tenanto/storage

Queue/Horizon Not Processing

# Check Horizon status
sudo supervisorctl status tenanto-horizon
php artisan horizon:status

# Restart Horizon
php artisan horizon:terminate
sudo supervisorctl restart tenanto-horizon

# Check Horizon dashboard for failed jobs
# https://admin.yourdomain.com/horizon

Cache Issues

php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear

Debug Mode (Temporary)

# Enable temporarily for debugging
php artisan down
# Edit .env: APP_DEBUG=true
php artisan config:clear
# Test, then revert
# Edit .env: APP_DEBUG=false
php artisan config:cache
php artisan up

Related Documentation


Support

For deployment assistance, contact the Tenanto development team.