559 lines
14 KiB
Markdown
559 lines
14 KiB
Markdown
# 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`
|