From 59d65cff879b7f6b4999f64a6284d1204d15f731 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Fri, 19 Dec 2025 15:15:17 +0100 Subject: [PATCH 1/2] feat: add automatic build - cleanup container images - use built images inside docker compose --- .env.example | 8 +- .github/workflows/build-container.yml | 71 +++++++ README.md | 5 + cachet-configuration-files/Dockerfile | 197 ++++++++---------- .../docker/supervisor/supervisord.conf | 13 +- docker-compose.yml | 68 ++---- middleware/Dockerfile | 4 +- traefik/dynamic/middlewares.yml.example | 64 ------ traefik/dynamic/routers.yml | 33 +++ traefik/dynamic/services.yml | 14 ++ traefik/traefik.local.yml | 5 - traefik/traefik.production.yml | 5 - 12 files changed, 238 insertions(+), 249 deletions(-) create mode 100644 .github/workflows/build-container.yml delete mode 100644 traefik/dynamic/middlewares.yml.example create mode 100644 traefik/dynamic/routers.yml create mode 100644 traefik/dynamic/services.yml diff --git a/.env.example b/.env.example index 9990d06..62d02d9 100644 --- a/.env.example +++ b/.env.example @@ -62,10 +62,10 @@ DB_PASSWORD=YOUR_STRONG_DB_PASSWORD_HERE # ============================================ # WEBHOOK AUTHENTICATION # ============================================ -# BasicAuth credentials for /webhook endpoint -# These will be automatically hashed and configured in Traefik during deployment -WEBHOOK_USERNAME=your-email@example.com -WEBHOOK_PASSWORD=your-secure-password +# BasicAuth credentials for /webhook endpoint (format: user:hash) +# Generate hash with: htpasswd -nb user password +# Default if not set: admin:admin +# WEBHOOK_BASIC_AUTH= # ============================================ # CACHET Admin user diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml new file mode 100644 index 0000000..ffa7f61 --- /dev/null +++ b/.github/workflows/build-container.yml @@ -0,0 +1,71 @@ +name: Build and Publish Container + +on: + push: + branches: [ main ] + tags: + - '*' + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + strategy: + matrix: + image: + - name: cachet-status-page + dockerfile: cachet-configuration-files/Dockerfile + path: cachet-configuration-files + - name: middleware-status-page + path: middleware + dockerfile: middleware/Dockerfile + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine image tags + id: tags + run: | + SHORT_SHA=${GITHUB_SHA::8} + IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER}/${{ matrix.image.name }} + # Default tag is sha + echo "tags=${IMAGE}:sha-${SHORT_SHA}" >> $GITHUB_OUTPUT + + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + TAG_NAME=${GITHUB_REF#refs/tags/} + echo "tags=${IMAGE}:sha-${SHORT_SHA},${IMAGE}:${TAG_NAME}" >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" == refs/heads/main ]]; then + echo "tags=${IMAGE}:sha-${SHORT_SHA},${IMAGE}:latest" >> $GITHUB_OUTPUT + else + # sanitize branch name: replace / with - and remove non-alphanum ._- characters + BRANCH=${GITHUB_REF#refs/heads/} + SAFE_BRANCH=$(echo "$BRANCH" | sed 's#[^A-Za-z0-9._-]#-#g') + echo "tags=${IMAGE}:sha-${SHORT_SHA},${IMAGE}:${SAFE_BRANCH}" >> $GITHUB_OUTPUT + fi + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: ${{ matrix.image.path }} + file: ${{ matrix.image.dockerfile }} + platforms: linux/amd64 + push: true + tags: ${{ steps.tags.outputs.tags }} diff --git a/README.md b/README.md index 8f2e21a..69bfc60 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,11 @@ nano .env - `APP_KEY` - `CACHET_API_TOKEN` +**Authentication:** +The webhook endpoint is protected by Basic Auth. You can configure the credentials by setting the `WEBHOOK_BASIC_AUTH` environment variable in `.env`. +The format is `user:hash`. You can generate the hash using `htpasswd -nb user password` or an online generator (BCrypt, MD5, SHA1). +If not set, the default credentials are `admin:admin`. + > **Note for rootless Podman and privileged ports (80/443):** > > By default, non-root users cannot bind to ports below 1024 (such as 80 and 443). If you want to expose Traefik or other services directly on these ports in rootless mode, you must configure the following kernel parameter on your host system: diff --git a/cachet-configuration-files/Dockerfile b/cachet-configuration-files/Dockerfile index 028e090..98094cb 100644 --- a/cachet-configuration-files/Dockerfile +++ b/cachet-configuration-files/Dockerfile @@ -1,92 +1,12 @@ -# Multi-stage build for production Cachet deployment -ARG PHP_VERSION +# Single-stage build for production Cachet deployment +ARG PHP_VERSION=8.3 +ARG CACHET_COMMIT=207a6d5a7c309fcdc4656a074869f0afa06d142c -# ============================================ -# Stage 1: Composer dependencies -# ============================================ -FROM docker.io/library/php:${PHP_VERSION}-cli-alpine AS composer - -# Allow use of all user's SSH keys/config for private repository authentication -# Temporarily copy the entire .ssh directory -COPY --chown=root:root .ssh /root/.ssh -RUN chmod 700 /root/.ssh && chmod 600 /root/.ssh/* - -ENV GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=yes" - -# Install composer, build tools, and required extensions -RUN apk add --no-cache \ - composer \ - icu-dev \ - libxml2-dev \ - autoconf \ - g++ \ - make \ - git \ - openssh \ - && docker-php-ext-install \ - intl \ - dom \ - xml \ - simplexml \ - session \ - && php -m | grep -i intl \ - && php -m | grep -i dom \ - && php -m | grep -i simplexml \ - && php -m | grep -i tokenizer \ - && php -m | grep -i session - -WORKDIR /app - -COPY ./cachet/composer.json ./cachet/composer.lock ./ - -# Install dependencies (including dev for now - Pail is required by config/app.php) -# Note: We ignore platform requirements for extensions that are: -# - Bundled in PHP 8.3 (fileinfo, session, tokenizer, simplexml, xmlreader, xmlwriter) -# - Installed but not detected correctly by composer (intl, dom, xml) -RUN composer install \ - --no-interaction \ - --no-progress \ - --no-scripts \ - --prefer-dist \ - --optimize-autoloader \ - --ignore-platform-req=php+ \ - --ignore-platform-req=ext-simplexml \ - --ignore-platform-req=ext-fileinfo \ - --ignore-platform-req=ext-session \ - --ignore-platform-req=ext-tokenizer \ - --ignore-platform-req=ext-xmlreader \ - --ignore-platform-req=ext-xmlwriter \ - --ignore-platform-req=ext-intl \ - --ignore-platform-req=ext-dom \ - --ignore-platform-req=ext-xml - -# Update cachethq/core dependency -# RUN composer update cachethq/core -# --no-interaction \ -# --no-dev \ -# --prefer-dist \ -# --no-progress \ -# --no-scripts \ -# --ignore-platform-req=ext-simplexml \ -# --ignore-platform-req=ext-fileinfo \ -# --ignore-platform-req=ext-session \ -# --ignore-platform-req=ext-tokenizer \ -# --ignore-platform-req=ext-xmlreader \ -# --ignore-platform-req=ext-xmlwriter \ -# --ignore-platform-req=ext-intl \ -# --ignore-platform-req=ext-dom \ -# --ignore-platform-req=ext-xml - -# Remove SSH keys after use for security -RUN rm -rf /root/.ssh - -# ============================================ -# Stage 3: Production PHP runtime -# ============================================ FROM docker.io/library/php:${PHP_VERSION}-fpm-alpine -# Install system dependencies +# Install system dependencies and composer RUN apk add --no-cache \ + composer \ nginx \ supervisor \ postgresql-dev \ @@ -97,10 +17,24 @@ RUN apk add --no-cache \ oniguruma-dev \ icu-dev \ libxml2-dev \ + libpq \ + icu-libs \ + libjpeg-turbo \ + libpng \ + freetype \ + libzip \ + oniguruma \ + libxml2 \ curl \ bash \ + git \ + autoconf \ + g++ \ + make \ + ca-certificates \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j$(nproc) \ + pdo \ pdo_pgsql \ pgsql \ zip \ @@ -111,41 +45,65 @@ RUN apk add --no-cache \ xml \ simplexml \ session \ - && rm -rf /var/cache/apk/* - -# Install PHP extensions for production -RUN docker-php-ext-enable opcache + && docker-php-ext-enable opcache pdo pdo_pgsql # Configure PHP for production (relative to podman-setup) -COPY ./cachet/docker/php/php.ini /usr/local/etc/php/conf.d/cachet.ini -COPY ./cachet/docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini +COPY ./docker/php/php.ini /usr/local/etc/php/conf.d/cachet.ini +COPY ./docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini # Configure Nginx -COPY ./cachet/docker/nginx/nginx.conf /etc/nginx/nginx.conf -COPY ./cachet/docker/nginx/default.conf /etc/nginx/http.d/default.conf +COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf +COPY ./docker/nginx/default.conf /etc/nginx/http.d/default.conf # Configure Supervisor -COPY ./cachet/docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Create application user -RUN addgroup -g 1000 -S cachet && \ - adduser -u 1000 -S cachet -G cachet +COPY ./docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf # Set working directory WORKDIR /var/www/html -# Copy application files (relative to podman-setup) -COPY --chown=cachet:cachet ./cachet/. . +# Clone Cachet repository +RUN git clone https://github.com/cachethq/cachet.git /var/www/html/ \ + && git --git-dir /var/www/html/.git checkout ${CACHET_COMMIT} + +# Copy local configuration files into the cloned Cachet repository +COPY --chown=root:root ./proxy/TrustProxies.php /var/www/html/app/Http/Middleware/TrustProxies.php -# Copy composer dependencies from builder -# Note: With symlink=false in composer.json, core is copied into vendor/cachethq/core -COPY --from=composer --chown=cachet:cachet /app/vendor ./vendor +# Copy AdminSeeder to enable admin user creation via environment variables +COPY ./token/AdminSeeder.php /var/www/html/database/seeders/AdminSeeder.php + +# Install composer dependencies (no dev) +# Note: We ignore platform requirements for extensions that are: +# - Bundled in PHP 8.3 (fileinfo, session, tokenizer, simplexml, xmlreader, xmlwriter) +# - Installed but not detected correctly by composer (intl, dom, xml) +RUN cd /var/www/html && composer install --no-dev \ + --no-interaction \ + --no-progress \ + --no-scripts \ + --prefer-dist \ + --optimize-autoloader \ + --ignore-platform-req=php+ \ + --ignore-platform-req=ext-simplexml \ + --ignore-platform-req=ext-fileinfo \ + --ignore-platform-req=ext-session \ + --ignore-platform-req=ext-tokenizer \ + --ignore-platform-req=ext-xmlreader \ + --ignore-platform-req=ext-xmlwriter \ + --ignore-platform-req=ext-intl \ + --ignore-platform-req=ext-dom \ + --ignore-platform-req=ext-xml + +# Clear composer cache to save space +RUN composer clear-cache + +# Remove .git folder to save space +RUN rm -rf .git # Publish Cachet assets (config, views, and public assets) RUN php artisan vendor:publish --tag=cachet --force # Copy custom images (from build context) into the published vendor folder (overwrite default images) -COPY ./cachet-configuration-files/images/ /var/www/html/public/vendor/cachethq/cachet/ +# build context is cachet-configuration-files, so images are in ./images/ +COPY ./images/ /var/www/html/public/vendor/cachethq/cachet/ # Publish Livewire config and views RUN php artisan livewire:publish @@ -156,10 +114,13 @@ RUN php artisan filament:assets # Create storage symlink for public uploads (banner, ecc.) RUN php artisan storage:link +# Ensure migrations for database-backed drivers exist +RUN php artisan cache:table || true +RUN php artisan session:table || true +RUN php artisan queue:table || true + # Fix permissions AFTER artisan commands - PHP-FPM runs as www-data -# Important: artisan commands may create files as cachet user, so we fix ownership -RUN chown -R www-data:www-data /var/www/html \ - && chmod -R 775 /var/www/html/storage \ +RUN chmod -R 775 /var/www/html/storage \ && chmod -R 775 /var/www/html/bootstrap/cache # Create necessary directories @@ -167,9 +128,25 @@ RUN mkdir -p /var/www/html/storage/framework/{sessions,views,cache} \ && mkdir -p /var/www/html/storage/logs \ && mkdir -p /var/log/supervisor \ && mkdir -p /var/run/supervisor \ - && chown -R www-data:www-data /var/www/html/storage \ - && chown -R www-data:www-data /var/www/html/bootstrap/cache + && chown -R www-data:www-data /var/www/html +# Clean up build dependencies +RUN apk del -r --no-cache \ + postgresql-dev \ + libzip-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + oniguruma-dev \ + icu-dev \ + libxml2-dev \ + autoconf \ + g++ \ + make \ + git \ + ca-certificates \ + && rm -rf /var/cache/apk/* + # Expose port 80 for Nginx EXPOSE 80 @@ -178,4 +155,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD php artisan cachet:health || exit 1 # Start supervisor -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +CMD ["sh", "-c", "until php artisan migrate --force; do echo 'Waiting for database...'; sleep 2; done && /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf"] diff --git a/cachet-configuration-files/docker/supervisor/supervisord.conf b/cachet-configuration-files/docker/supervisor/supervisord.conf index 7f4ddad..aa0d08d 100644 --- a/cachet-configuration-files/docker/supervisor/supervisord.conf +++ b/cachet-configuration-files/docker/supervisor/supervisord.conf @@ -1,7 +1,10 @@ [supervisord] nodaemon=true user=root -logfile=/var/log/supervisor/supervisord.log +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 pidfile=/var/run/supervisord.pid [program:php-fpm] @@ -29,8 +32,10 @@ autostart=true autorestart=true stopasgroup=true killasgroup=true -user=cachet +user=www-data numprocs=1 -redirect_stderr=true -stdout_logfile=/var/www/html/storage/logs/queue-worker.log +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 stopwaitsecs=3600 diff --git a/docker-compose.yml b/docker-compose.yml index 8143cef..ac2331d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8" volumes: - - cachet-db:/var/lib/postgresql/data + - cachet-db:/var/lib/postgresql/ healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-cachet}"] interval: 10s @@ -35,11 +35,7 @@ services: # Cachet Application cachet: - build: - context: . - dockerfile: cachet/Dockerfile - args: - - PHP_VERSION=${PHP_VERSION:-8.3} + image: ghcr.io/nethesis/cachet-status-page:${CACHET_TAG:-latest} container_name: cachet-app restart: unless-stopped networks: @@ -81,22 +77,6 @@ services: volumes: - cachet-storage:/var/www/html/storage - ./cachet/.env:/var/www/html/.env:ro - labels: - - "traefik.enable=true" - # Local - - "traefik.http.routers.cachet-local.rule=Host(`${CACHET_DOMAIN:-localhost}`)" - - "traefik.http.routers.cachet-local.entrypoints=web" - - "traefik.http.routers.cachet-local.service=cachet" - # Production - - "traefik.http.routers.cachet.rule=Host(`${CACHET_DOMAIN:-localhost}`)" - - "traefik.http.routers.cachet.entrypoints=websecure" - - "traefik.http.routers.cachet.tls=true" - - "traefik.http.routers.cachet.tls.certresolver=${CERT_RESOLVER:-letsencrypt}" - - "traefik.http.routers.cachet.middlewares=https-headers@file" - - "traefik.http.routers.cachet.middlewares=redirect-to-https@file" - - "traefik.http.routers.cachet.service=cachet" - # Service - - "traefik.http.services.cachet.loadbalancer.server.port=80" healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost/"] interval: 30s @@ -106,9 +86,7 @@ services: # Python Middleware - Webhook Handler middleware: - build: - context: ./middleware - dockerfile: Dockerfile + image: ghcr.io/nethesis/middleware-status-page:${MIDDLEWARE_TAG:-latest} container_name: cachet-middleware restart: unless-stopped networks: @@ -119,33 +97,15 @@ services: environment: # Cachet API configuration - CACHET_API_URL=${CACHET_API_URL:-http://cachet/api/v1} - - CACHET_API_TOKEN=${CACHET_API_TOKEN} + - CACHET_API_TOKEN=${CACHET_API_TOKEN:-secret} # Python app settings - FLASK_ENV=production - PYTHONUNBUFFERED=1 - - ENVIRONMENT=${ENVIRONMENT:-production} + - ENVIRONMENT=${ENVIRONMENT:-local} volumes: # Mount config files as read-only - - ./middleware/config.json:/app/config.json:ro - - ./middleware/prometheus.yml:/app/prometheus.yml:ro - labels: - - "traefik.enable=true" - # Local - - "traefik.http.routers.webhook-local.rule=Host(`${WEBHOOK_DOMAIN:-localhost}`) && Path(`/webhook`)" - - "traefik.http.routers.webhook-local.entrypoints=web" - - "traefik.http.routers.webhook-local.service=middleware" - # Production - - "traefik.http.routers.webhook.rule=Host(`${WEBHOOK_DOMAIN:-localhost}`) && Path(`/webhook`)" - - "traefik.http.routers.webhook.entrypoints=websecure" - - "traefik.http.routers.webhook.tls=true" - - "traefik.http.routers.webhook.tls.certresolver=${CERT_RESOLVER:-letsencrypt}" - - "traefik.http.routers.webhook.middlewares=webhook-auth@file" - - "traefik.http.routers.webhook.middlewares=redirect-to-https@file" - - "traefik.http.routers.webhook.service=middleware" - # Service definition - - "traefik.http.services.middleware.loadbalancer.server.port=5000" - - "traefik.http.services.middleware.loadbalancer.healthcheck.path=/health" - - "traefik.http.services.middleware.loadbalancer.healthcheck.interval=30s" + - ./middleware/config.json:/app/config.json:ro,z + - ./middleware/prometheus.yml:/app/prometheus.yml:ro,z healthcheck: test: ["CMD", "python3", "/app/healthcheck.py"] interval: 30s @@ -164,16 +124,16 @@ services: - "${HTTP_PORT:-8080}:80" # HTTP entrypoint - "${HTTPS_PORT:-8443}:443" # HTTPS entrypoint volumes: - - ${PODMAN_SOCKET}:/var/run/docker.sock:ro,z # Podman socket (set PODMAN_SOCKET in .env) - - ./traefik/traefik.${ENVIRONMENT:-production}.yml:/etc/traefik/traefik.yml:ro - - ./traefik/dynamic:/etc/traefik/dynamic:ro + - ./traefik/traefik.${ENVIRONMENT:-local}.yml:/etc/traefik/traefik.yml:ro,z + - ./traefik/dynamic:/etc/traefik/dynamic:ro,z - traefik-certs:/letsencrypt environment: - TZ=${TZ:-Europe/Rome} - - ENVIRONMENT=${ENVIRONMENT:-production} - labels: - - "traefik.enable=true" - + - ENVIRONMENT=${ENVIRONMENT:-local} + - CACHET_DOMAIN=${CACHET_DOMAIN:-localhost} + - WEBHOOK_DOMAIN=${WEBHOOK_DOMAIN:-localhost} + - CERT_RESOLVER=${CERT_RESOLVER:-letsencrypt} + - WEBHOOK_BASIC_AUTH=${WEBHOOK_BASIC_AUTH} healthcheck: test: ["CMD", "traefik", "healthcheck", "--ping"] interval: 10s diff --git a/middleware/Dockerfile b/middleware/Dockerfile index b9a7f56..55108e3 100644 --- a/middleware/Dockerfile +++ b/middleware/Dockerfile @@ -1,5 +1,5 @@ # Multi-stage build for smaller final image -FROM python:3.14-slim as builder +FROM python:3.14-slim AS builder # Install build dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -29,8 +29,6 @@ WORKDIR /app # Copy application files COPY alerts-middleware.py . -COPY config.json . -COPY prometheus.yml . COPY healthcheck.py . COPY setup.py . diff --git a/traefik/dynamic/middlewares.yml.example b/traefik/dynamic/middlewares.yml.example deleted file mode 100644 index 44624af..0000000 --- a/traefik/dynamic/middlewares.yml.example +++ /dev/null @@ -1,64 +0,0 @@ -# Traefik Dynamic Configuration -# Middlewares for authentication, rate limiting, security headers, etc. -# -# IMPORTANT: Copy this file to middlewares.yml and configure the authentication -# cp traefik/dynamic/middlewares.yml.example traefik/dynamic/middlewares.yml -# -# The deploy.sh script will automatically generate the webhook-auth hash from .env - -http: - middlewares: - - # BasicAuth middleware for /webhook endpoint - webhook-auth: - basicAuth: - users: - # This will be automatically generated from .env variables: - # WEBHOOK_USERNAME and WEBHOOK_PASSWORD - # The deploy.sh script will hash the password and update this line - - "WEBHOOK_CREDENTIALS_PLACEHOLDER" - - # HTTPS redirect middleware - redirect-to-https: - redirectScheme: - scheme: https - permanent: true - - # Forward HTTPS headers to Laravel - https-headers: - headers: - customRequestHeaders: - X-Forwarded-Proto: "https" - X-Forwarded-Port: "443" - X-Forwarded-For: "" # Let Traefik handle this automatically - X-Forwarded-Host: "" # Let Traefik handle this automatically - X-Forwarded-Ssl: "on" - sslRedirect: false # Already handled by redirect-to-https middleware - forceSTSHeader: false - sslProxyHeaders: - X-Forwarded-Proto: "https" - X-Forwarded-Ssl: "on" - - # Security headers - security-headers: - headers: - frameDeny: true - contentTypeNosniff: true - browserXssFilter: true - referrerPolicy: "same-origin" - customResponseHeaders: - X-Robots-Tag: "noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex" - stsSeconds: 31536000 - stsIncludeSubdomains: true - stsPreload: true - - # Rate limiting for webhook (optional) - webhook-rate-limit: - rateLimit: - average: 100 # requests per second - burst: 50 # burst size - period: 1s - - # Compress responses - compression: - compress: {} diff --git a/traefik/dynamic/routers.yml b/traefik/dynamic/routers.yml new file mode 100644 index 0000000..22ac08e --- /dev/null +++ b/traefik/dynamic/routers.yml @@ -0,0 +1,33 @@ +http: + routers: + cachet: + rule: 'Host(`{{ env "CACHET_DOMAIN" | default "localhost" }}`)' + service: cachet + {{ if eq (env "ENVIRONMENT") "production" }} + entryPoints: + - websecure + tls: + certResolver: '{{ env "CERT_RESOLVER" | default "letsencrypt" }}' + middlewares: + - https-headers + - redirect-to-https + {{ else }} + entryPoints: + - web + {{ end }} + + webhook: + rule: 'Host(`{{ env "WEBHOOK_DOMAIN" | default "localhost" }}`) && Path(`/webhook`)' + service: middleware + {{ if eq (env "ENVIRONMENT") "production" }} + entryPoints: + - websecure + tls: + certResolver: '{{ env "CERT_RESOLVER" | default "letsencrypt" }}' + middlewares: + - webhook-auth + - redirect-to-https + {{ else }} + entryPoints: + - web + {{ end }} diff --git a/traefik/dynamic/services.yml b/traefik/dynamic/services.yml new file mode 100644 index 0000000..7af43ac --- /dev/null +++ b/traefik/dynamic/services.yml @@ -0,0 +1,14 @@ +http: + services: + cachet: + loadBalancer: + servers: + - url: "http://cachet:80" + + middleware: + loadBalancer: + servers: + - url: "http://middleware:5000" + healthCheck: + path: /health + interval: 30s diff --git a/traefik/traefik.local.yml b/traefik/traefik.local.yml index 4473abc..cb946d2 100644 --- a/traefik/traefik.local.yml +++ b/traefik/traefik.local.yml @@ -14,11 +14,6 @@ entryPoints: # Providers providers: - docker: - endpoint: "unix:///var/run/docker.sock" - exposedByDefault: false - network: status_cachet-network - file: directory: /etc/traefik/dynamic watch: true diff --git a/traefik/traefik.production.yml b/traefik/traefik.production.yml index f4cf332..3c680b8 100644 --- a/traefik/traefik.production.yml +++ b/traefik/traefik.production.yml @@ -29,11 +29,6 @@ certificatesResolvers: # Providers providers: - docker: - endpoint: "unix:///var/run/docker.sock" - exposedByDefault: false - network: status_cachet-network - file: directory: /etc/traefik/dynamic watch: true From 9ed5c7410f0d021c6b936bc3813c36494760b95a Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 22 Dec 2025 13:44:47 +0100 Subject: [PATCH 2/2] fix: deploy, use pre-built images --- deploy.sh | 165 +++++++++--------------------------------------------- 1 file changed, 26 insertions(+), 139 deletions(-) diff --git a/deploy.sh b/deploy.sh index a69882e..0b04ecd 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,39 +7,6 @@ echo "==========================================" echo "Cachet Infrastructure Deployment" echo "==========================================" -# Prepare Cachet repository -echo "" - -echo "Preparing Cachet repository..." -# Remove existing local cachet folder if present -if [ -d "cachet" ]; then - echo "Removing existing cachet/ folder..." - rm -rf cachet -fi - -# Copy SSH directory for Docker build context (for composer update in Dockerfile) -SSH_BUILD_CONTEXT=".ssh" -if [ -d "$SSH_BUILD_CONTEXT" ]; then - echo "Removing old .ssh from build context root..." - rm -rf "$SSH_BUILD_CONTEXT" -fi -echo "Copying ~/.ssh to project root for build context..." -cp -r ~/.ssh "$SSH_BUILD_CONTEXT" - -# Clone Cachet repository -echo "Cloning cachet repository..." -git clone git@github.com:cachethq/cachet.git cachet || { echo "Error cloning cachet"; exit 1; } - -# Copy Dockerfile and docker folder into the new cachet folder -cp cachet-configuration-files/Dockerfile cachet/Dockerfile -cp -r cachet-configuration-files/docker cachet/ - -# Overwrite TrustProxies.php in the existing directory -cp cachet-configuration-files/proxy/TrustProxies.php cachet/app/Http/Middleware/TrustProxies.php - -# Copy AdminSeeder.php to the container to create admin user and API token -cp cachet-configuration-files/token/AdminSeeder.php cachet/database/seeders/AdminSeeder.php - # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -55,62 +22,29 @@ fi # Check if .env exists if [ ! -f .env ]; then - echo -e "${YELLOW}Warning: .env file not found${NC}" - echo "Copying .env.example to .env..." - cp .env.example .env - echo -e "${YELLOW}Please edit .env file with your configuration before proceeding${NC}" - echo "Press CTRL+C to exit and edit .env, or press Enter to continue..." - read + echo -e "${RED}Warning: .env file not found${NC}" + echo "Copy it from .env.example to .env" + exit 1 fi -# Load environment variables -source .env - -# Auto-detect and set PODMAN_SOCKET in .env if not set or invalid -detect_and_set_podman_socket() { - local socket_path="" - if [ "$EUID" -eq 0 ]; then - # Root context - socket_path="/run/podman/podman.sock" - else - # Rootless context - socket_path="/run/user/$(id -u)/podman/podman.sock" - fi - - # If PODMAN_SOCKET is not set or is not the detected value, update .env - if ! grep -q "^PODMAN_SOCKET=" .env; then - echo "PODMAN_SOCKET=\"$socket_path\"" >> .env - export PODMAN_SOCKET="$socket_path" - echo -e "${GREEN}✓${NC} PODMAN_SOCKET set to $socket_path in .env" - else - current_socket=$(grep "^PODMAN_SOCKET=" .env | cut -d= -f2- | tr -d '"') - if [ "$current_socket" != "$socket_path" ]; then - sed -i "s|^PODMAN_SOCKET=.*|PODMAN_SOCKET=\"$socket_path\"|" .env - export PODMAN_SOCKET="$socket_path" - echo -e "${GREEN}✓${NC} PODMAN_SOCKET updated to $socket_path in .env" - else - export PODMAN_SOCKET="$current_socket" - echo -e "${GREEN}✓${NC} PODMAN_SOCKET already set correctly in .env" - fi - fi -} +# Check if middleware/config.json exists +if [ ! -f middleware/config.json ]; then + echo -e "${RED}Warning: middleware/config.json file not found${NC}" + echo "Copy it from middleware/config.json.example to middleware/config.json" + exit 1 +fi -detect_and_set_podman_socket -# Check and start Podman rootless socket if needed -if [ "$EUID" -ne 0 ] && [[ "$PODMAN_SOCKET" == /run/user/* ]]; then - echo "Checking Podman rootless socket..." - if ! systemctl --user is-active --quiet podman.socket; then - echo "Podman rootless socket is not active. Starting it..." - systemctl --user start podman.socket - sleep 2 - fi - if ! systemctl --user is-active --quiet podman.socket; then - echo "Error: Podman rootless socket could not be started. Please check your user session and permissions." - exit 1 - fi +# Check if middleware/prometheus.yml exists +if [ ! -f middleware/prometheus.yml ]; then + echo -e "${RED}Warning: middleware/prometheus.yml file not found${NC}" + echo "Copy it from middleware/prometheus.yml.example to middleware/prometheus.yml" + exit 1 fi +# Load environment variables +source .env + # Check required environment variables REQUIRED_VARS=("DB_PASSWORD") MISSING_VARS=() @@ -142,20 +76,10 @@ fi # Check if podman-compose is available if ! command -v podman-compose &> /dev/null; then - echo -e "${YELLOW}podman-compose not found. Installing...${NC}" - pip3 install --user podman-compose + echo -e "${YELLOW}podman-compose not found. ${NC}" + exit 1 fi -# Create necessary directories -echo "Creating necessary directories..." -mkdir -p middleware/logs -mkdir -p cachet/storage/{app,framework,logs} -mkdir -p cachet/storage/framework/{sessions,views,cache} - -# Set proper permissions -chmod -R 755 middleware/logs -chmod -R 755 cachet/storage - # Generate APP_KEY if not present in podman-setup/.env echo "" echo "Checking Laravel APP_KEY in .env..." @@ -177,15 +101,14 @@ else echo -e "${GREEN}✓${NC} APP_KEY already exists in .env" fi -# Ensure cachet/.env exists (used for default values not overridden by ENV vars) +# Ensure .env exists (used for default values not overridden by ENV vars) echo "" -echo "Checking cachet/.env file..." -if [ ! -f cachet/.env ]; then - echo -e "${YELLOW}Warning: cachet/.env not found, copying from cachet/.env.example${NC}" - cp cachet/.env.example cachet/.env - echo -e "${GREEN}✓${NC} cachet/.env created from example" +echo "Checking .env file..." +if [ ! -f .env ]; then + echo -e "${RED}Warning: .env not found. Copy it from .env.example...${NC}" + exit 1 else - echo -e "${GREEN}✓${NC} cachet/.env exists" + echo -e "${GREEN}✓${NC} .env exists" fi # Configure webhook authentication in Traefik middlewares @@ -217,23 +140,6 @@ echo "==========================================" echo "Starting deployment..." echo "==========================================" -# Build and start services -echo "Building images..." - -# Check that .ssh exists in build context before build -if [ ! -d ".ssh" ]; then - echo "Error: .ssh directory not found in build context root! Aborting build." - exit 1 -fi - - -podman-compose build traefik postgres cachet - -# Remove SSH directory from build context after build for security -if [ -d "$SSH_BUILD_CONTEXT" ]; then - echo "Removing .ssh from build context root after build..." - rm -rf "$SSH_BUILD_CONTEXT" -fi echo "" echo "Starting core services (excluding middleware)..." @@ -244,11 +150,6 @@ echo "" echo "Waiting for database to be ready..." sleep 10 -# Run Cachet migrations and setup -echo "" -echo "Setting up Cachet database..." -podman-compose exec -T cachet php artisan migrate --force - # Run the AdminSeeder to create admin user and relative token for APIs echo "Running AdminSeeder to create admin user and API token..." adminseeder_output=$(podman-compose exec -T cachet php artisan db:seed --class=AdminSeeder --force 2>&1) @@ -279,11 +180,6 @@ echo "Clearing and optimizing cache..." podman-compose exec -T cachet php artisan optimize:clear podman-compose exec -T cachet php artisan optimize -echo "" -echo "Fixing permissions (post-startup)..." -podman-compose exec -T cachet chown -R www-data:www-data /var/www/html/bootstrap/cache /var/www/html/storage -podman-compose exec -T cachet chmod -R 775 /var/www/html/bootstrap/cache /var/www/html/storage - # Fix Traefik acme.json permissions for Let's Encrypt when deploying with rootless Podman echo "" echo "Checking traefik-certs volume permissions..." @@ -315,18 +211,9 @@ fi # === Build and Start Middleware and Setup Components === echo "" echo "==========================================" -echo "Building and Starting Middleware, Initializing Components" +echo "Starting Middleware, Initializing Components" echo "==========================================" -# Build middleware image (after token is generated) -echo "Building middleware image..." -podman-compose build middleware - -# Check that the middleware image exists after build -if ! podman images | grep -q "middleware"; then - echo -e "${RED}Error: Middleware image not found after build. Aborting.${NC}" - echo "Check that the 'middleware' service has a correct build: section in your docker-compose.yml." - exit 1 fi # Start middleware container