Self-Hosting with Docker Compose
Docker Compose is the recommended deployment method for operators who manage their own reverse proxy, firewall, or existing Docker host infrastructure.
Architecture Overview
Section titled “Architecture Overview”A standard production Compose deployment consists of four containers communicating over an internal Docker bridge network:
graph TD Client[Browser / Client] -->|HTTPS 443| Proxy[Reverse Proxy: Caddy / Nginx / Traefik] Proxy -->|HTTP 8000| App[KoAkademy FrankenPHP Octane] App -->|Port 5432| DB[(PostgreSQL 18)] App -->|Port 6379| Cache[(Redis 8)] App -->|Port 3000| PDF[Gotenberg 8 PDF Engine]- Application (
app): Runs PHP 8.5 on FrankenPHP (Laravel Octane), Supercronic (cron scheduler), and Supervisor worker processes on port8000. - PostgreSQL (
postgres): Relational database storage (PostgreSQL 18). - Redis (
redis): Cache, sessions, queue backplane, and real-time locking. - Gotenberg (
gotenberg): Headless Chromium and LibreOffice microservice on port3000for PDF generation (transcripts, certificates, and invoices).
Prerequisites
Section titled “Prerequisites”- Linux server (Ubuntu 22.04/24.04 LTS, Debian 12, Rocky Linux 9, or AlmaLinux 9).
- Docker Engine 24+ and Docker Compose v2.20+.
- Domain name pointing (A/AAAA records) to your server’s public IP address.
- At least 4 GB RAM and 2 vCPUs recommended for production workloads.
Step 1: Create Project Directory
Section titled “Step 1: Create Project Directory”Create a dedicated directory on your server:
sudo mkdir -p /opt/koakademysudo chown -R $USER:$USER /opt/koakademycd /opt/koakademyStep 2: Configure Environment Variables
Section titled “Step 2: Configure Environment Variables”Create .env based on the production template:
cat << 'EOF' > .env# Application SettingsAPP_NAME=KoAkademyAPP_ENV=productionAPP_DEBUG=falseAPP_URL=https://school.example.comAPP_KEY=
# Server & RuntimeAPP_PORT=8000OCTANE_SERVER=frankenphpAUTO_MIGRATE=trueRUN_OPTIMIZE=foreground
# Database (PostgreSQL)DB_CONNECTION=pgsqlDB_HOST=postgresDB_PORT=5432DB_DATABASE=koakademyDB_USERNAME=koakademyDB_PASSWORD=change_this_to_a_strong_database_password
# Cache, Sessions & Queues (Redis)CACHE_STORE=redisSESSION_DRIVER=redisQUEUE_CONNECTION=redisREDIS_HOST=redisREDIS_PORT=6379REDIS_PASSWORD=change_this_to_a_strong_redis_password
# PDF Generation EngineGOTENBERG_URL=http://gotenberg:3000
# Filesystem & Storage (local default, recommend S3/R2 for multi-node)FILESYSTEM_DISK=public
# Mail Configuration (Example: SMTP)MAIL_MAILER=smtpMAIL_HOST=smtp.mailgun.orgMAIL_PORT=587MAIL_USERNAME=postmaster@school.example.comMAIL_PASSWORD=your_smtp_passwordMAIL_ENCRYPTION=tlsMAIL_FROM_ADDRESS=notifications@school.example.comMAIL_FROM_NAME="KoAkademy Notification"EOFGenerate a secure application key:
docker run --rm ghcr.io/yukazakiri/koakademy:latest php artisan key:generate --showCopy the generated base64:... key and set it as APP_KEY in your .env file.
Step 3: Production compose.yaml
Section titled “Step 3: Production compose.yaml”Create your compose.yaml file:
name: koakademy
services: app: image: ghcr.io/yukazakiri/koakademy:latest restart: unless-stopped env_file: - .env ports: - "127.0.0.1:${APP_PORT:-8000}:8000" volumes: - app-storage:/app/storage depends_on: postgres: condition: service_healthy redis: condition: service_healthy gotenberg: condition: service_healthy healthcheck: test: ["CMD", "healthcheck"] interval: 30s timeout: 10s retries: 5 start_period: 45s
postgres: image: postgres:18-alpine restart: unless-stopped environment: POSTGRES_DB: ${DB_DATABASE:-koakademy} POSTGRES_USER: ${DB_USERNAME:-koakademy} POSTGRES_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env} volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 10s timeout: 5s retries: 5
redis: image: redis:8-alpine restart: unless-stopped command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}"] environment: REDIS_PASSWORD: ${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env} volumes: - redis-data:/data healthcheck: test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep PONG"] interval: 10s timeout: 5s retries: 5
gotenberg: image: gotenberg/gotenberg:8 restart: unless-stopped healthcheck: test: ["CMD", "curl", "--fail", "--silent", "http://localhost:3000/health"] interval: 15s timeout: 5s retries: 5
volumes: app-storage: postgres-data: redis-data:Step 4: Reverse Proxy Configuration
Section titled “Step 4: Reverse Proxy Configuration”The application container binds to 127.0.0.1:8000. You need a reverse proxy to terminate TLS/SSL.
Option A: Caddy (Recommended)
Section titled “Option A: Caddy (Recommended)”Caddy handles automatic HTTPS via Let’s Encrypt:
school.example.com { reverse_proxy 127.0.0.1:8000 { header_up Host {host} header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto {scheme} }}Reload Caddy:
sudo systemctl reload caddyOption B: Nginx
Section titled “Option B: Nginx”server { listen 80; server_name school.example.com; return 301 https://$host$request_uri;}
server { listen 443 ssl http2; server_name school.example.com;
ssl_certificate /etc/letsencrypt/live/school.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/school.example.com/privkey.pem;
client_max_body_size 100M;
location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port;
# Disable proxy buffering for streaming/Octane proxy_buffering off; proxy_read_timeout 300s; }}Step 5: Start Services and Initialize
Section titled “Step 5: Start Services and Initialize”Start the stack in detached mode:
docker compose up -dMonitor container startup and logs:
docker compose logs -f appOnce healthy, visit https://school.example.com/setup to complete the initial setup wizard and register the primary administrator.
Maintenance & Operations
Section titled “Maintenance & Operations”Run Database Migrations Manually
Section titled “Run Database Migrations Manually”docker compose exec app php artisan migrate --forceClear Application Cache
Section titled “Clear Application Cache”docker compose exec app php artisan optimize:cleardocker compose exec app php artisan optimizeUpgrading to a New Version
Section titled “Upgrading to a New Version”# Pull latest imagesdocker compose pull
# Restart services with zero-downtime rolling restartdocker compose up -d --remove-orphans
# Verify health statusdocker compose psDatabase Backup
Section titled “Database Backup”docker compose exec postgres pg_dump -U koakademy -d koakademy -F c > /opt/koakademy/backup_$(date +%Y%m%d_%H%M%S).dump