From 39ef1656551a679a8065ae93e1b15d224093e61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 29 Dec 2025 13:41:45 +0100 Subject: [PATCH] Add production Docker setup for easy self-hosting with the CI-built image (#42) - Add production Docker setup for easy self-hosting with the CI-built image - Build multi-architecture Docker images (amd64 + arm64) ### Changes Docker Production Setup - Add docker-compose.production.yml for running the production image locally - Add docker/entrypoint.sh with automatic setup (migrations, caching, storage, APP_KEY generation) - Add HEALTHCHECK to Dockerfile for container health monitoring - Add .env.production.example with documented production defaults ### CI/CD - Add QEMU for multi-arch builds - Build Docker images for both linux/amd64 and linux/arm64 ### Coolify / WIP - Add templates/coolify/whisper-money.yaml one-click template - Includes MySQL service with health checks - Auto-generates credentials using Coolify's SERVICE_* variables ### Documentation - Update README with production Docker instructions - Add environment variable reference table --- .env.production.example | 56 +++++++++++++++++++++++ .github/workflows/ci.yml | 4 ++ Dockerfile | 12 ++++- README.md | 31 +++++++++++++ docker-compose.production.yml | 36 +++++++++++++++ docker/entrypoint.sh | 62 +++++++++++++++++++++++++ templates/coolify/whisper-money.yaml | 68 ++++++++++++++++++++++++++++ 7 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 .env.production.example create mode 100644 docker-compose.production.yml create mode 100644 docker/entrypoint.sh create mode 100644 templates/coolify/whisper-money.yaml diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 00000000..1778e1ac --- /dev/null +++ b/.env.production.example @@ -0,0 +1,56 @@ +# =========================================== +# Whisper Money - Production Environment +# =========================================== +# Copy this file to .env and configure for your production environment + +# Application +APP_NAME="Whisper Money" +APP_ENV=production +APP_DEBUG=false +APP_URL=https://your-domain.com +APP_KEY= # Auto-generated on first startup if empty + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +# Logging +LOG_CHANNEL=stack +LOG_STACK=single +LOG_LEVEL=error + +# Database (MySQL) +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=whisper_money +DB_USERNAME=whisper_money +DB_PASSWORD=your-secure-password + +# Cache & Sessions (Redis runs inside the container) +CACHE_STORE=redis +SESSION_DRIVER=redis +SESSION_LIFETIME=120 +SESSION_ENCRYPT=true + +# Queue +QUEUE_CONNECTION=database + +# Redis (internal to container - no configuration needed) +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 + +# Email - Resend (Recommended for production) +MAIL_MAILER=resend +RESEND_API_KEY=your-resend-api-key +MAIL_FROM_ADDRESS=hi@your-domain.com +MAIL_FROM_NAME="Whisper Money" + +# Stripe (Optional - for subscriptions) +STRIPE_KEY= +STRIPE_SECRET= +STRIPE_WEBHOOK_SECRET= +SUBSCRIPTIONS_ENABLED=false + +# UI Configuration +HIDE_AUTH_BUTTONS=false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e2183e..f8a2c6ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,6 +166,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -189,6 +192,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 01702b25..249e824d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,5 +70,13 @@ RUN chown -R www-data:www-data /app \ # Expose port EXPOSE 80 -# Start supervisor -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost/up || exit 1 + +# Copy entrypoint script +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Run entrypoint script (handles migrations, caching, then starts supervisor) +CMD ["/entrypoint.sh"] diff --git a/README.md b/README.md index e9a63456..fed5fb1b 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,37 @@ This will concurrently start: The application will be available at `https://whispermoney.test`. +## Running with Docker (Production Image) + +For testing the production Docker image locally: + +1. **Copy the production environment file:** + +```bash +cp .env.production.example .env +``` + +2. **Start the services:** + +```bash +docker compose -f docker-compose.production.yml up -d +``` + +The application will be available at `http://localhost:8080`. + +To use a different port, set `APP_PORT`: + +```bash +APP_PORT=3000 docker compose -f docker-compose.production.yml up -d +``` + +## Other Environment Variables + +| Variable | Default | Description | +| ----------------------- | ------- | ------------------------------------------- | +| `HIDE_AUTH_BUTTONS` | `false` | Hide login/register buttons on landing page | +| `SUBSCRIPTIONS_ENABLED` | `false` | Enable Stripe subscriptions | + ## License This work is licensed under a diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 00000000..104b93f5 --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,36 @@ +services: + app: + image: ${WHISPER_IMAGE:-ghcr.io/whisper-money/whisper-money:latest} + ports: + - "${APP_PORT:-8080}:80" + env_file: + - .env + environment: + - DB_HOST=mysql + - DB_PORT=3306 + volumes: + - whisper-storage:/app/storage + depends_on: + mysql: + condition: service_healthy + restart: unless-stopped + + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - whisper-mysql:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + +volumes: + whisper-storage: + whisper-mysql: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..60df3dcb --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -e + +echo "=== Whisper Money Container Startup ===" + +# Ensure storage directories exist (volume may be empty on first deploy) +echo "Ensuring storage directories exist..." +mkdir -p /app/storage/app/public +mkdir -p /app/storage/framework/cache/data +mkdir -p /app/storage/framework/sessions +mkdir -p /app/storage/framework/views +mkdir -p /app/storage/logs +chown -R www-data:www-data /app/storage +chmod -R 775 /app/storage + +# Auto-generate APP_KEY if not set +if [ -z "$APP_KEY" ]; then + echo "No APP_KEY found, generating one..." + APP_KEY=$(php artisan key:generate --show) + export APP_KEY + echo "Generated APP_KEY: $APP_KEY" + echo "" + echo "==================================================" + echo "IMPORTANT: Save this key in your environment!" + echo "APP_KEY=$APP_KEY" + echo "==================================================" + echo "" +fi + +# Wait for MySQL to be ready +echo "Waiting for MySQL..." +max_attempts=30 +attempt=0 +until php artisan db:monitor --databases=mysql > /dev/null 2>&1 || [ $attempt -ge $max_attempts ]; do + attempt=$((attempt + 1)) + echo "MySQL not ready (attempt $attempt/$max_attempts)..." + sleep 2 +done + +if [ $attempt -ge $max_attempts ]; then + echo "Warning: MySQL may not be ready, proceeding anyway..." +fi + +# Run migrations +echo "Running migrations..." +php artisan migrate --force + +# Cache configuration +echo "Caching configuration..." +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan event:cache + +# Create storage link (harmless if already exists) +echo "Creating storage link..." +php artisan storage:link 2>/dev/null || true + +echo "=== Startup complete, launching services ===" + +# Start supervisor +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/templates/coolify/whisper-money.yaml b/templates/coolify/whisper-money.yaml new file mode 100644 index 00000000..a44dbf98 --- /dev/null +++ b/templates/coolify/whisper-money.yaml @@ -0,0 +1,68 @@ +# documentation: https://github.com/whisper-money/whisper-money +# slogan: Privacy-first personal finance with end-to-end encryption +# tags: finance, personal-finance, budgeting, privacy, encryption, laravel, react +# logo: svgs/whisper-money.svg +# port: 80 + +services: + whisper-money: + image: ghcr.io/whisper-money/whisper-money:latest + environment: + - SERVICE_FQDN_WHISPERMONEY_80 + - APP_NAME=Whisper Money + - APP_ENV=production + - APP_DEBUG=false + - APP_KEY=${SERVICE_BASE64_64_APPKEY} + - APP_URL=${SERVICE_FQDN_WHISPERMONEY_80} + - DB_CONNECTION=mysql + - DB_HOST=mysql + - DB_PORT=3306 + - DB_DATABASE=${MYSQL_DATABASE:-whisper_money} + - DB_USERNAME=${SERVICE_USER_MYSQL} + - DB_PASSWORD=${SERVICE_PASSWORD_MYSQL} + - CACHE_STORE=redis + - SESSION_DRIVER=redis + - QUEUE_CONNECTION=database + - REDIS_HOST=127.0.0.1 + - HIDE_AUTH_BUTTONS=${HIDE_AUTH_BUTTONS:-false} + - MAIL_MAILER=${MAIL_MAILER:-log} + - MAIL_HOST=${MAIL_HOST:-} + - MAIL_PORT=${MAIL_PORT:-587} + - MAIL_USERNAME=${MAIL_USERNAME:-} + - MAIL_PASSWORD=${MAIL_PASSWORD:-} + - MAIL_FROM_ADDRESS=${MAIL_FROM_ADDRESS:-hi@whisper.money} + - MAIL_FROM_NAME=${MAIL_FROM_NAME:-Whisper Money} + - RESEND_API_KEY=${RESEND_API_KEY:-} + - STRIPE_KEY=${STRIPE_KEY:-} + - STRIPE_SECRET=${STRIPE_SECRET:-} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-} + - SUBSCRIPTIONS_ENABLED=${SUBSCRIPTIONS_ENABLED:-false} + volumes: + - whisper-money-storage:/app/storage + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/up"] + interval: 30s + timeout: 10s + retries: 3 + + mysql: + image: mysql:8.0 + environment: + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT} + - MYSQL_DATABASE=${MYSQL_DATABASE:-whisper_money} + - MYSQL_USER=${SERVICE_USER_MYSQL} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} + volumes: + - whisper-money-mysql:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${SERVICE_PASSWORD_MYSQLROOT}"] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + whisper-money-storage: + whisper-money-mysql: