From 3993fb7fc32b01211e289cda45d46ba138a977b1 Mon Sep 17 00:00:00 2001 From: "lucas.mathieu" Date: Thu, 5 Feb 2026 16:03:38 +0100 Subject: [PATCH] chore(docker): add docker support --- .dockerignore | 34 ++++ DOCKER.md | 247 +++++++++++++++++++++++++++++ Dockerfile | 91 +++++++++++ docker-compose.dev.yml | 94 +++++++++++ docker-compose.yml | 98 ++++++++++++ docker/docker-entrypoint.sh | 41 +++++ docker/nginx/default.conf | 36 +++++ docker/nginx/nginx.conf | 33 ++++ docker/php/opcache.ini | 7 + docker/php/php.ini | 6 + docker/supervisor/supervisord.conf | 42 +++++ 11 files changed, 729 insertions(+) create mode 100644 .dockerignore create mode 100644 DOCKER.md create mode 100644 Dockerfile create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 docker/docker-entrypoint.sh create mode 100644 docker/nginx/default.conf create mode 100644 docker/nginx/nginx.conf create mode 100644 docker/php/opcache.ini create mode 100644 docker/php/php.ini create mode 100644 docker/supervisor/supervisord.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..1c231b8c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +.git +.gitignore +.gitattributes +.github +.editorconfig + +*.md +LICENSE* +SECURITY* +TRADEMARKS* + +.env +.env.* +!.env.example + +docker-compose*.yml +Dockerfile* + +vendor/ +node_modules/ + +storage/logs/* +storage/framework/cache/* +storage/framework/sessions/* +storage/framework/views/* +storage/app/* +bootstrap/cache/* + +tests/ +phpunit.xml +.phpunit* + +*.log +*.cache diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 00000000..e062759b --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,247 @@ +# Running Cachet with Docker + +## Quick Start + +```bash +# Build the image +docker compose build + +# Generate an application key +APP_KEY=$(openssl rand -base64 32) + +# Start the stack +APP_KEY="base64:$APP_KEY" docker compose up -d + +# Wait for services to be healthy (about 30-60 seconds) +docker compose ps + +# Create your first admin user +docker exec -it cachet-app php artisan cachet:make:user +``` + +Access Cachet at http://localhost:8000 + +## Configuration + +### Environment Variables + +Create a `.env` file in the project root or pass variables directly: + +| Variable | Default | Description | +|----------|---------|-------------| +| `APP_KEY` | (required) | Application encryption key | +| `APP_URL` | `http://localhost:8000` | Public URL of your instance | +| `APP_ENV` | `production` | Environment (`local`, `production`) | +| `APP_DEBUG` | `false` | Enable debug mode | +| `APP_TIMEZONE` | `UTC` | Application timezone | +| `APP_PORT` | `8000` | Host port to expose | +| `DB_CONNECTION` | `pgsql` | Database driver (`pgsql`, `mysql`, `sqlite`) | +| `DB_HOST` | `database` | Database hostname | +| `DB_PORT` | `5432` | Database port | +| `DB_DATABASE` | `cachet` | Database name | +| `DB_USERNAME` | `cachet` | Database username | +| `DB_PASSWORD` | `secret` | Database password | +| `REDIS_HOST` | `redis` | Redis hostname | +| `CACHE_STORE` | `redis` | Cache driver | +| `MAIL_MAILER` | `log` | Mail driver (`smtp`, `log`, `mailpit`) | +| `MAIL_HOST` | `mailpit` | SMTP host | +| `MAIL_PORT` | `1025` | SMTP port | +| `MAIL_FROM_ADDRESS` | `hello@example.com` | From email address | + +### Generate Application Key + +```bash +# Using OpenSSL +echo "base64:$(openssl rand -base64 32)" + +# Or using PHP +docker run --rm php:8.2-cli php -r "echo 'base64:' . base64_encode(random_bytes(32)) . PHP_EOL;" +``` + +## Production Deployment + +### Using docker-compose.yml + +```bash +# Create environment file +cat > .env.docker << 'EOF' +APP_KEY=base64:your-generated-key-here +APP_URL=https://status.example.com +APP_ENV=production +APP_DEBUG=false +DB_PASSWORD=your-secure-password +MAIL_MAILER=smtp +MAIL_HOST=smtp.example.com +MAIL_PORT=587 +MAIL_USERNAME=your-smtp-user +MAIL_PASSWORD=your-smtp-password +MAIL_FROM_ADDRESS=status@example.com +EOF + +# Start with custom env file +docker compose --env-file .env.docker up -d +``` + +### Using External Database + +To use an external PostgreSQL or MySQL database: + +```bash +docker compose up -d app redis # Skip the database service + +# Or modify docker-compose.yml to remove the database service +# and update DB_HOST to point to your external database +``` + +### Reverse Proxy (nginx/Traefik) + +When running behind a reverse proxy, set: + +```bash +APP_URL=https://status.example.com +CACHET_TRUSTED_PROXIES=* # Or specific proxy IPs +``` + +## Development + +### Using docker-compose.dev.yml + +```bash +docker compose -f docker-compose.dev.yml up -d +``` + +Features: +- Source code mounted for hot-reload +- PostgreSQL exposed on port 5432 +- Redis exposed on port 6379 +- Mailpit UI at http://localhost:8025 + +### Running Commands + +```bash +# Artisan commands +docker exec -it cachet-app php artisan + +# Composer +docker exec -it cachet-app composer + +# Database shell +docker exec -it cachet-db psql -U cachet -d cachet +``` + +## Maintenance + +### View Logs + +```bash +# All services +docker compose logs -f + +# Specific service +docker compose logs -f app + +# Application logs inside container +docker exec -it cachet-app tail -f storage/logs/laravel.log +``` + +### Backup Database + +```bash +# PostgreSQL +docker exec cachet-db pg_dump -U cachet cachet > backup.sql + +# Restore +docker exec -i cachet-db psql -U cachet cachet < backup.sql +``` + +### Update Cachet + +```bash +docker compose down +git pull +docker compose build --no-cache +docker compose up -d +docker exec -it cachet-app php artisan migrate --force +``` + +### Clear Caches + +```bash +docker exec -it cachet-app php artisan cache:clear +docker exec -it cachet-app php artisan config:clear +docker exec -it cachet-app php artisan view:clear +``` + +## Troubleshooting + +### Container Won't Start + +Check logs: +```bash +docker compose logs app +``` + +Common issues: +- Missing `APP_KEY`: Generate one using the commands above +- Database not ready: Wait for health checks or increase `start_period` + +### Permission Errors + +```bash +docker exec -it cachet-app chown -R www-data:www-data storage bootstrap/cache +docker exec -it cachet-app chmod -R 775 storage bootstrap/cache +``` + +### Database Connection Failed + +Verify database is healthy: +```bash +docker compose ps +docker exec cachet-db pg_isready -U cachet +``` + +### Queue Jobs Not Processing + +Check queue worker: +```bash +docker exec -it cachet-app supervisorctl status queue-worker +docker exec -it cachet-app cat storage/logs/queue.log +``` + +Restart queue worker: +```bash +docker exec -it cachet-app supervisorctl restart queue-worker +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ cachet-app │ +│ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ nginx │ │ php-fpm │ │ queue │ │ scheduler │ │ +│ │ :8000 │ │ :9000 │ │ worker │ │ (every minute)│ │ +│ └─────────┘ └─────────┘ └──────────┘ └───────────────┘ │ +│ supervised by supervisord │ +└─────────────────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌────────────┐ ┌────────────┐ ┌────────────┐ + │ postgres │ │ redis │ │ mailpit │ + │ :5432 │ │ :6379 │ │ :8025 │ + └────────────┘ └────────────┘ └────────────┘ +``` + +## Building Custom Image + +```bash +# Production image +docker build --target production -t cachet:latest . + +# Development image +docker build --target development -t cachet:dev . + +# With custom registry +docker build --target production -t registry.example.com/cachet:v3 . +docker push registry.example.com/cachet:v3 +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..ac6e8600 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,91 @@ +# syntax=docker/dockerfile:1.4 +FROM php:8.3-fpm-alpine AS base + +RUN apk add --no-cache \ + nginx \ + supervisor \ + curl \ + git \ + unzip \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + libzip-dev \ + icu-dev \ + oniguruma-dev \ + libpq-dev \ + sqlite-dev \ + linux-headers \ + $PHPIZE_DEPS + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) \ + pdo \ + pdo_mysql \ + pdo_pgsql \ + pdo_sqlite \ + gd \ + zip \ + intl \ + mbstring \ + opcache \ + bcmath \ + pcntl + +RUN pecl install redis && docker-php-ext-enable redis + +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" +COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini +COPY docker/php/php.ini /usr/local/etc/php/conf.d/custom.ini + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf +COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf + +COPY docker/supervisor/supervisord.conf /etc/supervisord.conf + +COPY docker/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# ================================ +# Development stage +# ================================ +FROM base AS development + +RUN apk add --no-cache nodejs npm + +COPY --chown=www-data:www-data . . + +RUN git config --global url."https://github.com/".insteadOf "git@github.com:" \ + && composer install --optimize-autoloader + +RUN chown -R www-data:www-data /var/www/html \ + && chmod -R 775 storage bootstrap/cache + +EXPOSE 8000 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["supervisord", "-c", "/etc/supervisord.conf"] + +# ================================ +# Production stage +# ================================ +FROM base AS production + +COPY --chown=www-data:www-data . . + +RUN git config --global url."https://github.com/".insteadOf "git@github.com:" \ + && composer install --no-dev --optimize-autoloader --no-interaction --no-progress + +RUN chown -R www-data:www-data /var/www/html \ + && chmod -R 775 storage bootstrap/cache + +RUN mkdir -p database && touch database/database.sqlite && chown www-data:www-data database/database.sqlite + +EXPOSE 8000 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["supervisord", "-c", "/etc/supervisord.conf"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..97367a09 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,94 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + target: development + container_name: cachet-dev + restart: unless-stopped + ports: + - "8000:8000" + - "5173:5173" + environment: + - APP_NAME=Cachet + - APP_ENV=local + - APP_DEBUG=true + - APP_TIMEZONE=UTC + - APP_URL=http://localhost:8000 + - LOG_CHANNEL=stack + - LOG_LEVEL=debug + - DB_CONNECTION=pgsql + - DB_HOST=database + - DB_PORT=5432 + - DB_DATABASE=cachet + - DB_USERNAME=cachet + - DB_PASSWORD=secret + - SESSION_DRIVER=database + - QUEUE_CONNECTION=database + - CACHE_STORE=redis + - REDIS_HOST=redis + - REDIS_PORT=6379 + - CACHET_BEACON=false + - CACHET_DOCKER=true + volumes: + - .:/var/www/html + - /var/www/html/vendor + - /var/www/html/node_modules + depends_on: + database: + condition: service_healthy + redis: + condition: service_healthy + networks: + - cachet-dev + + database: + image: postgres:16-alpine + container_name: cachet-dev-db + restart: unless-stopped + ports: + - "5432:5432" + environment: + - POSTGRES_DB=cachet + - POSTGRES_USER=cachet + - POSTGRES_PASSWORD=secret + volumes: + - cachet-dev-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U cachet -d cachet"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - cachet-dev + + redis: + image: redis:7-alpine + container_name: cachet-dev-redis + restart: unless-stopped + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - cachet-dev + + mailpit: + image: axllent/mailpit + container_name: cachet-dev-mail + restart: unless-stopped + ports: + - "1025:1025" + - "8025:8025" + networks: + - cachet-dev + +volumes: + cachet-dev-db: + +networks: + cachet-dev: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3a2917c3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,98 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + target: production + container_name: cachet-app + restart: unless-stopped + ports: + - "${APP_PORT:-8000}:8000" + environment: + - APP_NAME=${APP_NAME:-Cachet} + - APP_ENV=${APP_ENV:-production} + - APP_KEY=${APP_KEY} + - APP_DEBUG=${APP_DEBUG:-false} + - APP_TIMEZONE=${APP_TIMEZONE:-UTC} + - APP_URL=${APP_URL:-http://localhost:8000} + - APP_LOCALE=${APP_LOCALE:-en} + - LOG_CHANNEL=${LOG_CHANNEL:-stack} + - LOG_LEVEL=${LOG_LEVEL:-warning} + - DB_CONNECTION=${DB_CONNECTION:-pgsql} + - DB_HOST=${DB_HOST:-database} + - DB_PORT=${DB_PORT:-5432} + - DB_DATABASE=${DB_DATABASE:-cachet} + - DB_USERNAME=${DB_USERNAME:-cachet} + - DB_PASSWORD=${DB_PASSWORD:-secret} + - SESSION_DRIVER=${SESSION_DRIVER:-database} + - QUEUE_CONNECTION=${QUEUE_CONNECTION:-database} + - CACHE_STORE=${CACHE_STORE:-redis} + - REDIS_HOST=${REDIS_HOST:-redis} + - REDIS_PORT=${REDIS_PORT:-6379} + - REDIS_PASSWORD=${REDIS_PASSWORD:-null} + - MAIL_MAILER=${MAIL_MAILER:-log} + - MAIL_HOST=${MAIL_HOST:-mailpit} + - MAIL_PORT=${MAIL_PORT:-1025} + - MAIL_FROM_ADDRESS=${MAIL_FROM_ADDRESS:-hello@example.com} + - MAIL_FROM_NAME=${MAIL_FROM_NAME:-Cachet} + - CACHET_BEACON=${CACHET_BEACON:-false} + - CACHET_DOCKER=true + volumes: + - cachet-storage:/var/www/html/storage/app + - cachet-logs:/var/www/html/storage/logs + depends_on: + database: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + networks: + - cachet + + database: + image: postgres:16-alpine + container_name: cachet-db + restart: unless-stopped + environment: + - POSTGRES_DB=${DB_DATABASE:-cachet} + - POSTGRES_USER=${DB_USERNAME:-cachet} + - POSTGRES_PASSWORD=${DB_PASSWORD:-secret} + volumes: + - cachet-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-cachet} -d ${DB_DATABASE:-cachet}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - cachet + + redis: + image: redis:7-alpine + container_name: cachet-redis + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - cachet-redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - cachet + +volumes: + cachet-storage: + cachet-logs: + cachet-db: + cachet-redis: + +networks: + cachet: + driver: bridge diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100644 index 00000000..05fb0e26 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,41 @@ +#!/bin/sh +set -e + +echo "Starting Cachet initialization..." + +cd /var/www/html + +if [ ! -f ".env" ]; then + echo "Creating .env file from .env.example..." + cp .env.example .env +fi + +if [ -z "$APP_KEY" ] || [ "$APP_KEY" = "" ]; then + echo "Generating application key..." + php artisan key:generate --force +fi + +echo "Caching configuration..." +php artisan config:cache +php artisan route:cache +php artisan view:cache + +echo "Publishing Cachet assets..." +php artisan vendor:publish --tag=cachet --force + +echo "Publishing Filament assets..." +php artisan filament:assets + +echo "Running database migrations..." +php artisan migrate --force + +echo "Creating storage symlink..." +php artisan storage:link || true + +echo "Setting permissions..." +chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache +chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache + +echo "Cachet initialization complete!" + +exec "$@" diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 00000000..27ab192e --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,36 @@ +server { + listen 8000; + server_name _; + root /var/www/html/public; + index index.php; + + charset utf-8; + client_max_body_size 100M; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_hide_header X-Powered-By; + fastcgi_read_timeout 600; + } + + location ~ /\.(?!well-known).* { + deny all; + } + + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 00000000..48b1cbc9 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,33 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + + include /etc/nginx/http.d/*.conf; +} diff --git a/docker/php/opcache.ini b/docker/php/opcache.ini new file mode 100644 index 00000000..3b9576cf --- /dev/null +++ b/docker/php/opcache.ini @@ -0,0 +1,7 @@ +opcache.enable=1 +opcache.memory_consumption=256 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=20000 +opcache.validate_timestamps=0 +opcache.save_comments=1 +opcache.fast_shutdown=1 diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 00000000..51749aa5 --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,6 @@ +upload_max_filesize = 100M +post_max_size = 100M +memory_limit = 512M +max_execution_time = 600 +max_input_time = 600 +expose_php = Off diff --git a/docker/supervisor/supervisord.conf b/docker/supervisor/supervisord.conf new file mode 100644 index 00000000..cb70056e --- /dev/null +++ b/docker/supervisor/supervisord.conf @@ -0,0 +1,42 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:php-fpm] +command=/usr/local/sbin/php-fpm -F +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:queue-worker] +command=/usr/local/bin/php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 +user=www-data +autostart=true +autorestart=true +numprocs=1 +redirect_stderr=true +stdout_logfile=/var/www/html/storage/logs/queue.log +stopwaitsecs=3600 + +[program:scheduler] +command=/bin/sh -c "while true; do /usr/local/bin/php /var/www/html/artisan schedule:run --verbose --no-interaction >> /var/www/html/storage/logs/scheduler.log 2>&1; sleep 60; done" +user=www-data +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0