Deployment

Production Setup

Configure and optimize for production

Deploy Glueful to a production server with proper configuration and optimization.

Server Requirements

  • PHP 8.2+
  • Composer
  • Database (MySQL 8.0+, PostgreSQL 13+, or SQLite)
  • Web server (Nginx recommended)
  • Process supervisor (Supervisor, systemd)
  • Redis (optional, for caching/queues)

Installation

1. Clone Repository

cd /var/www
git clone https://github.com/your-org/your-app.git
cd your-app

2. Install Dependencies

composer install --no-dev --optimize-autoloader --classmap-authoritative

3. Configure Environment

cp .env.example .env
nano .env

Set production values:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://api.example.com

DB_DRIVER=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=production_db
DB_USERNAME=prod_user
DB_PASSWORD=secure_password

CACHE_DRIVER=redis
REDIS_HOST=localhost
REDIS_PORT=6379

QUEUE_CONNECTION=redis

TOKEN_SALT=generate-a-strong-random-salt
JWT_KEY=your-secure-jwt-key

4. Run Migrations

php glueful migrate:run

5. Set Permissions

chown -R www-data:www-data /var/www/your-app
chmod -R 755 /var/www/your-app
chmod -R 775 storage/

Web Server Configuration

Nginx

/etc/nginx/sites-available/your-app:

server {
    listen 80;
    server_name api.example.com;
    root /var/www/your-app/public;

    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 = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

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

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

Enable site:

ln -s /etc/nginx/sites-available/your-app /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

SSL/TLS with Let's Encrypt

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com

Updated Nginx config (SSL):

server {
    listen 443 ssl http2;
    server_name api.example.com;
    root /var/www/your-app/public;

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

    # ... rest of config
}

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

PHP-FPM Configuration

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

[www]
user = www-data
group = www-data

listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data

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

Restart PHP-FPM:

systemctl restart php8.2-fpm

Queue Workers

Supervisor Configuration

/etc/supervisor/conf.d/glueful-worker.conf:

[program:glueful-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/your-app/vendor/bin/glueful queue:work
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/worker.log
stopwaitsecs=3600

Start workers:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start glueful-worker:*

Task Scheduler

Add to crontab:

crontab -e -u www-data
* * * * * cd /var/www/your-app && php glueful queue:scheduler run >> /dev/null 2>&1

Database Optimization

MySQL Configuration

/etc/mysql/my.cnf:

[mysqld]
max_connections = 100
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
# MySQL 8 removes query cache; avoid deprecated settings
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT

Create Database

CREATE DATABASE production_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'prod_user'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON production_db.* TO 'prod_user'@'localhost';
FLUSH PRIVILEGES;

Redis Configuration

/etc/redis/redis.conf:

bind 127.0.0.1
port 6379
maxmemory 256mb
maxmemory-policy allkeys-lru

Restart Redis:

systemctl restart redis

PHP Configuration

/etc/php/8.2/fpm/php.ini:

memory_limit = 256M
upload_max_filesize = 20M
post_max_size = 20M
max_execution_time = 60

; OPcache
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0

Logging

Application Logs

mkdir -p storage/logs
touch storage/logs/app.log
chown www-data:www-data storage/logs/app.log

Log Rotation

/etc/logrotate.d/glueful:

/var/www/your-app/storage/logs/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data www-data
    sharedscripts
}

Monitoring

Health Endpoints (Built-in)

Glueful provides production-ready health routes out of the box; no custom route is required.

  • Overall: GET /health
  • Database: GET /health/database
  • Cache: GET /health/cache
  • Liveness: GET /healthz
  • Readiness: GET /ready (IP allowlist enforced)
  • Detailed metrics: GET /health/detailed (auth + permissions required)

Quick checks:

curl -fsS http://localhost/healthz
curl -fsS http://localhost/health | jq '.data.status'
curl -fsS http://localhost/ready -I

Security configuration:

# Allow only these IPs to access /ready (comma-separated)
HEALTH_IP_ALLOWLIST=127.0.0.1,10.0.0.0/8
# Require auth for health endpoints (optional)
HEALTH_AUTH_REQUIRED=false

Notes:

  • /health/detailed is protected and requires authentication and appropriate permissions.
  • Health routes include sensible rate limiting; use them with monitoring systems and load balancers.

Monitor Queue

# Check worker status
sudo supervisorctl status glueful-worker:*

# Monitor queue depth
# Note: Uses the configured Redis prefix (default: glueful:queue:)
# Default example key: glueful:queue:queue:default
redis-cli LLEN glueful:queue:queue:default

Backups

Database Backup Script

/usr/local/bin/backup-db.sh:

#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/database"
mkdir -p $BACKUP_DIR

mysqldump -u prod_user -p'secure_password' production_db > $BACKUP_DIR/backup_$DATE.sql
gzip $BACKUP_DIR/backup_$DATE.sql

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

Make executable:

chmod +x /usr/local/bin/backup-db.sh

Add to crontab:

0 2 * * * /usr/local/bin/backup-db.sh

Security

Firewall

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Disable Unnecessary Services

sudo systemctl disable apache2
sudo systemctl stop apache2

Secure PHP

; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen

; Hide PHP version
expose_php = Off

Performance Optimization

Enable OPcache

Already configured in php.ini above.

Composer Optimization

composer dump-autoload --optimize --classmap-authoritative

Database Indexes

Ensure all frequently queried columns are indexed:

$table->index('email');
$table->index('status');
$table->index(['user_id', 'created_at']);

Deployment Script

deploy.sh:

#!/bin/bash
set -e

echo "Pulling latest code..."
git pull origin main

echo "Installing dependencies..."
composer install --no-dev --optimize-autoloader --classmap-authoritative

echo "Running migrations..."
php vendor/bin/glueful migrate:run

echo "Production audit..."
php vendor/bin/glueful system:production --check || true

echo "(Optional) Compile DI container..."
php vendor/bin/glueful di:container:compile --optimize || true

echo "Restarting services..."
sudo supervisorctl restart glueful-worker:*

echo "Deployment complete!"

Make executable:

chmod +x deploy.sh

Troubleshooting

Check PHP-FPM Status

systemctl status php8.2-fpm
tail -f /var/log/php8.2-fpm.log

Check Nginx Status

systemctl status nginx
tail -f /var/log/nginx/error.log

Check Worker Status

sudo supervisorctl status
tail -f storage/logs/worker.log

Check Database Connection

php -r "new PDO('mysql:host=localhost;dbname=production_db', 'prod_user', 'secure_password');"

Next Steps