Deployment Guide
MonkeysLegion v2 — Deployment Guide
Complete production checklist and environment-specific deployment instructions for MonkeysLegion v2 applications built on PHP 8.4+.
Table of Contents
- Production Checklist
- Shared VPS / Dedicated Server
- Docker / Container Orchestration
- Platform-as-a-Service (PaaS)
- Serverless (AWS Lambda / Bref)
- CI/CD Pipeline Reference
- Monitoring & Observability
- Rollback Strategy
1 · Production Checklist
Complete every item before going live — regardless of environment type.
1.1 Environment & Secrets
| # | Item | Command / Action | Notes |
|---|---|---|---|
| 1 | Set APP_ENV=production | .env or host env vars | Never development in prod |
| 2 | Disable debug mode | APP_DEBUG=false | Leaks stack traces if true |
| 3 | Generate application key | php vendor/bin/ml key:generate | Unique per environment |
| 4 | Generate strong JWT secret | php -r "echo bin2hex(random_bytes(32));" | Set in JWT_SECRET |
| 5 | Set APP_URL | APP_URL=https://yourdomain.com | Used for URL generation |
| 6 | Protect .env file | chmod 600 .env | Must never be web-accessible |
| 7 | Remove .env.example from deploy | Exclude in CI artifact | Contains default secrets |
1.2 PHP Runtime
| # | Item | Recommended Value | Notes |
|---|---|---|---|
| 1 | PHP version | 8.4+ | Property hooks, readonly classes |
| 2 | OPcache enabled | opcache.enable=1 | Mandatory for performance |
| 3 | OPcache preloading | opcache.preload=preload.php | Optional, significant gains |
| 4 | JIT compilation | opcache.jit=1255 | Tracing JIT for max throughput |
| 5 | realpath_cache_size | 4096K | Reduces filesystem stat calls |
| 6 | memory_limit | 256M | Tune per workload |
| 7 | max_execution_time | 30 | Prevent runaway requests |
| 8 | display_errors | Off | Always off in production |
| 9 | expose_php | Off | Hide PHP version header |
Required PHP extensions:
pdo_mysql (or pdo_pgsql) mbstring openssl json ctype
tokenizer fileinfo bcmath intl redis (if using Redis)
1.3 Database
| # | Item | Action |
|---|---|---|
| 1 | Use dedicated credentials | Never use root in production |
| 2 | Restrict privileges | Grant only SELECT, INSERT, UPDATE, DELETE to app user |
| 3 | Run migrations | php vendor/bin/ml schema:update |
| 4 | Enable SSL connections | Set DB_SSL_CA / PDO SSL options |
| 5 | Connection pooling | Use ProxySQL or PgBouncer for high traffic |
| 6 | Automated backups | Daily snapshots with point-in-time recovery |
| 7 | Set DB_CHARSET=utf8mb4 | Full Unicode support |
1.4 Security Hardening
| # | Item | Configuration |
|---|---|---|
| 1 | HTTPS only | Redirect all HTTP → HTTPS |
| 2 | HSTS header | Strict-Transport-Security: max-age=31536000; includeSubDomains |
| 3 | Session cookies | SESSION_COOKIE_SECURE=true, SESSION_COOKIE_HTTPONLY=true, SESSION_COOKIE_SAMESITE=Lax |
| 4 | CORS lockdown | Replace allow_origin = ["*"] with exact domains in config/cors.mlc |
| 5 | CSRF protection | Enabled by default — verify VerifyCsrfToken in middleware pipeline |
| 6 | Rate limiting | Configure RateLimitMiddleware thresholds |
| 7 | Auth public paths | Remove "*" from auth.public_paths in config/auth.mlc |
| 8 | Content Security Policy | Add CSP headers via middleware |
| 9 | File upload limits | Review files.max_bytes and files.mime_allow in config/files.mlc |
| 10 | Hide server tokens | Remove X-Powered-By, set Server header to generic |
1.5 Performance
| # | Item | Configuration |
|---|---|---|
| 1 | Cache driver | CACHE_DRIVER=redis (never file in clustered setups) |
| 2 | Session driver | SESSION_DRIVER=redis or database for multi-server |
| 3 | Queue driver | QUEUE_CONNECTION=redis or database (never sync) |
| 4 | Template cache | CACHE_VIEWS=true in config/cache.mlc |
| 5 | Composer autoloader | composer install --optimize-autoloader --no-dev |
| 6 | Asset minification | Minify CSS/JS, enable gzip/brotli compression |
| 7 | Static file caching | Set Cache-Control / Expires headers for /assets/ |
1.6 Logging & Monitoring
| # | Item | Configuration |
|---|---|---|
| 1 | Log level | LOG_LEVEL=warning (or error for minimal noise) |
| 2 | Log rotation | Use daily channel — rotate & compress old logs |
| 3 | External logging | Ship to ELK / Datadog / CloudWatch |
| 4 | Error tracking | Integrate Sentry or Bugsnag |
| 5 | Health endpoint | Create /health route returning 200 + DB/Redis status |
| 6 | Uptime monitoring | External ping service (UptimeRobot, Pingdom) |
1.7 Pre-Deploy Validation
# Run the full check suite before deploying
composer check # PSR-12 lint + PHPStan level 9 + all tests
# Individual checks
composer cs # Code style (PSR-12)
composer phpstan # Static analysis (level 9)
composer test # PHPUnit full suite
composer test:unit # Unit tests only
composer test:feature # Feature / HTTP tests
2 · Shared VPS / Dedicated Server
Classic deployment on Ubuntu/Debian with Nginx + PHP-FPM.
2.1 System Requirements
Ubuntu 24.04 LTS (or Debian 12+)
PHP 8.4-fpm
Nginx 1.24+
MySQL 8.4+ or PostgreSQL 16+
Redis 7+ (recommended)
Composer 2.7+
Git
2.2 Nginx Configuration
server {
listen 443 ssl http2;
server_name yourdomain.com;
root /var/www/monkeyslegion/public;
index index.php;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# 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" always;
# Static assets with long cache
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2?|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Block dotfiles (except .well-known)
location ~ /\.(?!well-known) {
deny all;
}
# Front controller
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
}
# Block access to sensitive files
location ~ /\.(env|git|mlc) {
deny all;
}
}
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
2.3 PHP-FPM Pool (/etc/php/8.4/fpm/pool.d/monkeyslegion.conf)
[monkeyslegion]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
php_admin_value[display_errors] = Off
php_admin_value[expose_php] = Off
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.memory_consumption] = 256
php_admin_value[opcache.max_accelerated_files] = 20000
php_admin_value[opcache.validate_timestamps] = 0
php_admin_value[realpath_cache_size] = 4096K
php_admin_value[realpath_cache_ttl] = 600
2.4 Deployment Script
#!/usr/bin/env bash
set -euo pipefail
DEPLOY_DIR="/var/www/monkeyslegion"
REPO="git@github.com:your-org/your-app.git"
BRANCH="main"
echo "▸ Pulling latest code…"
cd "$DEPLOY_DIR"
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
echo "▸ Installing dependencies…"
composer install --no-dev --optimize-autoloader --no-interaction
echo "▸ Running migrations…"
php vendor/bin/ml schema:update --force
echo "▸ Clearing caches…"
rm -rf var/cache/*
rm -rf var/sessions/* 2>/dev/null || true
echo "▸ Setting permissions…"
chown -R www-data:www-data var/ storage/ public/files/
chmod -R 775 var/ storage/ public/files/
chmod 600 .env
echo "▸ Restarting PHP-FPM…"
sudo systemctl reload php8.4-fpm
echo "✔ Deployment complete!"
2.5 Directory Permissions
public/ → 755 (web root, read-only)
public/files/ → 775 (user uploads)
var/log/ → 775 (log files)
var/cache/ → 775 (compiled templates, cache)
var/sessions/ → 775 (file sessions)
storage/ → 775 (app storage)
.env → 600 (secrets, owner-only)
config/ → 750 (config files)
3 · Docker / Container Orchestration
Docker Compose for staging, Kubernetes or ECS for production clusters.
3.1 Production Dockerfile
# ── Build Stage ──────────────────────────────────────────────
FROM composer:2 AS deps
WORKDIR /build
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts --ignore-platform-reqs
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative
# ── Runtime Stage ────────────────────────────────────────────
FROM php:8.4-fpm-alpine AS runtime
# Install extensions
RUN apk add --no-cache icu-libs libpq \
&& docker-php-ext-install pdo_mysql intl opcache bcmath \
&& pecl install redis && docker-php-ext-enable redis
# OPcache tuning
RUN echo "opcache.enable=1\n\
opcache.memory_consumption=256\n\
opcache.max_accelerated_files=20000\n\
opcache.validate_timestamps=0\n\
opcache.jit=1255\n\
opcache.jit_buffer_size=128M\n\
expose_php=Off\n\
display_errors=Off" > /usr/local/etc/php/conf.d/production.ini
WORKDIR /app
COPY --from=deps /build /app
# Writable dirs
RUN mkdir -p var/log var/cache var/sessions storage public/files \
&& chown -R www-data:www-data var/ storage/ public/files/
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
3.2 Docker Compose (Staging)
services:
app:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
env_file: .env.staging
volumes:
- app-storage:/app/storage
- app-logs:/app/var/log
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./public:/app/public:ro
- ./docker/nginx/certs:/etc/nginx/certs:ro
depends_on:
- app
networks:
- backend
db:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD:-}
volumes:
- redis-data:/data
networks:
- backend
queue-worker:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
env_file: .env.staging
command: php vendor/bin/ml queue:work --sleep=3 --tries=3 --max-jobs=1000
depends_on:
- db
- redis
networks:
- backend
volumes:
db-data:
redis-data:
app-storage:
app-logs:
networks:
backend:
driver: bridge
3.3 Kubernetes Highlights
# Key points for K8s deployment (abbreviated)
apiVersion: apps/v1
kind: Deployment
metadata:
name: monkeyslegion-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
spec:
containers:
- name: php-fpm
image: registry.example.com/ml-app:latest
resources:
requests: { cpu: "250m", memory: "256Mi" }
limits: { cpu: "1000m", memory: "512Mi" }
readinessProbe:
httpGet: { path: /health, port: 80 }
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: { path: /health, port: 80 }
initialDelaySeconds: 15
periodSeconds: 30
envFrom:
- secretRef: { name: ml-app-secrets }
- configMapRef: { name: ml-app-config }
Key Kubernetes considerations:
- Use
Secretsfor all.envvalues — never bake into images SESSION_DRIVER=redis— file sessions don't work across podsCACHE_DRIVER=redis— shared cache across replicasQUEUE_CONNECTION=redis— single queue worker deployment- Run migrations as a
Job(not in the app container init) - Mount
var/log/to a persistent volume or ship to stdout
4 · Platform-as-a-Service (PaaS)
Deploy to Railway, Render, DigitalOcean App Platform, or similar.
4.1 Railway / Render
railway.json / render.yaml essentials:
{
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"startCommand": "php-fpm",
"healthcheckPath": "/health",
"restartPolicyType": "ON_FAILURE"
}
}
Environment variables — set via the platform dashboard:
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:...
APP_URL=https://your-app.railway.app
DB_HOST=<managed-db-host>
DB_DATABASE=<db-name>
DB_USERNAME=<db-user>
DB_PASSWORD=<db-pass>
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=<managed-redis-host>
LOG_LEVEL=warning
JWT_SECRET=<generated-secret>
4.2 DigitalOcean App Platform
# .do/app.yaml
name: monkeyslegion-app
services:
- name: web
dockerfile_path: Dockerfile
http_port: 80
instance_count: 2
instance_size_slug: professional-xs
routes:
- path: /
envs:
- key: APP_ENV
value: production
- key: APP_KEY
type: SECRET
value: "base64:..."
health_check:
http_path: /health
databases:
- name: db
engine: MYSQL
version: "8"
size: db-s-1vcpu-1gb
4.3 PaaS Checklist
| # | Item | Notes |
|---|---|---|
| 1 | Use managed database | Auto-backups, failover, SSL |
| 2 | Use managed Redis | For cache, sessions, and queues |
| 3 | Set all env vars in dashboard | Never commit .env to repo |
| 4 | Configure custom domain + SSL | Platform usually provides free SSL |
| 5 | Set up build hook | composer install --no-dev --optimize-autoloader |
| 6 | Set up release hook | php vendor/bin/ml schema:update --force |
| 7 | Configure health checks | Point to /health endpoint |
| 8 | Enable auto-scaling | If platform supports it |
5 · Serverless (AWS Lambda / Bref)
Run MonkeysLegion on AWS Lambda using Bref.
5.1 serverless.yml
service: monkeyslegion-app
provider:
name: aws
region: us-east-1
runtime: provided.al2
environment:
APP_ENV: production
APP_DEBUG: "false"
APP_KEY: ${ssm:/ml-app/app-key}
DB_HOST: ${ssm:/ml-app/db-host}
DB_DATABASE: ${ssm:/ml-app/db-name}
DB_USERNAME: ${ssm:/ml-app/db-user}
DB_PASSWORD: ${ssm:/ml-app/db-pass}
CACHE_DRIVER: redis
SESSION_DRIVER: redis
QUEUE_CONNECTION: sqs
LOG_CHANNEL: stderr
plugins:
- ./vendor/bref/bref
functions:
web:
handler: public/index.php
timeout: 28
memorySize: 1024
layers:
- ${bref:layer.php-84-fpm}
events:
- httpApi: "*"
queue:
handler: worker.php
timeout: 120
memorySize: 512
layers:
- ${bref:layer.php-84}
events:
- sqs:
arn: !GetAtt JobsQueue.Arn
resources:
Resources:
JobsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: ml-jobs
VisibilityTimeout: 120
5.2 Serverless Considerations
| # | Item | Notes |
|---|---|---|
| 1 | No local filesystem | Use S3 for uploads, DynamoDB/Redis for sessions |
| 2 | Cold starts | Keep memory ≥ 1024MB, use provisioned concurrency |
| 3 | Timeout limit | API Gateway = 29s max — offload long tasks to queue |
| 4 | Logs go to CloudWatch | Set LOG_CHANNEL=stderr |
| 5 | Database connections | Use RDS Proxy to pool connections |
| 6 | Static assets | Serve via CloudFront + S3, not Lambda |
| 7 | Migrations | Run as a separate CLI Lambda invoke |
6 · CI/CD Pipeline Reference
6.1 GitHub Actions
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: ml_test
ports: ["3306:3306"]
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-retries=5
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
extensions: pdo_mysql, mbstring, intl, redis, bcmath
coverage: xdebug
- run: composer install --no-interaction
- run: composer check
env:
DB_HOST: 127.0.0.1
DB_DATABASE: ml_test
DB_USERNAME: root
DB_PASSWORD: test
deploy:
needs: test
runs-on: ubuntu-latest
if: success()
steps:
- uses: actions/checkout@v4
# Add your deployment step here:
# - SSH deploy, Docker push, serverless deploy, etc.
6.2 Pipeline Stages
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Lint & │───▸│ Test & │───▸│ Build │───▸│ Deploy │
│ Analyse │ │ Coverage │ │ Artifact │ │ Release │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
PSR-12 CS PHPUnit composer ssh / docker
PHPStan L9 Coverage ≥80% install push / sls
--no-dev deploy
7 · Monitoring & Observability
7.1 Health Check Endpoint
Create a /health route in your application:
#[Route('GET', '/health', name: 'health.check')]
public function health(): Response
{
$checks = [
'status' => 'ok',
'php' => PHP_VERSION,
'time' => date('c'),
];
// Optional: verify database connectivity
try {
$this->db->query('SELECT 1');
$checks['database'] = 'connected';
} catch (\Throwable) {
$checks['database'] = 'error';
$checks['status'] = 'degraded';
}
$code = $checks['status'] === 'ok' ? 200 : 503;
return Response::json($checks, $code);
}
7.2 Recommended Stack
| Layer | Tools |
|---|---|
| APM | Datadog, New Relic, or Elastic APM |
| Error tracking | Sentry, Bugsnag, or Rollbar |
| Log aggregation | ELK Stack, Loki + Grafana, or CloudWatch |
| Uptime | UptimeRobot, Pingdom, or Better Uptime |
| Metrics | Prometheus + Grafana or Datadog |
| Alerting | PagerDuty, Opsgenie, or Slack webhooks |
8 · Rollback Strategy
8.1 Quick Rollback Procedures
VPS / Bare Metal:
cd /var/www/monkeyslegion
git log --oneline -5 # Find previous commit
git reset --hard <previous-commit> # Revert code
composer install --no-dev --optimize-autoloader
sudo systemctl reload php8.4-fpm
Docker:
docker pull registry.example.com/ml-app:<previous-tag>
docker compose up -d --no-deps app
Kubernetes:
kubectl rollout undo deployment/monkeyslegion-app
kubectl rollout status deployment/monkeyslegion-app
Serverless:
serverless rollback --timestamp <previous-timestamp>
8.2 Database Rollback
Warning: Always test migration rollbacks in staging first.
- Keep migration files idempotent when possible
- Maintain a
down()method for every migration - Take a database snapshot before every deploy
- For breaking schema changes, use expand-contract migration pattern
Quick Reference Card
┌─────────────────────────────────────────────────────────────┐
│ DEPLOY QUICK REF │
├─────────────────────────────────────────────────────────────┤
│ APP_ENV=production APP_DEBUG=false │
│ php vendor/bin/ml key:generate │
│ composer install --no-dev --optimize-autoloader │
│ php vendor/bin/ml schema:update --force │
│ CACHE_DRIVER=redis SESSION_DRIVER=redis │
│ QUEUE_CONNECTION=redis │
│ LOG_LEVEL=warning │
│ opcache.enable=1 opcache.validate_timestamps=0 │
│ chmod 600 .env │
│ Verify /health returns 200 │
└─────────────────────────────────────────────────────────────┘
MonkeysLegion v2 — Built with 🐵 by MonkeysCloud Team