trip-planner/docker/prod
2025-11-16 15:53:52 +01:00
..
README.md 2 - tmp 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

# 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 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

# 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:

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

  1. Install Docker:

    curl -fsSL https://get.docker.com -o get-docker.sh
    sudo sh get-docker.sh
    sudo usermod -aG docker $USER
    
  2. Create deployment directory:

    mkdir -p ~/trip-planner
    cd ~/trip-planner
    
  3. 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:
    
  4. Create .env file:

    echo "APP_KEY=base64:$(openssl rand -base64 32)" > .env
    echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env
    
  5. Start the application:

    docker compose up -d
    
  6. 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 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:

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:

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!

# .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 main
  • 0.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