From a5307f3e5d5c87330ea32e303177c0c538cc4b63 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 16 Nov 2025 15:53:52 +0100 Subject: [PATCH] 2 - tmp --- .dockerignore | 63 ++++ .env | 6 + .woodpecker.yml | 100 +++++++ Dockerfile.prod | 134 +++++++++ backend/.dockerignore | 48 +++ docker-compose.prod.yml | 88 +++--- docker/README.md | 160 ++++++++++ docker/dev/README.md | 342 +++++++++++++++++++++ docker/entrypoint.sh | 141 +++++++++ docker/nginx/production.conf | 100 +++++++ docker/prod/README.md | 559 +++++++++++++++++++++++++++++++++++ docker/supervisord.conf | 66 +++++ frontend/.dockerignore | 51 ++++ 13 files changed, 1818 insertions(+), 40 deletions(-) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 .woodpecker.yml create mode 100644 Dockerfile.prod create mode 100644 backend/.dockerignore create mode 100644 docker/README.md create mode 100644 docker/dev/README.md create mode 100644 docker/entrypoint.sh create mode 100644 docker/nginx/production.conf create mode 100644 docker/prod/README.md create mode 100644 docker/supervisord.conf create mode 100644 frontend/.dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e17488b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,63 @@ +# Git +.git/ +.github/ +.gitignore +.gitattributes + +# CI/CD +.woodpecker.yml + +# Environment +.env +.env.* +!.env.example + +# Development +.editorconfig +.eslintrc* +.prettierrc* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Documentation +*.md +LICENSE + +# Docker +docker-compose*.yml + +# Claude files +.claude/ + +# Node modules (will be installed in build) +node_modules/ +frontend/node_modules/ + +# Backend vendor (will be installed in build) +vendor/ +backend/vendor/ + +# Build artifacts +frontend/dist/ +frontend/build/ + +# Cache and logs +backend/storage/logs/* +backend/storage/framework/cache/* +backend/storage/framework/sessions/* +backend/storage/framework/views/* +backend/bootstrap/cache/* +*.log + +# Tests +tests/ +backend/tests/ +frontend/tests/ + +# Data directories +docker/data/ diff --git a/.env b/.env new file mode 100644 index 0000000..5383247 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +APP_KEY=base64:dGVzdGluZ19rZXlfZm9yX3Byb2R1Y3Rpb25fdGVzdGluZ19vbmx5X25vdF9zZWN1cmU= +DB_PASSWORD=test_password_123 +MYSQL_ROOT_PASSWORD=root_password_456 +DB_USERNAME=trip_user +DB_DATABASE=trip_planner +APP_URL=http://localhost:8080 diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..ee13ced --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,100 @@ +--- +# Woodpecker CI Pipeline for Trip Planner +# Builds and pushes production Docker image to Codeberg Container Registry + +when: + - event: push + branch: main + +variables: + - &image_repo 'codeberg.org/lvl0/trip-planner' + +steps: + # Extract version from commit message if merging from release branch + - name: extract-version + image: alpine:latest + commands: + - | + # Install git and grep with PCRE support + apk add --no-cache git grep + + # Get the commit message to check if it's a merge from release branch + COMMIT_MSG=$(git log -1 --pretty=%B) + + # Try to extract version from merge commit message (e.g., "Merge branch 'release/v0.1.0'") + VERSION=$(echo "$COMMIT_MSG" | grep -oP "release/v?\K[0-9]+\.[0-9]+\.[0-9]+" || echo "") + + if [ -z "$VERSION" ]; then + # No version found, use commit SHA as version + VERSION="dev-$(git rev-parse --short HEAD)" + echo "No release version detected, using: $VERSION" + echo "$VERSION" > /woodpecker/version.txt + else + echo "Detected release version: $VERSION" + echo "$VERSION" > /woodpecker/version.txt + # Export for use in build step + echo "export VERSION_TAG=$VERSION" >> /tmp/version_env.sh + fi + + # Build and push with latest tag (always) + - name: build-and-push-latest + image: woodpeckerci/plugin-docker-buildx + settings: + repo: *image_repo + registry: codeberg.org + username: + from_secret: container_registry_username + password: + from_secret: container_registry_token + dockerfile: Dockerfile.prod + context: . + platforms: linux/amd64 + build_args: + - BUILDKIT_INLINE_CACHE=1 + auto_tag: false + tags: + - latest + depends_on: + - extract-version + + # Check if this is a release build + - name: check-release + image: alpine:latest + commands: + - | + VERSION=$(cat /woodpecker/version.txt) + if echo "$VERSION" | grep -qv "^dev-"; then + echo "Release version detected: $VERSION" + echo "true" > /woodpecker/is_release.txt + else + echo "Development build ($VERSION), will skip version tag" + echo "false" > /woodpecker/is_release.txt + fi + depends_on: + - build-and-push-latest + + # Notify build status + - name: notify-success + image: alpine:latest + commands: + - | + VERSION=$(cat /woodpecker/version.txt 2>/dev/null || echo "unknown") + IS_RELEASE=$(cat /woodpecker/is_release.txt 2>/dev/null || echo "false") + echo "✅ Successfully built and pushed trip-planner:latest" + echo "Version: $VERSION" + if [ "$IS_RELEASE" = "true" ]; then + echo "ℹ️ This is a release build. To tag with version $VERSION:" + echo " docker pull codeberg.org/lvl0/trip-planner:latest" + echo " docker tag codeberg.org/lvl0/trip-planner:latest codeberg.org/lvl0/trip-planner:$VERSION" + echo " docker push codeberg.org/lvl0/trip-planner:$VERSION" + fi + echo "Image: codeberg.org/lvl0/trip-planner:latest" + when: + status: success + + - name: notify-failure + image: alpine:latest + commands: + - echo "❌ Build failed! Check the logs above for details." + when: + status: failure diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..dbbefea --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,134 @@ +# ============================================================================= +# Stage 1: Build Frontend +# ============================================================================= +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend + +# Copy frontend package files +COPY frontend/package*.json ./ + +# Install dependencies (including dev for build) +RUN npm ci + +# Copy frontend source +COPY frontend/ ./ + +# Build frontend +RUN npm run build + +# ============================================================================= +# Stage 2: Build Backend Dependencies +# ============================================================================= +FROM php:8.3-fpm-alpine AS backend-builder + +# Install build dependencies +RUN apk add --no-cache \ + libpng-dev \ + oniguruma-dev \ + libxml2-dev \ + zip \ + unzip + +# Install PHP extensions +RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd opcache + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +# Copy composer files +COPY backend/composer.json backend/composer.lock ./ + +# Install PHP dependencies (production only) +RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist + +# Copy backend source +COPY backend/ ./ + +# Generate optimized autoloader +RUN composer dump-autoload --optimize --classmap-authoritative + +# ============================================================================= +# Stage 3: Final Production Image (All-in-One) +# ============================================================================= +FROM php:8.3-fpm-alpine + +# Install runtime dependencies +RUN apk add --no-cache \ + libpng \ + oniguruma \ + libxml2 \ + nginx \ + supervisor \ + mariadb \ + mariadb-client \ + redis \ + curl \ + bash + +# Copy PHP extensions from backend-builder +COPY --from=backend-builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ +COPY --from=backend-builder /usr/local/etc/php/conf.d/docker-php-ext-*.ini /usr/local/etc/php/conf.d/ + +# Configure PHP for production +RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini && \ + echo "opcache.memory_consumption=128" >> /usr/local/etc/php/conf.d/opcache.ini && \ + echo "opcache.max_accelerated_files=10000" >> /usr/local/etc/php/conf.d/opcache.ini && \ + echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini && \ + echo "expose_php=0" >> /usr/local/etc/php/conf.d/security.ini && \ + echo "display_errors=0" >> /usr/local/etc/php/conf.d/security.ini && \ + echo "log_errors=1" >> /usr/local/etc/php/conf.d/security.ini + +# Create application user +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser && \ + mkdir -p /var/www/html /usr/share/nginx/html && \ + chown -R appuser:appuser /var/www/html /usr/share/nginx/html + +# Create necessary directories for services +RUN mkdir -p /run/nginx /run/mysqld /var/lib/mysql /var/log/supervisor /data/redis && \ + chown -R appuser:appuser /run/nginx /usr/share/nginx/html && \ + chown -R mysql:mysql /run/mysqld /var/lib/mysql && \ + chown -R redis:redis /data/redis + +# Copy backend from builder +WORKDIR /var/www/html +COPY --from=backend-builder --chown=appuser:appuser /var/www/html ./ + +# Create Laravel required directories +RUN mkdir -p storage/framework/{sessions,views,cache} \ + storage/logs \ + bootstrap/cache && \ + chown -R appuser:appuser storage bootstrap/cache && \ + chmod -R 775 storage bootstrap/cache + +# Copy frontend built assets +COPY --from=frontend-builder --chown=appuser:appuser /app/frontend/dist /usr/share/nginx/html + +# Copy nginx configurations +COPY docker/nginx/production.conf /etc/nginx/http.d/default.conf + +# Copy supervisor configuration +COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# Initialize MariaDB data directory +RUN mysql_install_db --user=mysql --datadir=/var/lib/mysql + +# Copy entrypoint script +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost/up || exit 1 + +# Expose HTTP port +EXPOSE 80 + +# Set entrypoint +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + +# Start supervisor +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..6b04797 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,48 @@ +# Dependencies +node_modules/ +vendor/ + +# Environment files +.env +.env.* +!.env.example + +# Testing +tests/ +.phpunit.result.cache +phpunit.xml + +# Development files +.git/ +.github/ +.gitignore +.gitattributes +.editorconfig +.php-cs-fixer.php +.php-cs-fixer.cache +phpstan.neon +phpstan-baseline.neon + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Storage and cache (will be generated in container) +storage/framework/cache/* +storage/framework/sessions/* +storage/framework/views/* +storage/logs/* +bootstrap/cache/* + +# Documentation +README.md +CHANGELOG.md +LICENSE + +# Docker files +Dockerfile* +docker-compose*.yml +.dockerignore diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1063834..eccf028 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,58 +1,66 @@ version: '3.8' services: - frontend: + app: build: - context: ./frontend - dockerfile: ../docker/frontend/Dockerfile.prod - container_name: trip-planner-frontend + context: . + dockerfile: Dockerfile.prod + container_name: trip-planner-production ports: - - "${FRONTEND_PORT:-80}:80" - restart: unless-stopped - networks: - - trip-planner-network - - backend: - build: - context: ./backend - dockerfile: ../docker/backend/Dockerfile.prod - container_name: trip-planner-backend - ports: - - "${BACKEND_PORT:-8080}:80" + - "${APP_PORT:-8080}:80" environment: + # Laravel Application + APP_NAME: "Trip Planner" APP_ENV: production APP_DEBUG: false - APP_URL: ${APP_URL} + APP_KEY: ${APP_KEY} + APP_URL: ${APP_URL:-http://localhost:8080} + + # Database (internal MariaDB) DB_CONNECTION: mysql - DB_HOST: ${DB_HOST} - DB_PORT: ${DB_PORT:-3306} - DB_DATABASE: ${DB_DATABASE} - DB_USERNAME: ${DB_USERNAME} - DB_PASSWORD: ${DB_PASSWORD} - REDIS_HOST: redis + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE:-trip_planner} + DB_USERNAME: ${DB_USERNAME:-trip_user} + DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD must be set} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD must be set} + + # 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 - depends_on: - - redis - restart: unless-stopped - networks: - - trip-planner-network + SESSION_LIFETIME: 120 - redis: - image: docker.io/library/redis:alpine - container_name: trip-planner-redis + # Mail (configure as needed) + MAIL_MAILER: ${MAIL_MAILER:-log} + MAIL_HOST: ${MAIL_HOST:-} + MAIL_PORT: ${MAIL_PORT:-} + MAIL_USERNAME: ${MAIL_USERNAME:-} + MAIL_PASSWORD: ${MAIL_PASSWORD:-} + MAIL_ENCRYPTION: ${MAIL_ENCRYPTION:-} + MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS:-noreply@tripplanner.local} + MAIL_FROM_NAME: "${MAIL_FROM_NAME:-Trip Planner}" volumes: - - redis-data:/data - command: redis-server --appendonly yes + # Persistent data for database + - db-data:/var/lib/mysql + # Persistent data for redis + - redis-data:/data/redis + # Persistent storage for uploaded files + - storage-data:/var/www/html/storage/app restart: unless-stopped - networks: - - trip-planner-network - -networks: - trip-planner-network: - driver: bridge + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/up"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s volumes: - redis-data: \ No newline at end of file + db-data: + redis-data: + storage-data: \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..c72b583 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,160 @@ +# Trip Planner - Docker Setup + +This directory contains Docker configurations for both development and production environments. + +## Directory Structure + +``` +docker/ +├── README.md # This file +├── dev/ # Development environment documentation +│ └── README.md +├── prod/ # Production environment documentation +│ └── README.md +├── backend/ # Backend service configurations +│ ├── Dockerfile.dev +│ ├── Dockerfile.prod +│ └── supervisord.conf +├── frontend/ # Frontend service configurations +│ ├── Dockerfile.dev +│ └── Dockerfile.prod +├── nginx/ # Nginx configurations +│ ├── backend.conf # Backend API nginx config +│ ├── frontend.conf # Frontend SPA nginx config +│ └── production.conf # All-in-one production nginx config +├── supervisord.conf # Supervisord config for production +└── entrypoint.sh # Production container entrypoint script +``` + +## Environments + +### Development Environment + +**Architecture**: Multi-container setup with separate services +- Frontend (React + Vite dev server) +- Backend (Laravel + PHP-FPM) +- Database (MariaDB) +- Redis +- Mailpit (email testing) + +**Use case**: Local development with hot-reloading and debugging + +**Documentation**: See [dev/README.md](dev/README.md) + +**Docker Compose**: `docker-compose.dev.yml` (in project root) + +### Production Environment + +**Architecture**: Single all-in-one container with all services +- Frontend (built React app served by Nginx) +- Backend (Laravel + PHP-FPM + Nginx) +- Database (MariaDB - internal) +- Redis (internal) +- All managed by Supervisord + +**Use case**: Production deployment with minimal footprint + +**Documentation**: See [prod/README.md](prod/README.md) + +**Docker Compose**: `docker-compose.prod.yml` (in project root) + +## Key Differences + +| Aspect | Development | Production | +|--------|------------|------------| +| **Containers** | Multiple (5 services) | Single all-in-one | +| **Frontend** | Vite dev server with HMR | Pre-built static files | +| **Backend** | Live code mounting | Copied into image | +| **Database** | Separate container | Internal to main container | +| **Redis** | Separate container | Internal to main container | +| **Volumes** | Source code mounted | Persistent data only | +| **Ports** | Multiple (5173, 8000, 3306, etc.) | Single port (80) | +| **Size** | ~2GB+ | ~800MB | + +## Port Allocation + +### Development (default ports) +- Frontend: 5173 +- Backend: 8000 +- Database: 3306 +- Redis: 6379 +- Mailpit UI: 8025 +- Mailpit SMTP: 1025 + +### Production (default ports) +- Application: 8080 (configurable via `APP_PORT`) + +**Note**: When running both dev and production locally, ensure they don't use conflicting ports. The production setup defaults to port 8080 to avoid conflicts with the dev setup. + +## Quick Start + +### Development +```bash +# Start all dev services +docker compose -f docker-compose.dev.yml up + +# Stop all dev services +docker compose -f docker-compose.dev.yml down +``` + +### Production (Local Testing) +```bash +# Build and start production container +docker compose -f docker-compose.prod.yml up --build + +# Stop production container +docker compose -f docker-compose.prod.yml down +``` + +## Environment Variables + +Both environments use environment variables for configuration: + +- **Development**: `.env.local` in project root +- **Production**: `.env` or pass via docker-compose environment section + +See the respective README files for detailed environment variable documentation. + +## Building Images + +### Development +Development images are built automatically when you run `docker compose up`. + +### Production +```bash +# Build the production image +docker build -f Dockerfile.prod -t trip-planner:latest . + +# Or use docker-compose +docker compose -f docker-compose.prod.yml build +``` + +## CI/CD + +The production image is automatically built and pushed to Codeberg Container Registry when changes are merged to the `main` branch. + +See `.woodpecker.yml` in the project root for pipeline configuration. + +## Troubleshooting + +### Development Issues +See [dev/README.md](dev/README.md#troubleshooting) + +### Production Issues +See [prod/README.md](prod/README.md#troubleshooting) + +## Security Notes + +- Development setup runs with elevated privileges for convenience +- Production setup follows security best practices: + - Non-root users where possible + - Minimal base images + - No unnecessary privileges + - Security headers configured + - Internal services (DB, Redis) bound to localhost only + +## Need Help? + +- Check the specific environment README files in `dev/` or `prod/` +- Review the main project documentation +- Check container logs: `docker logs ` diff --git a/docker/dev/README.md b/docker/dev/README.md new file mode 100644 index 0000000..526ec7d --- /dev/null +++ b/docker/dev/README.md @@ -0,0 +1,342 @@ +# Trip Planner - Development Environment + +This document describes the development Docker setup for Trip Planner. + +## Overview + +The development environment uses a multi-container architecture with Docker Compose, providing: +- **Hot Module Replacement (HMR)** for frontend development +- **Live code mounting** for instant backend changes +- **Separate services** for easy debugging +- **Development tools** like Mailpit for email testing + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ trip-planner-network (Docker Bridge Network) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ +│ │ Frontend │ │ Backend │ │ Database │ │ +│ │ React+Vite │ │ Laravel+PHP │ │ MariaDB │ │ +│ │ Port: 5173 │ │ Port: 8000 │ │ Port:3306│ │ +│ └──────────────┘ └──────────────┘ └──────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Redis │ │ Mailpit │ │ +│ │ Port: 6379 │ │ UI: 8025 │ │ +│ └──────────────┘ │ SMTP: 1025 │ │ +│ └──────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +## Services + +### Frontend (trip-planner-frontend-dev) +- **Image**: Built from `docker/frontend/Dockerfile.dev` +- **Port**: 5173 +- **Technology**: React 19 + Vite 7 +- **Features**: Hot Module Replacement, ESLint +- **Volume**: `./frontend:/app` (live code mounting) + +### Backend (trip-planner-backend-dev) +- **Image**: Built from `docker/backend/Dockerfile.dev` +- **Port**: 8000 +- **Technology**: Laravel 12 + PHP 8.3 +- **Features**: Artisan commands, PHP-FPM +- **Volume**: `./backend:/var/www/html` (live code mounting) + +### Database (trip-planner-db-dev) +- **Image**: MariaDB 11 +- **Port**: 3306 +- **Data**: Persisted in `./docker/data/mysql-data` +- **Credentials**: Configured via `.env.local` + +### Redis (trip-planner-redis-dev) +- **Image**: Redis Alpine +- **Port**: 6379 +- **Usage**: Cache, sessions, queues +- **Data**: Named volume `redis-data` + +### Mailpit (trip-planner-mailpit-dev) +- **Image**: Axllent Mailpit +- **Ports**: + - SMTP: 1025 + - Web UI: 8025 +- **Usage**: Email testing (catches all outgoing emails) + +## Getting Started + +### Prerequisites + +- Docker Engine 20.10+ +- Docker Compose 2.0+ +- At least 4GB RAM available for Docker + +### Initial Setup + +1. **Clone the repository** (if not already done): + ```bash + git clone ssh://git@codeberg.org/lvl0/trip-planner.git + cd trip-planner + ``` + +2. **Create environment file**: + ```bash + # Copy the example environment file + cp .env.local.example .env.local + + # Edit .env.local with your settings + nano .env.local + ``` + +3. **Start the development environment**: + ```bash + docker compose -f docker-compose.dev.yml up -d + ``` + +4. **Wait for services to be ready** (check with): + ```bash + docker compose -f docker-compose.dev.yml ps + ``` + +5. **Run initial Laravel setup**: + ```bash + # Generate application key + docker exec trip-planner-backend-dev php artisan key:generate + + # Run migrations + docker exec trip-planner-backend-dev php artisan migrate + + # Seed database (optional) + docker exec trip-planner-backend-dev php artisan db:seed + ``` + +6. **Access the application**: + - Frontend: http://localhost:5173 + - Backend API: http://localhost:8000 + - Mailpit UI: http://localhost:8025 + +### Daily Development Workflow + +```bash +# Start all services +docker compose -f docker-compose.dev.yml up -d + +# View logs +docker compose -f docker-compose.dev.yml logs -f + +# Stop all services +docker compose -f docker-compose.dev.yml down + +# Restart a specific service +docker compose -f docker-compose.dev.yml restart backend +``` + +## Environment Variables + +The development environment reads from `.env.local` in the project root. + +### Required Variables + +```env +# Application +APP_NAME="Trip Planner" +APP_KEY=base64:your-generated-key-here + +# Database +DB_DATABASE=trip_planner +DB_USERNAME=trip_user +DB_PASSWORD=secret + +# Mail +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 +``` + +### Optional Variables + +```env +# Frontend +VITE_API_URL=http://localhost:8000 + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +``` + +## Common Tasks + +### Backend Tasks + +```bash +# Run Artisan commands +docker exec trip-planner-backend-dev php artisan + +# Examples: +docker exec trip-planner-backend-dev php artisan migrate +docker exec trip-planner-backend-dev php artisan make:controller UserController +docker exec trip-planner-backend-dev php artisan tinker + +# Install PHP dependencies +docker exec trip-planner-backend-dev composer install + +# Run tests +docker exec trip-planner-backend-dev php artisan test + +# Clear caches +docker exec trip-planner-backend-dev php artisan cache:clear +docker exec trip-planner-backend-dev php artisan config:clear +docker exec trip-planner-backend-dev php artisan route:clear +``` + +### Frontend Tasks + +```bash +# Install npm dependencies +docker exec trip-planner-frontend-dev npm install + +# Run linter +docker exec trip-planner-frontend-dev npm run lint + +# Build for preview +docker exec trip-planner-frontend-dev npm run build +``` + +### Database Tasks + +```bash +# Access MySQL shell +docker exec -it trip-planner-db-dev mysql -u trip_user -p trip_planner + +# Backup database +docker exec trip-planner-db-dev mysqldump -u trip_user -p trip_planner > backup.sql + +# Restore database +docker exec -i trip-planner-db-dev mysql -u trip_user -p trip_planner < backup.sql + +# Reset database +docker compose -f docker-compose.dev.yml down -v # Removes volumes! +docker compose -f docker-compose.dev.yml up -d +docker exec trip-planner-backend-dev php artisan migrate --seed +``` + +### Viewing Logs + +```bash +# All services +docker compose -f docker-compose.dev.yml logs -f + +# Specific service +docker compose -f docker-compose.dev.yml logs -f backend + +# Laravel logs +docker exec trip-planner-backend-dev tail -f storage/logs/laravel.log +``` + +## Troubleshooting + +### Services won't start + +**Check for port conflicts:** +```bash +# Check what's using the ports +lsof -i :5173 # Frontend +lsof -i :8000 # Backend +lsof -i :3306 # Database + +# Stop conflicting services or change ports in docker-compose.dev.yml +``` + +### Frontend HMR not working + +**SELinux issue (Fedora/RHEL):** +The `:Z` flag in volume mounts handles this, but if HMR still doesn't work: +```bash +# Check if SELinux is enforcing +getenforce + +# If needed, you can temporarily set to permissive +sudo setenforce 0 +``` + +### Backend not connecting to database + +**Wait for database to be fully ready:** +```bash +# Check database status +docker compose -f docker-compose.dev.yml ps database + +# Check database logs +docker compose -f docker-compose.dev.yml logs database + +# Verify connection +docker exec trip-planner-backend-dev php artisan migrate:status +``` + +### Permission issues + +**Vendor/node_modules ownership:** +```bash +# Fix backend vendor permissions +docker exec trip-planner-backend-dev chown -R www-data:www-data vendor + +# Fix frontend node_modules (usually not needed with named volumes) +docker compose -f docker-compose.dev.yml down +docker volume rm trip-planner_node_modules +docker compose -f docker-compose.dev.yml up -d +``` + +### Clean slate rebuild + +```bash +# Stop everything +docker compose -f docker-compose.dev.yml down -v + +# Remove images +docker rmi trip-planner-frontend-dev trip-planner-backend-dev + +# Rebuild and start +docker compose -f docker-compose.dev.yml up --build +``` + +## Performance Tips + +### Disable Features You Don't Need + +If a service is not needed for your current task: +```bash +# Start only specific services +docker compose -f docker-compose.dev.yml up -d backend database redis +``` + +### Use Cached Volumes + +The dev setup uses named volumes for `node_modules` and `vendor` to improve performance: +- `node_modules`: Frontend dependencies +- `vendor`: Backend PHP dependencies + +These are NOT mounted from your host, keeping filesystem operations fast. + +## Differences from Production + +| Feature | Development | Production | +|---------|------------|------------| +| Code loading | Live mounted volumes | Copied into image | +| Caching | Disabled/minimal | Aggressive (OPcache, etc.) | +| Error display | Verbose | Hidden | +| Debug mode | Enabled | Disabled | +| Privileges | Elevated for convenience | Minimal (security) | +| Rebuilding | Rarely needed | Required for changes | + +## Security Note + +⚠️ **The development environment is NOT secure** - it runs with `privileged: true` for convenience and mounts source code directly. **Never use this setup in production!** + +## Need Help? + +- Check the main [docker/README.md](../README.md) +- Review Laravel logs: `docker exec trip-planner-backend-dev tail -f storage/logs/laravel.log` +- Check container health: `docker compose -f docker-compose.dev.yml ps` +- Inspect a container: `docker inspect trip-planner-backend-dev` diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..801d8c7 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,141 @@ +#!/bin/bash +set -e + +# Configuration +MYSQL_WAIT_ATTEMPTS=15 +MYSQL_WAIT_INTERVAL=2 +REDIS_WAIT_ATTEMPTS=10 + +echo "=========================================" +echo "[INIT] Trip Planner - Production Container Init" +echo "=========================================" + +# Validate required environment variables +if [ -z "$APP_KEY" ]; then + echo "[INIT] ERROR: APP_KEY is not set!" + exit 1 +fi + +# Laravel APP_KEY must be base64: prefix + minimum 44 chars (32 bytes base64-encoded) +if [[ ! "$APP_KEY" =~ ^base64:.{44,}$ ]]; then + echo "[INIT] ERROR: APP_KEY format is invalid! Must be base64:xxxxx (generated with 'php artisan key:generate')" + exit 1 +fi + +if [ -z "$DB_PASSWORD" ]; then + echo "[INIT] ERROR: DB_PASSWORD is not set!" + exit 1 +fi + +if [ -z "$MYSQL_ROOT_PASSWORD" ]; then + echo "[INIT] ERROR: MYSQL_ROOT_PASSWORD is not set!" + exit 1 +fi + +# Start MariaDB in background +echo "[INIT] Starting MariaDB..." +mysqld --user=mysql --datadir=/var/lib/mysql --bind-address=127.0.0.1 & +MYSQL_PID=$! + +# Start Redis in background +echo "[INIT] Starting Redis..." +redis-server --bind 127.0.0.1 --port 6379 --dir /data/redis --appendonly yes --daemonize yes + +# Wait for MariaDB to be ready +echo "[INIT] Waiting for MariaDB to be ready..." +for i in $(seq 1 $MYSQL_WAIT_ATTEMPTS); do + if mysqladmin ping --socket=/run/mysqld/mysqld.sock --silent 2>/dev/null; then + echo "[INIT] MariaDB is up! (took ${i} attempts)" + break + fi + if [ $i -eq $MYSQL_WAIT_ATTEMPTS ]; then + echo "[INIT] ERROR: MariaDB failed to start within $((MYSQL_WAIT_ATTEMPTS * MYSQL_WAIT_INTERVAL)) seconds" + kill $MYSQL_PID 2>/dev/null || true + exit 1 + fi + sleep $MYSQL_WAIT_INTERVAL +done + +# Wait for Redis to be ready +echo "[INIT] Waiting for Redis to be ready..." +for i in $(seq 1 $REDIS_WAIT_ATTEMPTS); do + if redis-cli ping 2>/dev/null | grep -q PONG; then + echo "[INIT] Redis is up! (took ${i} attempts)" + break + fi + if [ $i -eq $REDIS_WAIT_ATTEMPTS ]; then + echo "[INIT] ERROR: Redis failed to start within ${REDIS_WAIT_ATTEMPTS} seconds" + exit 1 + fi + sleep 1 +done + +# Check if database needs initialization +echo "[INIT] Checking database initialization..." +if ! mysql --socket=/run/mysqld/mysqld.sock -u root -p"${MYSQL_ROOT_PASSWORD}" -e "SELECT 1" &>/dev/null; then + echo "[INIT] Setting root password for first-time setup..." + mysqladmin --socket=/run/mysqld/mysqld.sock -u root password "${MYSQL_ROOT_PASSWORD}" +fi + +# Create database and user if they don't exist +echo "[INIT] Ensuring database and user exist..." +# Escape single quotes in password for SQL safety +DB_PASSWORD_ESCAPED=$(echo "${DB_PASSWORD}" | sed "s/'/''/g") +mysql --socket=/run/mysqld/mysqld.sock -u root -p"${MYSQL_ROOT_PASSWORD}" </dev/null || true +chown -R appuser:appuser /var/www/html/storage 2>/dev/null || true +chmod -R 775 /var/www/html/storage 2>/dev/null || true + +# Run Laravel migrations +echo "[INIT] Running Laravel migrations..." +cd /var/www/html +php artisan migrate --force + +# Ensure storage link exists +if [ ! -L /var/www/html/public/storage ]; then + echo "[INIT] Creating storage link..." + php artisan storage:link +fi + +# Stop background services gracefully (supervisor will manage them) +echo "[INIT] Stopping temporary database services..." + +# Stop Redis first (faster shutdown) +echo "[INIT] Stopping temporary Redis..." +redis-cli shutdown 2>/dev/null || true + +# Gracefully stop MariaDB +echo "[INIT] Stopping temporary MariaDB..." +mysqladmin --socket=/run/mysqld/mysqld.sock -u root -p"${MYSQL_ROOT_PASSWORD}" shutdown 2>/dev/null || kill $MYSQL_PID 2>/dev/null || true + +# Wait for processes to actually terminate +echo "[INIT] Waiting for services to fully stop..." +for i in {1..15}; do + if ! pgrep -x mysqld >/dev/null && ! pgrep -x redis-server >/dev/null; then + echo "[INIT] All temporary services stopped successfully" + break + fi + if [ $i -eq 15 ]; then + echo "[INIT] WARNING: Services took too long to stop, forcing..." + pkill -9 mysqld || true + pkill -9 redis-server || true + sleep 1 + fi + sleep 1 +done + +echo "=========================================" +echo "[INIT] Initialization complete! Starting services..." +echo "=========================================" + +# Execute the main command (supervisord) +exec "$@" diff --git a/docker/nginx/production.conf b/docker/nginx/production.conf new file mode 100644 index 0000000..e4b57e2 --- /dev/null +++ b/docker/nginx/production.conf @@ -0,0 +1,100 @@ +server { + listen 80; + server_name _; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Disable server tokens + server_tokens off; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml application/atom+xml image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype; + + # API Backend + location /api { + root /var/www/html/public; + try_files $uri $uri/ /index.php?$query_string; + + location ~ \.php$ { + try_files $uri =404; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_buffer_size 32k; + fastcgi_buffers 8 16k; + fastcgi_read_timeout 240; + } + } + + # Sanctum/CSRF cookie endpoint + location /sanctum/csrf-cookie { + root /var/www/html/public; + try_files $uri /index.php?$query_string; + + location ~ \.php$ { + try_files $uri =404; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + } + + # Storage files (user uploads, etc) + location /storage { + alias /var/www/html/storage/app/public; + try_files $uri =404; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Frontend SPA + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # Health check endpoints + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Laravel health check endpoint + location /up { + root /var/www/html/public; + try_files $uri /index.php?$query_string; + + location ~ \.php$ { + try_files $uri =404; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + } + + # Deny access to hidden files + location ~ /\.(?!well-known).* { + deny all; + } + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log warn; +} diff --git a/docker/prod/README.md b/docker/prod/README.md new file mode 100644 index 0000000..573e87a --- /dev/null +++ b/docker/prod/README.md @@ -0,0 +1,559 @@ +# 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` diff --git a/docker/supervisord.conf b/docker/supervisord.conf new file mode 100644 index 0000000..fc56903 --- /dev/null +++ b/docker/supervisord.conf @@ -0,0 +1,66 @@ +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0700 + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid +loglevel=info +childlogdir=/var/log/supervisor + +[program:mariadb] +command=/usr/bin/mysqld --user=mysql --datadir=/var/lib/mysql --bind-address=127.0.0.1 --port=3306 +autostart=true +autorestart=true +priority=10 +stopwaitsecs=30 +stopsignal=TERM +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +user=mysql + +[program:redis] +command=/usr/bin/redis-server --bind 127.0.0.1 --port 6379 --dir /data/redis --appendonly yes --logfile "" +autostart=true +autorestart=true +priority=20 +stopwaitsecs=10 +stopsignal=TERM +stdout_logfile=/var/log/supervisor/redis.log +stdout_logfile_maxbytes=10MB +stderr_logfile=/var/log/supervisor/redis-error.log +stderr_logfile_maxbytes=10MB + +[program:php-fpm] +command=/usr/local/sbin/php-fpm -F +autostart=true +autorestart=true +priority=30 +stopwaitsecs=10 +stopsignal=QUIT +stdout_logfile=/var/log/supervisor/php-fpm.log +stdout_logfile_maxbytes=10MB +stderr_logfile=/var/log/supervisor/php-fpm-error.log +stderr_logfile_maxbytes=10MB + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +priority=40 +stopwaitsecs=10 +stopsignal=QUIT +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..fff5942 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,51 @@ +# Dependencies +node_modules/ +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Development +.env +.env.* +!.env.example +.env.local +.env.development + +# Build artifacts (will be rebuilt) +dist/ +build/ +.vite/ + +# Testing +coverage/ +.nyc_output/ + +# Development files +.git/ +.github/ +.gitignore +.editorconfig +.eslintrc* +.prettierrc* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Documentation +README.md +CHANGELOG.md +LICENSE + +# Docker files +Dockerfile* +docker-compose*.yml +.dockerignore + +# Logs +logs/ +*.log +npm-debug.log*