# Trip Planner - Production Environment This document describes the production Docker setup for Trip Planner. ## Overview The production environment uses a **single all-in-one container** that includes: - ✅ Frontend (React SPA built and served by Nginx) - ✅ Backend (Laravel API with PHP-FPM) - ✅ Database (MariaDB) - ✅ Cache/Sessions (Redis) - ✅ Web Server (Nginx as reverse proxy) All services are managed by **Supervisord** within a single container. ## Architecture ``` ┌─────────────────────────────────────────────────┐ │ trip-planner-production (Single Container) │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ Nginx (Port 80) │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ Frontend │ │ Backend API │ │ │ │ │ │ Static Files│ │ PHP-FPM:9000│ │ │ │ │ │ (/) │ │ (/api/*) │ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ MariaDB │ │ Redis │ │ │ │ localhost │ │ localhost │ │ │ │ :3306 │ │ :6379 │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ Managed by Supervisord │ └─────────────────────────────────────────────────┘ │ └─ Port 80 (or configured APP_PORT) ``` ## Key Features ### Security - ✅ Non-root users for services where possible - ✅ Minimal Alpine-based image - ✅ Database and Redis bound to localhost only - ✅ Security headers configured - ✅ OPcache enabled with production settings - ✅ PHP display_errors disabled ### Optimization - ✅ Multi-stage build (smaller image size ~800MB) - ✅ OPcache with no timestamp validation - ✅ Gzip compression enabled - ✅ Static asset caching (1 year) - ✅ Optimized Composer autoloader ### Reliability - ✅ Health checks configured - ✅ Automatic service restart via Supervisord - ✅ Persistent data volumes for database, redis, and storage - ✅ Proper initialization and migration on startup ## Building the Image ### Locally ```bash # From project root docker build -f Dockerfile.prod -t trip-planner:latest . # Check image size docker images trip-planner:latest ``` ### Using Docker Compose ```bash docker compose -f docker-compose.prod.yml build ``` ### CI/CD (Automatic) The image is automatically built and pushed to Codeberg Container Registry when: - Changes are merged to `main` branch - Pipeline extracts version from merge commit (e.g., from `release/v0.1.0`) - Tagged as both `latest` and version number (e.g., `0.1.0`) ## Running the Container ### Using Docker Compose (Recommended) ```bash # Start the container docker compose -f docker-compose.prod.yml up -d # View logs docker compose -f docker-compose.prod.yml logs -f # Stop the container docker compose -f docker-compose.prod.yml down # Stop and remove volumes (⚠️ deletes data!) docker compose -f docker-compose.prod.yml down -v ``` ### Using Docker Run ```bash docker run -d \ --name trip-planner \ -p 8080:80 \ -e APP_KEY=base64:your-key-here \ -e DB_PASSWORD=secure-password \ -e DB_USERNAME=trip_user \ -e DB_DATABASE=trip_planner \ -v trip-planner-db:/var/lib/mysql \ -v trip-planner-redis:/data/redis \ -v trip-planner-storage:/var/www/html/storage/app \ trip-planner:latest ``` ### Using the Published Image ```bash # Pull from Codeberg Container Registry docker pull codeberg.org/lvl0/trip-planner:latest # Or a specific version docker pull codeberg.org/lvl0/trip-planner:0.1.0 # Run it docker run -d \ --name trip-planner \ -p 8080:80 \ -e APP_KEY=base64:your-key-here \ -e DB_PASSWORD=secure-password \ codeberg.org/lvl0/trip-planner:latest ``` ## Environment Variables ### Required Variables ```env # Application Key (generate with: php artisan key:generate) APP_KEY=base64:your-generated-key-here # Database Credentials DB_PASSWORD=your-secure-password DB_USERNAME=trip_user DB_DATABASE=trip_planner ``` ### Optional Variables (with defaults) ```env # Application APP_NAME="Trip Planner" APP_ENV=production APP_DEBUG=false APP_URL=http://localhost:8080 # Database (internal MariaDB) DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 # Redis (internal) REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD=null # Cache & Session CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=redis SESSION_LIFETIME=120 # Mail MAIL_MAILER=log MAIL_FROM_ADDRESS=noreply@tripplanner.local MAIL_FROM_NAME="Trip Planner" # Ports APP_PORT=8080 # External port to expose ``` ### Setting Environment Variables **Docker Compose (recommended):** ```yaml # Create .env file in project root APP_KEY=base64:... DB_PASSWORD=secret APP_URL=https://tripplanner.example.com ``` **Docker Run:** ```bash docker run -e APP_KEY=base64:... -e DB_PASSWORD=secret ... ``` ## Persistent Data The production setup uses three volumes for persistent data: | Volume | Purpose | Path in Container | |--------|---------|-------------------| | `db-data` | MariaDB database files | `/var/lib/mysql` | | `redis-data` | Redis persistence | `/data/redis` | | `storage-data` | User uploads, files | `/var/www/html/storage/app` | ### Backup ```bash # Backup database docker exec trip-planner-production mysqldump -u trip_user -p trip_planner > backup.sql # Backup volumes docker run --rm \ -v trip-planner-db-data:/data \ -v $(pwd):/backup \ alpine tar czf /backup/db-backup.tar.gz /data # Backup uploaded files docker run --rm \ -v trip-planner-storage-data:/data \ -v $(pwd):/backup \ alpine tar czf /backup/storage-backup.tar.gz /data ``` ### Restore ```bash # Restore database docker exec -i trip-planner-production mysql -u trip_user -p trip_planner < backup.sql # Restore volumes docker run --rm \ -v trip-planner-db-data:/data \ -v $(pwd):/backup \ alpine sh -c "cd / && tar xzf /backup/db-backup.tar.gz" ``` ## Health Checks The container includes a health check endpoint: ```bash # Check container health docker inspect trip-planner-production | grep -A 5 Health # Manual health check curl http://localhost:8080/health # Should return: healthy # Check specific services docker exec trip-planner-production supervisorctl status ``` ## Accessing Services When the container is running: - **Application**: http://localhost:8080 (or your configured `APP_PORT`) - **Health Check**: http://localhost:8080/health - **API**: http://localhost:8080/api/* ### Internal Services (not exposed) These services run inside the container and are not accessible from outside: - MariaDB: `127.0.0.1:3306` - Redis: `127.0.0.1:6379` - PHP-FPM: `127.0.0.1:9000` ## Maintenance ### View Logs ```bash # All services docker compose -f docker-compose.prod.yml logs -f # Specific service logs via supervisord docker exec trip-planner-production supervisorctl tail -f nginx docker exec trip-planner-production supervisorctl tail -f php-fpm docker exec trip-planner-production supervisorctl tail -f mariadb # Laravel logs docker exec trip-planner-production tail -f /var/www/html/storage/logs/laravel.log ``` ### Execute Commands ```bash # Laravel Artisan docker exec trip-planner-production php artisan # Examples: docker exec trip-planner-production php artisan migrate:status docker exec trip-planner-production php artisan cache:clear docker exec trip-planner-production php artisan queue:work # Run queue worker # Database access docker exec -it trip-planner-production mysql -u trip_user -p trip_planner # Shell access docker exec -it trip-planner-production sh ``` ### Update Application ```bash # Pull latest image docker pull codeberg.org/lvl0/trip-planner:latest # Recreate container (preserves volumes) docker compose -f docker-compose.prod.yml up -d --force-recreate # Or specific version docker pull codeberg.org/lvl0/trip-planner:0.2.0 docker compose -f docker-compose.prod.yml up -d ``` ## Deployment ### On a VPS/Server 1. **Install Docker**: ```bash curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo usermod -aG docker $USER ``` 2. **Create deployment directory**: ```bash mkdir -p ~/trip-planner cd ~/trip-planner ``` 3. **Create docker-compose.yml**: ```yaml version: '3.8' services: app: image: codeberg.org/lvl0/trip-planner:latest container_name: trip-planner ports: - "8080:80" environment: APP_KEY: ${APP_KEY} DB_PASSWORD: ${DB_PASSWORD} APP_URL: https://your-domain.com volumes: - db-data:/var/lib/mysql - redis-data:/data/redis - storage-data:/var/www/html/storage/app restart: unless-stopped volumes: db-data: redis-data: storage-data: ``` 4. **Create .env file**: ```bash echo "APP_KEY=base64:$(openssl rand -base64 32)" > .env echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env ``` 5. **Start the application**: ```bash docker compose up -d ``` 6. **Set up reverse proxy** (optional, recommended): Use Nginx, Caddy, or Traefik to handle HTTPS. ### With Reverse Proxy (Nginx Example) ```nginx server { listen 80; server_name tripplanner.example.com; location / { proxy_pass http://localhost:8080; 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; } } ``` Then use Certbot for HTTPS: ```bash sudo certbot --nginx -d tripplanner.example.com ``` ## Troubleshooting ### Container won't start **Check logs:** ```bash docker compose -f docker-compose.prod.yml logs ``` **Common issues:** - Missing `APP_KEY`: Generate one with `php artisan key:generate` - Port already in use: Change `APP_PORT` in docker-compose - Insufficient memory: Allocate at least 1GB RAM ### Database initialization fails **Manually initialize:** ```bash docker exec -it trip-planner-production sh mysql_install_db --user=mysql --datadir=/var/lib/mysql ``` ### Services not responding **Check Supervisord status:** ```bash docker exec trip-planner-production supervisorctl status ``` **Restart a service:** ```bash docker exec trip-planner-production supervisorctl restart nginx docker exec trip-planner-production supervisorctl restart php-fpm ``` ### Permission errors **Fix storage permissions:** ```bash docker exec trip-planner-production chown -R appuser:appuser /var/www/html/storage docker exec trip-planner-production chmod -R 775 /var/www/html/storage ``` ### Health check failing **Test manually:** ```bash docker exec trip-planner-production curl -f http://localhost/health # Check individual services docker exec trip-planner-production supervisorctl status ``` ### Performance issues **Check resource usage:** ```bash docker stats trip-planner-production # Allocate more resources if needed (docker-compose) # Add under 'app' service: # deploy: # resources: # limits: # memory: 2G ``` ## Testing Production Locally To test the production setup alongside your dev environment: ```bash # Production runs on port 8080 (default) docker compose -f docker-compose.prod.yml up -d # Dev runs on separate ports (5173, 8000, etc.) docker compose -f docker-compose.dev.yml up -d # Both can run simultaneously without conflicts ``` Access: - Production: http://localhost:8080 - Development: http://localhost:5173 ## Security Considerations ### Securing the Production Deployment 1. **Change default passwords** in `.env` 2. **Use strong APP_KEY** (generate with `php artisan key:generate`) 3. **Enable HTTPS** with a reverse proxy 4. **Firewall rules**: Only expose necessary ports 5. **Regular updates**: Pull latest images regularly 6. **Monitor logs**: Set up log aggregation 7. **Backup regularly**: Automate volume backups ### Environment Variable Security **Never commit secrets to git!** ```bash # .env files are gitignored # Use a secrets manager for production # Or use Docker secrets/Kubernetes secrets ``` ## Performance Tips - The image includes OPcache with aggressive caching - Static assets are cached for 1 year - Gzip compression is enabled - Redis handles sessions and cache - Database is optimized for InnoDB For high-traffic scenarios: - Run multiple container replicas behind a load balancer - Use external managed database (RDS, etc.) - Use external Redis cluster - Configure CDN for static assets ## Differences from Development | Feature | Development | Production | |---------|------------|------------| | Containers | 5 separate | 1 all-in-one | | Code | Live mounted | Baked into image | | Frontend | Vite dev server | Pre-built static files | | Debugging | Enabled | Disabled | | Caching | Minimal | Aggressive | | Security | Relaxed | Hardened | | Size | ~2GB+ | ~800MB | ## CI/CD Integration Images are automatically built via Woodpecker CI on Codeberg: ```yaml # .woodpecker.yml extracts version from merge commits # Example: merging release/v0.1.0 → tags 0.1.0 and latest ``` **Registry**: `codeberg.org/lvl0/trip-planner` **Tags**: - `latest`: Most recent build from main - `0.1.0`, `0.2.0`, etc.: Version tags ## Need Help? - Check the main [docker/README.md](../README.md) - Review container logs: `docker logs trip-planner-production` - Check service status: `docker exec trip-planner-production supervisorctl status` - Inspect health: `docker inspect trip-planner-production`