14 KiB
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
# From project root
docker build -f Dockerfile.prod -t trip-planner:latest .
# Check image size
docker images trip-planner:latest
Using Docker Compose
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
mainbranch - Pipeline extracts version from merge commit (e.g., from
release/v0.1.0) - Tagged as both
latestand version number (e.g.,0.1.0)
Running the Container
Using Docker Compose (Recommended)
# 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
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
# 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
# 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)
# 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):
# Create .env file in project root
APP_KEY=base64:...
DB_PASSWORD=secret
APP_URL=https://tripplanner.example.com
Docker Run:
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
# 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
# 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:
# 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
# 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
# Laravel Artisan
docker exec trip-planner-production php artisan <command>
# 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
# 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
-
Install Docker:
curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo usermod -aG docker $USER -
Create deployment directory:
mkdir -p ~/trip-planner cd ~/trip-planner -
Create docker-compose.yml:
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: -
Create .env file:
echo "APP_KEY=base64:$(openssl rand -base64 32)" > .env echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env -
Start the application:
docker compose up -d -
Set up reverse proxy (optional, recommended): Use Nginx, Caddy, or Traefik to handle HTTPS.
With Reverse Proxy (Nginx Example)
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:
sudo certbot --nginx -d tripplanner.example.com
Troubleshooting
Container won't start
Check logs:
docker compose -f docker-compose.prod.yml logs
Common issues:
- Missing
APP_KEY: Generate one withphp artisan key:generate - Port already in use: Change
APP_PORTin docker-compose - Insufficient memory: Allocate at least 1GB RAM
Database initialization fails
Manually initialize:
docker exec -it trip-planner-production sh
mysql_install_db --user=mysql --datadir=/var/lib/mysql
Services not responding
Check Supervisord status:
docker exec trip-planner-production supervisorctl status
Restart a service:
docker exec trip-planner-production supervisorctl restart nginx
docker exec trip-planner-production supervisorctl restart php-fpm
Permission errors
Fix storage permissions:
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:
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:
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:
# 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
- Change default passwords in
.env - Use strong APP_KEY (generate with
php artisan key:generate) - Enable HTTPS with a reverse proxy
- Firewall rules: Only expose necessary ports
- Regular updates: Pull latest images regularly
- Monitor logs: Set up log aggregation
- Backup regularly: Automate volume backups
Environment Variable Security
Never commit secrets to git!
# .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:
# .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 main0.1.0,0.2.0, etc.: Version tags
Need Help?
- Check the main docker/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