trip-planner/docker/prod/README.md

560 lines
14 KiB
Markdown
Raw Permalink Normal View History

2025-11-16 15:53:52 +01:00
# 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 <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
```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`