diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile new file mode 100644 index 0000000..34432af --- /dev/null +++ b/docker/production/Dockerfile @@ -0,0 +1,85 @@ +# Multi-stage build for FFR Laravel application +FROM node:20 AS frontend-builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install Node dependencies +RUN npm ci + +# Copy frontend source +COPY resources/ resources/ +COPY public/ public/ +COPY vite.config.js ./ +COPY tsconfig.json ./ + +# Build frontend assets +RUN npm run build + +# PHP runtime stage +FROM php:8.4-fpm-alpine + +# Install system dependencies +RUN apk add --no-cache \ + git \ + curl \ + libpng-dev \ + libxml2-dev \ + zip \ + unzip \ + oniguruma-dev \ + mysql-client \ + nginx \ + supervisor \ + autoconf \ + gcc \ + g++ \ + make + +# Install PHP extensions +RUN docker-php-ext-install \ + pdo_mysql \ + mbstring \ + exif \ + pcntl \ + bcmath \ + gd \ + && pecl install redis \ + && docker-php-ext-enable redis + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Set working directory +WORKDIR /var/www/html + +# Copy application code first +COPY . . + +# Install PHP dependencies after copying all files +RUN composer install --no-dev --optimize-autoloader --no-interaction + +# Copy built frontend assets from builder stage +COPY --from=frontend-builder /app/public/build/ ./public/build/ + +# Copy nginx and supervisor configurations +COPY docker/production/nginx.conf /etc/nginx/http.d/default.conf +COPY docker/production/supervisord.conf /etc/supervisord.conf +COPY docker/production/start-app.sh /usr/local/bin/start-app + +# Set proper permissions +RUN chown -R www-data:www-data storage bootstrap/cache public/build \ + && chmod -R 755 storage bootstrap/cache \ + && chmod +x /usr/local/bin/start-app + +# Expose port 80 for nginx +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD php artisan --version || exit 1 + +# Start the application +CMD ["/usr/local/bin/start-app"] \ No newline at end of file diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml new file mode 100644 index 0000000..3870f55 --- /dev/null +++ b/docker/production/docker-compose.yml @@ -0,0 +1,130 @@ +services: + app: + image: codeberg.org/lvl0/ffr:latest +# build: +# context: ../.. +# dockerfile: docker/production/Dockerfile + container_name: ffr-app + restart: unless-stopped + working_dir: /var/www/html + environment: + - APP_ENV=production + - APP_DEBUG=false + - DB_CONNECTION=mysql + - DB_HOST=db + - DB_PORT=3306 + - DB_DATABASE=ffr + - DB_USERNAME=ffr_user + - DB_PASSWORD=ffr_password + - REDIS_HOST=redis + - REDIS_PORT=6379 + - CACHE_DRIVER=redis + - SESSION_DRIVER=redis + - QUEUE_CONNECTION=redis + volumes: [] + ports: + - "8000:80" + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + networks: + - ffr-network + + queue: + image: codeberg.org/lvl0/ffr:latest + container_name: ffr-queue + restart: unless-stopped + working_dir: /var/www/html + environment: + - APP_ENV=production + - APP_DEBUG=false + - DB_CONNECTION=mysql + - DB_HOST=db + - DB_PORT=3306 + - DB_DATABASE=ffr + - DB_USERNAME=ffr_user + - DB_PASSWORD=ffr_password + - REDIS_HOST=redis + - REDIS_PORT=6379 + - CACHE_DRIVER=redis + - SESSION_DRIVER=redis + - QUEUE_CONNECTION=redis + command: ["php", "artisan", "horizon"] + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + networks: + - ffr-network + + scheduler: + image: codeberg.org/lvl0/ffr:latest + container_name: ffr-scheduler + restart: unless-stopped + working_dir: /var/www/html + environment: + - APP_ENV=production + - APP_DEBUG=false + - DB_CONNECTION=mysql + - DB_HOST=db + - DB_PORT=3306 + - DB_DATABASE=ffr + - DB_USERNAME=ffr_user + - DB_PASSWORD=ffr_password + - REDIS_HOST=redis + - REDIS_PORT=6379 + - CACHE_DRIVER=redis + - SESSION_DRIVER=redis + - QUEUE_CONNECTION=redis + command: ["sh", "-c", "while true; do php artisan schedule:run --verbose --no-interaction; sleep 60; done"] + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + networks: + - ffr-network + + db: + image: docker.io/library/mysql:8.0 + container_name: ffr-db + restart: unless-stopped + environment: + - MYSQL_DATABASE=ffr + - MYSQL_USER=ffr_user + - MYSQL_PASSWORD=ffr_password + - MYSQL_ROOT_PASSWORD=root_password + volumes: + - db_data:/var/lib/mysql + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "ffr_user", "-pffr_password"] + timeout: 5s + retries: 5 + interval: 3s + start_period: 30s + networks: + - ffr-network + + redis: + image: docker.io/library/redis:7-alpine + container_name: ffr-redis + restart: unless-stopped + volumes: + - redis_data:/data + networks: + - ffr-network + +networks: + ffr-network: + driver: bridge + +volumes: + db_data: + driver: local + redis_data: + driver: local \ No newline at end of file diff --git a/docker/production/nginx.conf b/docker/production/nginx.conf new file mode 100644 index 0000000..393969c --- /dev/null +++ b/docker/production/nginx.conf @@ -0,0 +1,69 @@ +server { + listen 80; + server_name localhost; + root /var/www/html/public; + index index.php index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + add_header X-XSS-Protection "1; mode=block"; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + + # Increase timeouts for long-running requests + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # Deny access to hidden files + location ~ /\.ht { + deny all; + } + + # Deny access to sensitive files + location ~ /\.(env|git) { + deny all; + } + + # Static assets with far-future expiry + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Laravel specific optimizations + location = /favicon.ico { + access_log off; + log_not_found off; + } + + location = /robots.txt { + access_log off; + log_not_found off; + } + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; +} \ No newline at end of file diff --git a/docker/production/start-app.sh b/docker/production/start-app.sh new file mode 100644 index 0000000..1819690 --- /dev/null +++ b/docker/production/start-app.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# Create .env file if it doesn't exist +if [ ! -f /var/www/html/.env ]; then + cp /var/www/html/.env.example /var/www/html/.env 2>/dev/null || touch /var/www/html/.env +fi + +# Wait for database to be ready +echo "Waiting for database..." +while ! mysql -h db -u ffr_user -pffr_password --connect-timeout=2 -e "SELECT 1" >/dev/null 2>&1; do + echo "Database not ready, waiting..." + sleep 1 +done +echo "Database connection established!" + +# Generate app key if not set +if ! grep -q "APP_KEY=base64:" /var/www/html/.env; then + php artisan key:generate --force +fi + +# Laravel optimizations for production +php artisan config:cache +php artisan route:cache +php artisan view:cache + +# Run migrations +php artisan migrate --force + +# Run seeders (only if needed for production data) +php artisan db:seed --force --class=PlatformInstanceSeeder +php artisan db:seed --force --class=SettingsSeeder + +# Start supervisor to manage nginx and php-fpm +supervisord -c /etc/supervisord.conf \ No newline at end of file diff --git a/docker/production/supervisord.conf b/docker/production/supervisord.conf new file mode 100644 index 0000000..209c706 --- /dev/null +++ b/docker/production/supervisord.conf @@ -0,0 +1,25 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:nginx] +command=nginx -g "daemon off;" +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +priority=10 + +[program:php-fpm] +command=php-fpm --nodaemonize +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +priority=10 \ No newline at end of file