Deployment Guide
This document provides step-by-step instructions for deploying Tenanto to production environments.
Table of Contents
- Prerequisites
- Docker Production Deployment
- Environment Configuration
- Database Setup
- Application Deployment
- Web Server Configuration
- Queue Workers
- Scheduler
- SSL/TLS Configuration
- Monitoring
- Scaling
- Staging Environment
- 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:
- No exposed database ports - PostgreSQL and Redis only accessible within Docker network
- No Mailhog - Configure real SMTP provider (Mailgun, SES, Postmark)
- Hardened PHP settings (
php-prod.ini):display_errors = Offexpose_php = Offsession.cookie_secure = Ondisable_functionsfor dangerous functionsopen_basedirrestriction- OPcache optimized for production
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
/health- Basic health check (returns 200 if app is running)/ready- Readiness check (verifies database, cache, queue)
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:
- Papertrail
- Loggly
- Elasticsearch + Kibana
Scaling
Horizontal Scaling
- Load Balancer - Use nginx, HAProxy, or cloud LB
- Session Storage - Redis (already configured)
- Cache - Redis cluster
- Database - PostgreSQL replication
- 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
- Run all tests locally
- Review environment configuration
- Backup current production database
- Notify users of maintenance window
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
- Verify health check endpoints
- Test critical user flows
- Monitor error logs
- Check queue workers are processing
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:
- All migrations run successfully
- User registration and login work
- Tenant creation works
- Subdomain routing works
- Stripe test payments process correctly
- Email delivery works (check Mailtrap)
- Queue jobs process correctly
- API endpoints respond correctly
- No errors in application logs
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:
- Never share databases - Staging must have its own database
- Never use production API keys - Always use Stripe test keys
- Protect staging access - Use basic auth or IP whitelisting
- Separate Redis instances - Use different Redis databases or prefixes
- 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
- Security Guide - Security configuration
- Performance Guide - Optimization strategies
- Backup & Restore - Backup strategies
- Launch Checklist - Pre-launch checklist
Support
For deployment assistance, contact the Tenanto development team.