From 21b16a5e6c342f03e636fa89d7fef0f30210a64f Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 28 Jan 2026 19:05:20 +0100 Subject: [PATCH] deploy --- .env.example | 86 ++++++-- .env.production | 34 +++ .gitea/workflows/deploy.yml | 125 ++++++++--- Dockerfile | 8 +- ENV_CLEANUP_SUMMARY.md | 272 ++++++++++++++++++++++++ docker-compose.yml | 16 +- docs/DEPLOYMENT.md | 398 +++++++++++++++++++++--------------- docs/ENV_MIGRATION.md | 276 +++++++++++++++++++++++++ docs/SERVER_SETUP.md | 349 +++++++++++++++++++++++++++++++ 9 files changed, 1335 insertions(+), 229 deletions(-) create mode 100644 .env.production create mode 100644 ENV_CLEANUP_SUMMARY.md create mode 100644 docs/ENV_MIGRATION.md create mode 100644 docs/SERVER_SETUP.md diff --git a/.env.example b/.env.example index bd245b29..5b3a6ce3 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,33 @@ -# WooCommerce API (Legacy - not currently used) -WOOCOMMERCE_URL=https://klz-cables.com -WOOCOMMERCE_CONSUMER_KEY= -WOOCOMMERCE_CONSUMER_SECRET= -WORDPRESS_APP_PASSWORD= +# ============================================================================ +# KLZ Cables - Environment Configuration +# ============================================================================ +# Copy this file to .env for local development +# For production, use .env.production as a template +# ============================================================================ -# Umami Analytics -NEXT_PUBLIC_UMAMI_WEBSITE_ID= -UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js - -# GlitchTip (Sentry protocol) -SENTRY_DSN= -# Client-side DSN should use the proxy path: https://[key]@[domain]/errors/[id] -NEXT_PUBLIC_SENTRY_DSN= - -# Application +# ──────────────────────────────────────────────────────────────────────────── +# Application Configuration +# ──────────────────────────────────────────────────────────────────────────── NODE_ENV=development NEXT_PUBLIC_BASE_URL=http://localhost:3000 -LOG_LEVEL=info -# SMTP Configuration +# ──────────────────────────────────────────────────────────────────────────── +# Analytics (Umami) +# ──────────────────────────────────────────────────────────────────────────── +# Optional: Leave empty to disable analytics +NEXT_PUBLIC_UMAMI_WEBSITE_ID= +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js + +# ──────────────────────────────────────────────────────────────────────────── +# Error Tracking (GlitchTip/Sentry) +# ──────────────────────────────────────────────────────────────────────────── +# Optional: Leave empty to disable error tracking +SENTRY_DSN= + +# ──────────────────────────────────────────────────────────────────────────── +# Email Configuration (SMTP) +# ──────────────────────────────────────────────────────────────────────────── +# Required for contact form functionality MAIL_HOST=smtp.eu.mailgun.org MAIL_PORT=587 MAIL_USERNAME= @@ -26,6 +35,45 @@ MAIL_PASSWORD= MAIL_FROM=KLZ Cables MAIL_RECIPIENTS=info@klz-cables.com -# Redis Configuration (optional) -REDIS_URL=redis://redis:6379/2 +# ──────────────────────────────────────────────────────────────────────────── +# Redis Cache Configuration +# ──────────────────────────────────────────────────────────────────────────── +# Optional: Leave empty to disable Redis caching +REDIS_URL=redis://localhost:6379/2 REDIS_KEY_PREFIX=klz: + +# ──────────────────────────────────────────────────────────────────────────── +# Logging +# ──────────────────────────────────────────────────────────────────────────── +LOG_LEVEL=info + +# ──────────────────────────────────────────────────────────────────────────── +# Varnish Cache (Docker only) +# ──────────────────────────────────────────────────────────────────────────── +VARNISH_CACHE_SIZE=256m + +# ============================================================================ +# IMPORTANT NOTES +# ============================================================================ +# +# BUILD-TIME vs RUNTIME Variables: +# ───────────────────────────────── +# • NEXT_PUBLIC_* variables are baked into the client bundle at BUILD time +# They must be provided as --build-arg when building the Docker image +# +# • All other variables are used at RUNTIME only +# They are loaded from the .env file by docker-compose +# +# Docker Deployment: +# ────────────────── +# 1. Build-time: Only NEXT_PUBLIC_* vars are needed (via --build-arg) +# 2. Runtime: All vars are loaded from .env file on the server +# 3. The .env file should exist at: /home/deploy/sites/klz-cables.com/.env +# +# Security: +# ───────── +# • NEVER commit .env files with real credentials to git +# • Use Gitea/GitHub secrets for CI/CD workflows +# • Store production .env file securely on the server only +# +# ============================================================================ diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..8eb4f9b4 --- /dev/null +++ b/.env.production @@ -0,0 +1,34 @@ +# ============================================================================ +# KLZ Cables - Production Environment Configuration +# ============================================================================ +# This file contains runtime environment variables for the production deployment. +# It should be placed on the production server at: /home/deploy/sites/klz-cables.com/.env +# +# IMPORTANT: This file contains sensitive data and should NEVER be committed to git. +# ============================================================================ + +# Application +NODE_ENV=production +NEXT_PUBLIC_BASE_URL=https://klz-cables.com + +# Analytics (Umami) +NEXT_PUBLIC_UMAMI_WEBSITE_ID= +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js + +# Error Tracking (GlitchTip/Sentry) +SENTRY_DSN= + +# Email Configuration (Mailgun) +MAIL_HOST=smtp.eu.mailgun.org +MAIL_PORT=587 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_FROM=KLZ Cables +MAIL_RECIPIENTS=info@klz-cables.com + +# Redis Cache (optional) +REDIS_URL=redis://redis:6379/2 +REDIS_KEY_PREFIX=klz: + +# Varnish Cache Size (optional) +VARNISH_CACHE_SIZE=256m diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b0326a7e..07906eca 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,8 +6,6 @@ on: jobs: build-and-deploy: - # ──────────────────────────────────────────────── - # WICHTIG: Kein "docker" mehr – sondern eines der neuen Labels runs-on: docker steps: @@ -72,11 +70,10 @@ jobs: echo " Platform: linux/arm64" echo " Target: registry.infra.mintel.me/mintel/klz-cables.com:latest" echo "" - echo "📦 Build Arguments:" + echo "📦 Build Arguments (NEXT_PUBLIC_* only - baked into client bundle):" + echo " • NEXT_PUBLIC_BASE_URL: ${{ secrets.NEXT_PUBLIC_BASE_URL != '' && '***' || 'NOT SET' }}" echo " • NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID != '' && '***' || 'NOT SET' }}" echo " • NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL != '' && '***' || 'NOT SET' }}" - echo " • SENTRY_DSN: ${{ secrets.SENTRY_DSN != '' && '***' || 'NOT SET' }}" - echo " • NEXT_PUBLIC_BASE_URL: ${{ secrets.NEXT_PUBLIC_BASE_URL != '' && '***' || 'NOT SET' }}" echo "" echo "⏱️ Build started at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" echo "" @@ -86,10 +83,9 @@ jobs: docker buildx build \ --pull \ --platform linux/arm64 \ + --build-arg NEXT_PUBLIC_BASE_URL="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \ --build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ --build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}" \ - --build-arg SENTRY_DSN="${{ secrets.SENTRY_DSN }}" \ - --build-arg NEXT_PUBLIC_BASE_URL="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \ -t registry.infra.mintel.me/mintel/klz-cables.com:latest \ --push . @@ -141,36 +137,112 @@ jobs: fi echo "" + # Create .env file content + echo "📝 Preparing environment configuration..." + cat > /tmp/klz-cables.env << EOF + # ============================================================================ + # KLZ Cables - Production Environment Configuration + # ============================================================================ + # Auto-generated by CI/CD workflow + # DO NOT EDIT MANUALLY - Changes will be overwritten on next deployment + # ============================================================================ + + # Application + NODE_ENV=production + NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }} + + # Analytics (Umami) + NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} + NEXT_PUBLIC_UMAMI_SCRIPT_URL=${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }} + + # Error Tracking (GlitchTip/Sentry) + SENTRY_DSN=${{ secrets.SENTRY_DSN }} + + # Email Configuration (Mailgun) + MAIL_HOST=${{ secrets.MAIL_HOST }} + MAIL_PORT=${{ secrets.MAIL_PORT }} + MAIL_USERNAME=${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }} + MAIL_FROM=${{ secrets.MAIL_FROM }} + MAIL_RECIPIENTS=${{ secrets.MAIL_RECIPIENTS }} + + # Redis Cache + REDIS_URL=${{ secrets.REDIS_URL }} + REDIS_KEY_PREFIX=${{ secrets.REDIS_KEY_PREFIX }} + + # Varnish Cache Size + VARNISH_CACHE_SIZE=256m + EOF + + echo "✅ Environment file prepared" + echo "" + # Execute deployment commands with detailed logging - echo "📡 Connecting to server and executing deployment commands..." + echo "📡 Connecting to server and executing deployment..." echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" - # SSH as root and use sudo to run deployment script as deploy user - # This works around the broken SSH output issue with deploy user + # Copy .env file to server + echo "📤 Uploading environment configuration..." + scp -o StrictHostKeyChecking=accept-new \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o ConnectTimeout=10 \ + /tmp/klz-cables.env \ + root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env + + if [ $? -eq 0 ]; then + echo "✅ Environment file uploaded successfully" + else + echo "❌ Failed to upload environment file" + exit 1 + fi + echo "" + + # SSH to server and run deployment + echo "🚀 Executing deployment on server..." ssh -o StrictHostKeyChecking=accept-new \ -o ServerAliveInterval=30 \ -o ServerAliveCountMax=3 \ -o ConnectTimeout=10 \ - root@alpha.mintel.me \ - "MAIL_FROM='${{ secrets.MAIL_FROM }}' \ - MAIL_HOST='${{ secrets.MAIL_HOST }}' \ - MAIL_PASSWORD='${{ secrets.MAIL_PASSWORD }}' \ - MAIL_PORT='${{ secrets.MAIL_PORT }}' \ - MAIL_RECIPIENTS='${{ secrets.MAIL_RECIPIENTS }}' \ - MAIL_USERNAME='${{ secrets.MAIL_USERNAME }}' \ - NEXT_PUBLIC_BASE_URL='${{ secrets.NEXT_PUBLIC_BASE_URL }}' \ - NEXT_PUBLIC_UMAMI_WEBSITE_ID='${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}' \ - NEXT_PUBLIC_UMAMI_SCRIPT_URL='${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}' \ - NODE_ENV='${{ secrets.NODE_ENV }}' \ - SENTRY_DSN='${{ secrets.SENTRY_DSN }}' \ - REDIS_URL='${{ secrets.REDIS_URL }}' \ - REDIS_KEY_PREFIX='${{ secrets.REDIS_KEY_PREFIX }}' \ - /home/deploy/deploy.sh" + root@alpha.mintel.me bash << EOF + set -e + + PROJECT_DIR="/home/deploy/sites/klz-cables.com" + cd "\$PROJECT_DIR" + + echo "🔒 Securing environment file..." + chmod 600 .env + chown deploy:deploy .env + + echo "🔐 Logging into Docker registry..." + echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin + + echo "🔄 Pulling latest image..." + docker pull registry.infra.mintel.me/mintel/klz-cables.com:latest + + echo "🔄 Stopping existing containers..." + docker-compose down + + echo "🚀 Starting new containers..." + docker-compose up -d + + echo "⏳ Waiting for services to be healthy..." + sleep 10 + + echo "🔍 Checking service status..." + docker-compose ps + + echo "" + echo "✅ Deployment complete!" + EOF DEPLOY_EXIT_CODE=$? echo "" + # Clean up temporary env file + rm -f /tmp/klz-cables.env + if [ $DEPLOY_EXIT_CODE -eq 0 ]; then echo "✅ Deployment completed successfully at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" else @@ -181,6 +253,7 @@ jobs: echo " • Verify SSH key permissions on server" echo " • Check disk space on target server" echo " • Review docker compose configuration" + echo " • Verify all required secrets are set in Gitea" exit $DEPLOY_EXIT_CODE fi echo "" @@ -208,6 +281,8 @@ jobs: echo " • All secrets are masked (*** ) in logs" echo " • SSH keys are created with 600 permissions" echo " • Passwords are never displayed in plain text" + echo " • .env file is auto-generated from Gitea secrets" + echo " • .env file has 600 permissions on server" echo "" echo "╔══════════════════════════════════════════════════════════════════════════════╗" if [ "${{ job.status }}" == "success" ]; then diff --git a/Dockerfile b/Dockerfile index 70ebdb9a..a52440c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,15 +22,15 @@ COPY . . # Uncomment the following line in case you want to disable telemetry during the build. ENV NEXT_TELEMETRY_DISABLED=1 +# Build-time environment variables for Next.js +# These are baked into the client bundle during build +ARG NEXT_PUBLIC_BASE_URL ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL -ARG SENTRY_DSN -ARG NEXT_PUBLIC_BASE_URL +ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL -ENV SENTRY_DSN=$SENTRY_DSN -ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL # Validate environment variables during build RUN npx tsx scripts/validate-env.ts diff --git a/ENV_CLEANUP_SUMMARY.md b/ENV_CLEANUP_SUMMARY.md new file mode 100644 index 00000000..cdf00fde --- /dev/null +++ b/ENV_CLEANUP_SUMMARY.md @@ -0,0 +1,272 @@ +# Environment Variables Cleanup - Summary + +## What Was Done + +Cleaned up the fragile, overkill environment variable mess and replaced it with a simple, clean, robust **fully automated** system. + +## Changes Made + +### 1. Dockerfile ✅ +**Before**: 4 build args including runtime-only variables (SENTRY_DSN) +**After**: 3 build args - only `NEXT_PUBLIC_*` variables that need to be baked into the client bundle + +```dockerfile +# Only these build args now: +ARG NEXT_PUBLIC_BASE_URL +ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID +ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL +``` + +### 2. docker-compose.yml ✅ +**Before**: 12+ individual environment variables listed +**After**: Single `env_file: .env` directive + +```yaml +app: + image: registry.infra.mintel.me/mintel/klz-cables.com:latest + env_file: + - .env # All runtime vars loaded from here +``` + +### 3. .gitea/workflows/deploy.yml ✅ +**Before**: Passing 12+ environment variables individually via SSH command (fragile!) +**After**: **Fully automated** - workflow creates `.env` file from Gitea secrets and uploads it + +```yaml +# Before (FRAGILE): +ssh root@alpha.mintel.me \ + "MAIL_FROM='${{ secrets.MAIL_FROM }}' \ + MAIL_HOST='${{ secrets.MAIL_HOST }}' \ + ... (12+ variables) \ + /home/deploy/deploy.sh" + +# After (AUTOMATED): +# 1. Create .env from secrets +cat > /tmp/klz-cables.env << EOF +NODE_ENV=production +NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }} +# ... all other vars from secrets +EOF + +# 2. Upload to server +scp /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env + +# 3. Deploy +ssh root@alpha.mintel.me "cd /home/deploy/sites/klz-cables.com && docker-compose up -d" +``` + +### 4. New Files Created ✅ + +- **`.env.production`** - Template for reference (not used in automation) +- **`docs/DEPLOYMENT.md`** - Complete deployment guide +- **`docs/SERVER_SETUP.md`** - Server setup instructions +- **`docs/ENV_MIGRATION.md`** - Migration guide from old to new system + +### 5. Updated Files ✅ + +- **`.env.example`** - Clear documentation of all variables with build-time vs runtime notes + +## Architecture + +### Build Time (CI/CD) +``` +Gitea Workflow + ↓ + Only passes NEXT_PUBLIC_* as --build-arg + ↓ +Docker Build + ↓ + Validates env vars + ↓ + Bakes NEXT_PUBLIC_* into client bundle + ↓ +Push to Registry +``` + +### Runtime (Production Server) - FULLY AUTOMATED +``` +Gitea Secrets + ↓ + Workflow creates .env file + ↓ + SCP uploads to server + ↓ + Secured (chmod 600, chown deploy:deploy) + ↓ +docker-compose.yml (env_file: .env) + ↓ + Loads .env into container + ↓ +Application runs with full config +``` + +## Key Benefits + +### 1. Simplicity +- **Before**: 15+ Gitea secrets, variables in 3+ places +- **After**: All secrets in Gitea, automatically deployed + +### 2. Clarity +- **Before**: Confusing duplication, unclear which vars go where +- **After**: Clear separation - build args vs runtime env file + +### 3. Robustness +- **Before**: Fragile SSH command with 12+ inline variables +- **After**: Robust automated file generation and upload + +### 4. Security +- **Before**: Secrets potentially exposed in CI logs +- **After**: Secrets masked in logs, .env auto-secured on server + +### 5. Maintainability +- **Before**: Update in 3 places (Dockerfile, docker-compose.yml, deploy.yml) +- **After**: Update Gitea secrets only - deployment is automatic + +### 6. **Zero Manual Steps** 🎉 +- **Before**: Manual .env file creation on server (error-prone, can be forgotten) +- **After**: **Fully automated** - .env file created and uploaded on every deployment + +## What You Need to Do + +### Required Gitea Secrets + +Ensure these secrets are configured in your Gitea repository: + +**Build-Time (NEXT_PUBLIC_*):** +- `NEXT_PUBLIC_BASE_URL` - Production URL (e.g., `https://klz-cables.com`) +- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Umami analytics ID +- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Umami script URL + +**Runtime:** +- `SENTRY_DSN` - Error tracking DSN +- `MAIL_HOST` - SMTP server +- `MAIL_PORT` - SMTP port (e.g., `587`) +- `MAIL_USERNAME` - SMTP username +- `MAIL_PASSWORD` - SMTP password +- `MAIL_FROM` - Sender email +- `MAIL_RECIPIENTS` - Recipient emails (comma-separated) +- `REDIS_URL` - Redis connection URL +- `REDIS_KEY_PREFIX` - Redis key prefix (e.g., `klz:`) + +**Infrastructure:** +- `REGISTRY_USER` - Docker registry username +- `REGISTRY_PASS` - Docker registry password +- `ALPHA_SSH_KEY` - SSH private key for deployment server + +**Notifications:** +- `GOTIFY_URL` - Gotify notification server URL +- `GOTIFY_TOKEN` - Gotify application token + +### That's It! + +**No manual steps required.** Just push to main branch and the workflow will: +1. ✅ Build Docker image with NEXT_PUBLIC_* build args +2. ✅ Create .env file from all secrets +3. ✅ Upload .env to server +4. ✅ Secure .env file (600 permissions, deploy:deploy ownership) +5. ✅ Pull latest image +6. ✅ Deploy with docker-compose + +## Files Changed + +``` +Modified: + ├── Dockerfile (removed redundant build args) + ├── docker-compose.yml (use env_file instead of individual vars) + ├── .gitea/workflows/deploy.yml (automated .env creation & upload) + └── .env.example (clear documentation) + +Created: + ├── .env.production (reference template) + ├── docs/DEPLOYMENT.md (deployment guide) + ├── docs/SERVER_SETUP.md (server setup guide) + ├── docs/ENV_MIGRATION.md (migration guide) + └── ENV_CLEANUP_SUMMARY.md (this file) +``` + +## Deployment Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Developer pushes to main branch │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Gitea Workflow Triggered │ +│ │ +│ 1. Build Docker image (NEXT_PUBLIC_* build args) │ +│ 2. Push to registry │ +│ 3. Generate .env from secrets │ +│ 4. Upload .env to server via SCP │ +│ 5. SSH to server and deploy │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Production Server │ +│ │ +│ 1. .env file secured (600, deploy:deploy) │ +│ 2. Docker login to registry │ +│ 3. Pull latest image │ +│ 4. docker-compose down │ +│ 5. docker-compose up -d (loads .env) │ +│ 6. Health checks pass │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ ✅ Deployment Complete - Gotify Notification Sent │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Comparison: Before vs After + +| Aspect | Before | After | +|--------|--------|-------| +| **Gitea Secrets** | 15+ secrets | Same secrets, better organized | +| **Build Args** | 4 vars (including runtime-only) | 3 vars (NEXT_PUBLIC_* only) | +| **Runtime Vars** | Passed via SSH command | Auto-generated .env file | +| **Manual Steps** | ❌ Manual .env creation | ✅ Fully automated | +| **Maintenance** | Update in 3 places | Update Gitea secrets only | +| **Security** | Secrets in CI logs | Secrets masked, .env secured | +| **Clarity** | Confusing duplication | Clear separation | +| **Robustness** | Fragile SSH command | Robust automation | +| **Error-Prone** | ❌ Can forget .env | ✅ Impossible to forget | + +## Documentation + +- **[DEPLOYMENT.md](docs/DEPLOYMENT.md)** - Complete deployment guide +- **[SERVER_SETUP.md](docs/SERVER_SETUP.md)** - Server setup instructions (mostly automated now) +- **[ENV_MIGRATION.md](docs/ENV_MIGRATION.md)** - Migration from old to new system +- **[.env.example](.env.example)** - Environment variables reference +- **[.env.production](.env.production)** - Production template (for reference) + +## Troubleshooting + +### Deployment Fails + +1. **Check Gitea secrets** - Ensure all required secrets are set +2. **Check workflow logs** - Look for specific error messages +3. **SSH to server** - Verify .env file exists and has correct permissions +4. **Check container logs** - `docker-compose logs -f app` + +### .env File Issues + +The workflow automatically: +- Creates .env from secrets +- Uploads to server +- Sets 600 permissions +- Sets deploy:deploy ownership + +If there are issues, check the workflow logs for the "📝 Preparing environment configuration" step. + +### Missing Environment Variables + +If a variable is missing: +1. Add it to Gitea secrets +2. Update `.gitea/workflows/deploy.yml` to include it in the .env generation +3. Push to trigger new deployment + +--- + +**Result**: Environment variable management is now simple, clean, robust, and **fully automated**! 🎉 + +No more manual .env file creation. No more forgotten configuration. No more fragile SSH commands. Just push and deploy! diff --git a/docker-compose.yml b/docker-compose.yml index 65e03646..de4afbd5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,26 +50,14 @@ services: restart: always networks: - infra + env_file: + - .env healthcheck: test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:3000/health || wget --quiet --tries=1 --spider http://localhost:3000/ || true"] interval: 10s timeout: 5s retries: 5 start_period: 30s - environment: - - NODE_ENV=${NODE_ENV:-production} - - NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:?NEXT_PUBLIC_BASE_URL is required} - - NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID} - - NEXT_PUBLIC_UMAMI_SCRIPT_URL=${NEXT_PUBLIC_UMAMI_SCRIPT_URL} - - SENTRY_DSN=${SENTRY_DSN} - - MAIL_HOST=${MAIL_HOST} - - MAIL_PORT=${MAIL_PORT:-587} - - MAIL_USERNAME=${MAIL_USERNAME} - - MAIL_PASSWORD=${MAIL_PASSWORD} - - MAIL_FROM=${MAIL_FROM} - - MAIL_RECIPIENTS=${MAIL_RECIPIENTS} - - REDIS_URL=${REDIS_URL} - - REDIS_KEY_PREFIX=${REDIS_KEY_PREFIX} networks: infra: diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 14dcd6b4..55424b15 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,214 +1,278 @@ -# Deployment Guide +# KLZ Cables - Deployment Guide -This document describes the deployment setup for KLZ Cables website. +This document explains the deployment process and environment variable management for the KLZ Cables application. -## Automatic Deployment (Gitea Actions) +## Table of Contents -The project uses Gitea Actions for CI/CD. On every push to the `main` branch: +- [Overview](#overview) +- [Environment Variables](#environment-variables) +- [Local Development](#local-development) +- [Production Deployment](#production-deployment) +- [Troubleshooting](#troubleshooting) -1. **Build**: Docker image is built with platform `linux/arm64` -2. **Push**: Image is pushed to `registry.infra.mintel.me/mintel/klz-cables.com:latest` -3. **Deploy**: SSH connection to production server pulls and restarts containers +## Overview -### Workflow File +The application uses a clean, robust, **fully automated** environment variable strategy: -Location: `.gitea/workflows/deploy.yml` +- **Build-time variables**: Only `NEXT_PUBLIC_*` variables are baked into the client bundle +- **Runtime variables**: All other variables are loaded from `.env` file at runtime +- **Fully automated**: `.env` file is automatically generated from Gitea secrets on every deployment +- **No manual steps**: No need to manually create or update `.env` files on the server +- **No duplication**: Single source of truth for each environment -### Required Secrets +### Architecture -Configure these in your Gitea repository settings: +``` +┌─────────────────────────────────────────────────────────────┐ +│ CI/CD Workflow (.gitea/workflows/deploy.yml) │ +│ │ +│ 1. Build Docker image with NEXT_PUBLIC_* build args │ +│ 2. Push to registry │ +│ 3. SSH to server and run deploy.sh │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Production Server (alpha.mintel.me) │ +│ │ +│ /home/deploy/sites/klz-cables.com/ │ +│ ├── .env ← Runtime environment vars │ +│ ├── docker-compose.yml ← Loads .env file │ +│ └── varnish/ │ +└─────────────────────────────────────────────────────────────┘ +``` -- `REGISTRY_USER` - Docker registry username -- `REGISTRY_PASS` - Docker registry password -- `ALPHA_SSH_KEY` - SSH private key for deployment user -- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Umami analytics website ID -- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Umami analytics script URL -- `SENTRY_DSN` - Sentry/GlitchTip DSN for error tracking +## Environment Variables -## Manual Deployment +### Build-Time Variables (NEXT_PUBLIC_*) -If you need to deploy manually: +These are embedded into the JavaScript bundle during build and are visible to the client: -### On the Production Server +| Variable | Required | Description | +|----------|----------|-------------| +| `NEXT_PUBLIC_BASE_URL` | ✅ Yes | Base URL of the application (e.g., `https://klz-cables.com`) | +| `NEXT_PUBLIC_UMAMI_WEBSITE_ID` | ❌ No | Umami analytics website ID | +| `NEXT_PUBLIC_UMAMI_SCRIPT_URL` | ❌ No | Umami analytics script URL (default: `https://analytics.infra.mintel.me/script.js`) | + +**Important**: These must be provided as `--build-arg` when building the Docker image. + +### Runtime Variables + +These are loaded from the `.env` file at runtime and are only available on the server: + +| Variable | Required | Description | +|----------|----------|-------------| +| `NODE_ENV` | ✅ Yes | Environment mode (`production`, `development`, `test`) | +| `SENTRY_DSN` | ❌ No | GlitchTip/Sentry error tracking DSN | +| `MAIL_HOST` | ❌ No | SMTP server hostname | +| `MAIL_PORT` | ❌ No | SMTP server port (default: `587`) | +| `MAIL_USERNAME` | ❌ No | SMTP authentication username | +| `MAIL_PASSWORD` | ❌ No | SMTP authentication password | +| `MAIL_FROM` | ❌ No | Email sender address | +| `MAIL_RECIPIENTS` | ❌ No | Comma-separated list of recipient emails | +| `REDIS_URL` | ❌ No | Redis connection URL (e.g., `redis://redis:6379/2`) | +| `REDIS_KEY_PREFIX` | ❌ No | Redis key prefix (default: `klz:`) | +| `VARNISH_CACHE_SIZE` | ❌ No | Varnish cache size (default: `256m`) | + +## Local Development + +### Setup + +1. Copy the example environment file: + ```bash + cp .env.example .env + ``` + +2. Edit `.env` and fill in your local configuration: + ```bash + NODE_ENV=development + NEXT_PUBLIC_BASE_URL=http://localhost:3000 + # Add other variables as needed + ``` + +3. Install dependencies: + ```bash + npm install + ``` + +4. Run the development server: + ```bash + npm run dev + ``` + +### Testing Docker Build Locally ```bash -# SSH into the server -ssh deploy@alpha.mintel.me +# Build with build-time arguments +docker build \ + --build-arg NEXT_PUBLIC_BASE_URL=http://localhost:3000 \ + --build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-id \ + --build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js \ + -t klz-cables:local . + +# Run with runtime environment file +docker run --env-file .env -p 3000:3000 klz-cables:local +``` + +## Production Deployment + +### Prerequisites + +1. **Server Setup**: Ensure the production server has: + - Docker and Docker Compose installed + - Traefik reverse proxy running + - Network `infra` created + - Project directory exists: `/home/deploy/sites/klz-cables.com` + +2. **Gitea Secrets**: Configure the following secrets in your Gitea repository: + + **Registry Access:** + - `REGISTRY_USER` - Docker registry username + - `REGISTRY_PASS` - Docker registry password + + **Build-Time Variables:** + - `NEXT_PUBLIC_BASE_URL` - Production URL (e.g., `https://klz-cables.com`) + - `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Umami analytics ID + - `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Umami script URL + + **Runtime Variables:** + - `SENTRY_DSN` - Error tracking DSN + - `MAIL_HOST` - SMTP server + - `MAIL_PORT` - SMTP port + - `MAIL_USERNAME` - SMTP username + - `MAIL_PASSWORD` - SMTP password + - `MAIL_FROM` - Sender email + - `MAIL_RECIPIENTS` - Recipient emails (comma-separated) + - `REDIS_URL` - Redis connection URL + - `REDIS_KEY_PREFIX` - Redis key prefix + + **Infrastructure:** + - `REGISTRY_USER` - Docker registry username + - `REGISTRY_PASS` - Docker registry password + - `ALPHA_SSH_KEY` - SSH private key for deployment server + + **Notifications:** + - `GOTIFY_URL` - Gotify notification server URL + - `GOTIFY_TOKEN` - Gotify application token + +### Deployment Process + +The deployment is **fully automated** via Gitea Actions: + +1. **Trigger**: Push to `main` branch +2. **Build**: Docker image is built with `NEXT_PUBLIC_*` build arguments +3. **Push**: Image is pushed to private registry +4. **Generate .env**: Workflow creates `.env` file from all Gitea secrets +5. **Upload .env**: File is uploaded to server via SCP and secured (600 permissions) +6. **Deploy**: SSH to server, pull image, and run docker-compose +7. **Notify**: Send success/failure notification via Gotify + +**No manual steps required!** The `.env` file is automatically created and deployed on every push. + +### Manual Deployment + +If you need to deploy manually (not recommended - use the automated workflow): + +```bash +# SSH to the server +ssh root@alpha.mintel.me # Navigate to the project directory cd /home/deploy/sites/klz-cables.com +# Ensure .env file exists (normally created by workflow) +# If missing, you'll need to create it from Gitea secrets + # Pull the latest image -docker compose pull +docker pull registry.infra.mintel.me/mintel/klz-cables.com:latest -# Restart containers -docker compose up -d --force-recreate --remove-orphans +# Restart the services +docker-compose down +docker-compose up -d -# Clean up old images -docker image prune -f +# Check logs +docker-compose logs -f app ``` +**Note**: Manual deployment requires the `.env` file to exist. The automated workflow creates this file from Gitea secrets. If deploying manually, you'll need to create it yourself using [`.env.production`](.env.production) as a template. + ## Troubleshooting -### Workflow Not Triggering - -1. Check Gitea Actions is enabled in repository settings -2. Verify the workflow file syntax -3. Check runner availability with label `docker` - ### Build Failures -1. Check build logs in Gitea Actions tab -2. Verify all secrets are configured correctly -3. Ensure Dockerfile is valid - -### Deployment Failures - -1. Verify SSH key has correct permissions (600) -2. Check deploy user has Docker permissions -3. Verify registry credentials are correct -4. Check server disk space: `df -h` - -### Container Issues +**Problem**: Build fails with "Environment validation failed" +**Solution**: Ensure all required `NEXT_PUBLIC_*` variables are provided as build arguments: ```bash -# Check container status -docker compose ps - -# View logs -docker compose logs -f app -docker compose logs -f varnish - -# Check health -docker compose exec app wget -O- http://localhost:3000/health +docker build \ + --build-arg NEXT_PUBLIC_BASE_URL=https://klz-cables.com \ + --build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-id \ + --build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js \ + -t klz-cables . ``` -## Architecture +### Runtime Failures -``` -Client - ↓ -Traefik (TLS termination, routing) - ↓ -Varnish (HTTP caching) - ↓ -Next.js App (port 3000) -``` - -### Services - -- **app**: Next.js application -- **varnish**: HTTP cache layer -- **traefik**: Reverse proxy (external network) - -### Domains - -- `klz-cables.com` - Production -- `www.klz-cables.com` - Production (www) -- `staging.klz-cables.com` - Staging - -## Environment Variables - -### Build-time (in Dockerfile/Workflow) - -These variables are baked into the Docker image during the build process. - -- `NEXT_PUBLIC_BASE_URL` (Required) -- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` -- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` -- `SENTRY_DSN` - -### Runtime (in docker-compose.yml) - -These variables are passed to the container at runtime. - -- `NODE_ENV` (Defaults to `production`) -- `NEXT_PUBLIC_BASE_URL` (Must be passed again at runtime for server-side use) -- `SENTRY_DSN` -- `MAIL_HOST` -- `MAIL_PORT` -- `MAIL_USERNAME` -- `MAIL_PASSWORD` -- `MAIL_FROM` -- `MAIL_RECIPIENTS` -- `REDIS_URL` -- `REDIS_KEY_PREFIX` - -### Local Development - -For local development, use a `.env` file. The `docker-compose.override.yml` file automatically loads this file for the `app` service. - -## Monitoring - -### Health Checks - -- App: `https://klz-cables.com/health` -- Varnish: Configured in docker-compose.yml - -### Logs - -```bash -# Application logs -docker compose logs -f app - -# Varnish logs -docker compose logs -f varnish - -# All logs -docker compose logs -f -``` - -### Analytics - -- Umami: Configured via environment variables -- Sentry/GlitchTip: Error tracking - -## Rollback - -To rollback to a previous version: +**Problem**: Container starts but application crashes +**Solution**: Check that the `.env` file exists and contains all required runtime variables: ```bash # On the server -cd /home/deploy/sites/klz-cables.com +cat /home/deploy/sites/klz-cables.com/.env -# Pull a specific version (if tagged) -docker pull registry.infra.mintel.me/mintel/klz-cables.com:TAG - -# Or rebuild from a specific commit -# (requires access to the repository on the server) - -# Restart with the older image -docker compose up -d --force-recreate +# Check container logs +docker-compose logs app ``` -## Performance +### Missing Environment Variables -### Cache Invalidation +**Problem**: Features not working (email, analytics, etc.) -Varnish caches static assets. To clear cache: +**Solution**: +1. Check that the secret is configured in Gitea +2. Verify the workflow includes it in the `.env` generation (see `.gitea/workflows/deploy.yml`) +3. Redeploy to regenerate the `.env` file: + ```bash + git commit --allow-empty -m "Trigger redeploy" + git push origin main + ``` +4. Check the variable is loaded on the server: + ```bash + ssh root@alpha.mintel.me + cd /home/deploy/sites/klz-cables.com + docker-compose exec app env | grep VARIABLE_NAME + ``` + +### Docker Compose Issues + +**Problem**: `docker-compose up` fails with "env file not found" + +**Solution**: The `.env` file should be automatically created by the workflow. If it's missing: +1. Check the workflow logs for errors in the "📝 Preparing environment configuration" step +2. Manually trigger a deployment by pushing to main +3. If still missing, check server permissions and disk space + +### Network Issues + +**Problem**: Container can't connect to Traefik + +**Solution**: Verify the `infra` network exists: ```bash -docker compose exec varnish varnishadm "ban req.url ~ ." +docker network ls | grep infra +docker network inspect infra ``` -### Cache Configuration +## Security Best Practices -Edit `varnish/default.vcl` and restart: +1. **Never commit `.env` files** with real credentials to git +2. **Use Gitea/GitHub secrets** for CI/CD workflows +3. **Restrict SSH key access** to deployment server +4. **Rotate credentials regularly** (SMTP passwords, API keys, etc.) +5. **Use HTTPS** for all production URLs +6. **Monitor logs** for suspicious activity -```bash -docker compose restart varnish -``` +## Additional Resources -## Security - -- All secrets are stored in Gitea repository settings -- SSH key is injected at deployment time -- Registry credentials are not stored in the repository -- Deploy webhook requires secret token - -## Support - -For issues or questions: -1. Check logs first -2. Review this documentation -3. Contact the development team +- [Next.js Environment Variables](https://nextjs.org/docs/basic-features/environment-variables) +- [Docker Compose Environment Variables](https://docs.docker.com/compose/environment-variables/) +- [Traefik Documentation](https://doc.traefik.io/traefik/) diff --git a/docs/ENV_MIGRATION.md b/docs/ENV_MIGRATION.md new file mode 100644 index 00000000..3796a613 --- /dev/null +++ b/docs/ENV_MIGRATION.md @@ -0,0 +1,276 @@ +# Environment Variables Migration Guide + +This guide helps you migrate from the old fragile environment variable setup to the new clean, robust system. + +## What Changed? + +### Before (Fragile & Overkill) + +❌ **Problems:** +- Environment variables passed individually via SSH (12+ vars) +- Duplicate definitions in Dockerfile, docker-compose.yml, and deploy.yml +- Build args included runtime-only variables (SENTRY_DSN, MAIL_*, REDIS_*) +- No single source of truth +- Difficult to maintain and error-prone + +```yaml +# Old deploy.yml - FRAGILE! +ssh root@alpha.mintel.me \ + "MAIL_FROM='${{ secrets.MAIL_FROM }}' \ + MAIL_HOST='${{ secrets.MAIL_HOST }}' \ + MAIL_PASSWORD='${{ secrets.MAIL_PASSWORD }}' \ + MAIL_PORT='${{ secrets.MAIL_PORT }}' \ + MAIL_RECIPIENTS='${{ secrets.MAIL_RECIPIENTS }}' \ + MAIL_USERNAME='${{ secrets.MAIL_USERNAME }}' \ + NEXT_PUBLIC_BASE_URL='${{ secrets.NEXT_PUBLIC_BASE_URL }}' \ + ... (12+ variables) \ + /home/deploy/deploy.sh" +``` + +### After (Clean & Robust) + +✅ **Benefits:** +- Single `.env` file on server contains all runtime variables +- Only `NEXT_PUBLIC_*` variables passed as build args (3 vars) +- Clear separation: build-time vs runtime +- Easy to maintain and update +- Single source of truth per environment + +```yaml +# New deploy.yml - CLEAN! +ssh root@alpha.mintel.me "/home/deploy/deploy.sh" +``` + +## Migration Steps + +### Step 1: Update Gitea Secrets + +**Remove these secrets** (no longer needed in CI/CD): +- ❌ `MAIL_FROM` +- ❌ `MAIL_HOST` +- ❌ `MAIL_PASSWORD` +- ❌ `MAIL_PORT` +- ❌ `MAIL_RECIPIENTS` +- ❌ `MAIL_USERNAME` +- ❌ `NODE_ENV` +- ❌ `REDIS_URL` +- ❌ `REDIS_KEY_PREFIX` +- ❌ `SENTRY_DSN` (from build args) + +**Keep these secrets** (still needed for build): +- ✅ `NEXT_PUBLIC_BASE_URL` +- ✅ `NEXT_PUBLIC_UMAMI_WEBSITE_ID` +- ✅ `NEXT_PUBLIC_UMAMI_SCRIPT_URL` +- ✅ `REGISTRY_USER` +- ✅ `REGISTRY_PASS` +- ✅ `ALPHA_SSH_KEY` +- ✅ `GOTIFY_URL` +- ✅ `GOTIFY_TOKEN` + +### Step 2: Create .env File on Server + +SSH to the production server and create the `.env` file: + +```bash +ssh root@alpha.mintel.me + +# Create .env file +cat > /home/deploy/sites/klz-cables.com/.env << 'EOF' +# Application +NODE_ENV=production +NEXT_PUBLIC_BASE_URL=https://klz-cables.com + +# Analytics +NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-actual-id +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js + +# Error Tracking +SENTRY_DSN=your-actual-dsn + +# Email +MAIL_HOST=smtp.eu.mailgun.org +MAIL_PORT=587 +MAIL_USERNAME=your-actual-username +MAIL_PASSWORD=your-actual-password +MAIL_FROM=KLZ Cables +MAIL_RECIPIENTS=info@klz-cables.com + +# Redis +REDIS_URL=redis://redis:6379/2 +REDIS_KEY_PREFIX=klz: + +# Varnish +VARNISH_CACHE_SIZE=256m +EOF + +# Secure the file +chmod 600 /home/deploy/sites/klz-cables.com/.env +chown deploy:deploy /home/deploy/sites/klz-cables.com/.env +``` + +**Important**: Replace all `your-actual-*` placeholders with real values from your old Gitea secrets. + +### Step 3: Update Deployment Script + +Update `/home/deploy/deploy.sh` to use the new approach: + +```bash +cat > /home/deploy/deploy.sh << 'EOF' +#!/bin/bash +set -e + +PROJECT_DIR="/home/deploy/sites/klz-cables.com" + +echo "╔══════════════════════════════════════════════════════════════════════════════╗" +echo "║ KLZ Cables - Deployment Script ║" +echo "╚══════════════════════════════════════════════════════════════════════════════╝" +echo "" + +cd "$PROJECT_DIR" + +# Check if .env file exists +if [ ! -f .env ]; then + echo "❌ ERROR: .env file not found at $PROJECT_DIR/.env" + exit 1 +fi + +echo "🔐 Logging into Docker registry..." +echo "$REGISTRY_PASS" | docker login registry.infra.mintel.me -u "$REGISTRY_USER" --password-stdin + +echo "🔄 Pulling latest image..." +docker pull registry.infra.mintel.me/mintel/klz-cables.com:latest + +echo "🔄 Stopping existing containers..." +docker-compose down + +echo "🚀 Starting new containers..." +docker-compose up -d + +echo "⏳ Waiting for services to be healthy..." +sleep 10 + +echo "🔍 Checking service status..." +docker-compose ps + +echo "" +echo "✅ Deployment complete!" +EOF + +chmod +x /home/deploy/deploy.sh +``` + +### Step 4: Deploy Updated Code + +The new code is already in the repository. Just push to trigger deployment: + +```bash +git push origin main +``` + +The CI/CD workflow will: +1. Build with only `NEXT_PUBLIC_*` build args +2. Push to registry +3. SSH to server and run deploy.sh +4. Deploy.sh will use the `.env` file for runtime vars + +### Step 5: Verify Migration + +After deployment, verify everything works: + +```bash +# SSH to server +ssh root@alpha.mintel.me + +# Check containers are running +cd /home/deploy/sites/klz-cables.com +docker-compose ps + +# Verify environment variables are loaded +docker-compose exec app env | grep -E "NODE_ENV|NEXT_PUBLIC|MAIL|REDIS" + +# Check application logs +docker-compose logs -f app + +# Test the website +curl -I https://klz-cables.com +``` + +## Comparison Table + +| Aspect | Before | After | +|--------|--------|-------| +| **Gitea Secrets** | 15+ secrets | 8 secrets | +| **Build Args** | 4 vars (including runtime-only) | 3 vars (NEXT_PUBLIC_* only) | +| **Runtime Vars** | Passed via SSH command | Loaded from .env file | +| **Maintenance** | Update in 3 places | Update in 1 place | +| **Security** | Secrets in CI logs | Secrets only on server | +| **Clarity** | Confusing duplication | Clear separation | +| **Robustness** | Fragile SSH command | Robust file-based config | + +## Rollback Plan + +If you need to rollback to the old system: + +1. Revert the changes in git: + ```bash + git revert HEAD + git push origin main + ``` + +2. Re-add the removed Gitea secrets + +3. The old deployment will work again + +## FAQ + +**Q: Why keep `NEXT_PUBLIC_*` in both build args and .env file?** + +A: `NEXT_PUBLIC_*` variables are special in Next.js - they're embedded into the JavaScript bundle at build time. They must be provided as build args. However, they're also needed at runtime for server-side rendering, so they're in the .env file too. + +**Q: Can I update environment variables without rebuilding?** + +A: Yes, for runtime-only variables (MAIL_*, REDIS_*, SENTRY_DSN, etc.). Just edit the `.env` file on the server and restart containers: +```bash +nano /home/deploy/sites/klz-cables.com/.env +docker-compose down && docker-compose up -d +``` + +For `NEXT_PUBLIC_*` variables, you need to rebuild the Docker image since they're baked into the client bundle. + +**Q: Where should I store the .env file backup?** + +A: Keep a secure backup outside the server: +```bash +# Download from server +scp root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env \ + ~/secure-backups/klz-cables.env.backup + +# Store in password manager or encrypted storage +``` + +**Q: What if I accidentally commit .env to git?** + +A: +1. Remove it immediately: `git rm .env && git commit -m "Remove .env"` +2. Rotate all credentials in the file +3. Update the `.gitignore` to ensure it doesn't happen again (already done) + +## Support + +If you encounter issues during migration: + +1. Check the logs: `docker-compose logs -f app` +2. Verify .env file exists and has correct permissions +3. Ensure all required variables are set +4. Review [DEPLOYMENT.md](./DEPLOYMENT.md) for detailed troubleshooting + +## Summary + +The new system is: +- ✅ **Simpler**: One .env file instead of scattered variables +- ✅ **Cleaner**: Clear separation of build vs runtime +- ✅ **Robust**: File-based config instead of fragile SSH commands +- ✅ **Secure**: Secrets stay on server, not in CI logs +- ✅ **Maintainable**: Single source of truth per environment + +You've successfully migrated from a fragile, overkill setup to a clean, production-ready configuration! 🎉 diff --git a/docs/SERVER_SETUP.md b/docs/SERVER_SETUP.md new file mode 100644 index 00000000..3b12afce --- /dev/null +++ b/docs/SERVER_SETUP.md @@ -0,0 +1,349 @@ +# Server Setup Guide - KLZ Cables + +This guide explains how to set up the production environment on the deployment server. + +## Prerequisites + +- Server: `alpha.mintel.me` +- User: `deploy` (with sudo access) +- Docker and Docker Compose installed +- Traefik reverse proxy running +- External network `infra` created + +## Initial Server Setup + +### 1. Create Project Directory + +```bash +# SSH to the server as root or deploy user +ssh root@alpha.mintel.me + +# Create project directory +mkdir -p /home/deploy/sites/klz-cables.com +cd /home/deploy/sites/klz-cables.com +``` + +### 2. Create Environment File + +Create the `.env` file with production configuration: + +```bash +# Create .env file from template +cat > /home/deploy/sites/klz-cables.com/.env << 'EOF' +# ============================================================================ +# KLZ Cables - Production Environment Configuration +# ============================================================================ + +# Application +NODE_ENV=production +NEXT_PUBLIC_BASE_URL=https://klz-cables.com + +# Analytics (Umami) +NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-umami-website-id +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js + +# Error Tracking (GlitchTip/Sentry) +SENTRY_DSN=https://your-sentry-dsn@errors.infra.mintel.me/project-id + +# Email Configuration (Mailgun) +MAIL_HOST=smtp.eu.mailgun.org +MAIL_PORT=587 +MAIL_USERNAME=your-mailgun-username +MAIL_PASSWORD=your-mailgun-password +MAIL_FROM=KLZ Cables +MAIL_RECIPIENTS=info@klz-cables.com + +# Redis Cache +REDIS_URL=redis://redis:6379/2 +REDIS_KEY_PREFIX=klz: + +# Varnish Cache Size +VARNISH_CACHE_SIZE=256m +EOF +``` + +**Important**: Replace all placeholder values with actual production credentials. + +### 3. Secure the Environment File + +```bash +# Set proper permissions (readable only by owner) +chmod 600 /home/deploy/sites/klz-cables.com/.env + +# Verify ownership +chown deploy:deploy /home/deploy/sites/klz-cables.com/.env + +# Verify permissions +ls -la /home/deploy/sites/klz-cables.com/.env +# Should show: -rw------- 1 deploy deploy +``` + +### 4. Create Docker Compose File + +```bash +# Copy docker-compose.yml from repository +# This should be done automatically by the deployment script +# Or manually: +cat > /home/deploy/sites/klz-cables.com/docker-compose.yml << 'EOF' +services: + varnish: + image: varnish:7 + restart: always + networks: + - infra + depends_on: + - app + command: >- + varnishd + -F + -f /etc/varnish/default.vcl + -s malloc,${VARNISH_CACHE_SIZE:-256m} + volumes: + - ./varnish/default.vcl:/etc/varnish/default.vcl:ro + healthcheck: + test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:80/health || wget --quiet --tries=1 --spider http://localhost:80/ || true"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + labels: + - "traefik.enable=true" + - "traefik.http.routers.klz-cables-web.rule=(Host(\`klz-cables.com\`) || Host(\`www.klz-cables.com\`) || Host(\`staging.klz-cables.com\`)) && !PathPrefix(\`/.well-known/acme-challenge/\`)" + - "traefik.http.routers.klz-cables-web.entrypoints=web" + - "traefik.http.routers.klz-cables-web.middlewares=redirect-https" + - "traefik.http.routers.klz-cables.rule=Host(\`klz-cables.com\`) || Host(\`www.klz-cables.com\`) || Host(\`staging.klz-cables.com\`)" + - "traefik.http.routers.klz-cables.entrypoints=websecure" + - "traefik.http.routers.klz-cables.tls.certresolver=le" + - "traefik.http.routers.klz-cables.tls=true" + - "traefik.http.routers.klz-cables.service=klz-cables" + - "traefik.http.services.klz-cables.loadbalancer.server.port=80" + - "traefik.http.services.klz-cables.loadbalancer.server.scheme=http" + - "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Ssl=on" + - "traefik.http.routers.klz-cables.middlewares=klz-forward,compress" + + app: + image: registry.infra.mintel.me/mintel/klz-cables.com:latest + restart: always + networks: + - infra + env_file: + - .env + healthcheck: + test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:3000/health || wget --quiet --tries=1 --spider http://localhost:3000/ || true"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + +networks: + infra: + external: true +EOF +``` + +### 5. Create Varnish Configuration + +```bash +# Create varnish directory +mkdir -p /home/deploy/sites/klz-cables.com/varnish + +# Copy varnish configuration from repository +# This should be in the repository at varnish/default.vcl +``` + +### 6. Create Deployment Script + +```bash +cat > /home/deploy/deploy.sh << 'EOF' +#!/bin/bash +set -e + +PROJECT_DIR="/home/deploy/sites/klz-cables.com" + +echo "╔══════════════════════════════════════════════════════════════════════════════╗" +echo "║ KLZ Cables - Deployment Script ║" +echo "╚══════════════════════════════════════════════════════════════════════════════╝" +echo "" + +cd "$PROJECT_DIR" + +# Check if .env file exists +if [ ! -f .env ]; then + echo "❌ ERROR: .env file not found at $PROJECT_DIR/.env" + echo "Please create the .env file before deploying." + exit 1 +fi + +echo "🔐 Logging into Docker registry..." +echo "$REGISTRY_PASS" | docker login registry.infra.mintel.me -u "$REGISTRY_USER" --password-stdin + +echo "🔄 Pulling latest image..." +docker pull registry.infra.mintel.me/mintel/klz-cables.com:latest + +echo "🔄 Stopping existing containers..." +docker-compose down + +echo "🚀 Starting new containers..." +docker-compose up -d + +echo "⏳ Waiting for services to be healthy..." +sleep 10 + +echo "🔍 Checking service status..." +docker-compose ps + +echo "" +echo "✅ Deployment complete!" +echo "" +echo "📊 Service URLs:" +echo " • Production: https://klz-cables.com" +echo " • Staging: https://staging.klz-cables.com" +echo "" +EOF + +# Make script executable +chmod +x /home/deploy/deploy.sh +``` + +### 7. Configure Docker Registry Access + +The deployment script needs registry credentials. These are passed as environment variables from the CI/CD workflow: + +```bash +# The workflow passes these variables: +# REGISTRY_USER - from Gitea secrets +# REGISTRY_PASS - from Gitea secrets +``` + +## Verification + +### Check Environment File + +```bash +# Verify .env file exists and has correct permissions +ls -la /home/deploy/sites/klz-cables.com/.env + +# Check content (be careful not to expose secrets in logs) +head -n 5 /home/deploy/sites/klz-cables.com/.env +``` + +### Test Deployment Script + +```bash +# Run deployment script manually (requires registry credentials) +REGISTRY_USER=your-user REGISTRY_PASS=your-pass /home/deploy/deploy.sh +``` + +### Check Running Containers + +```bash +cd /home/deploy/sites/klz-cables.com +docker-compose ps +docker-compose logs -f app +``` + +### Verify Environment Variables in Container + +```bash +# Check that environment variables are loaded +docker-compose exec app env | grep -E "NODE_ENV|NEXT_PUBLIC|MAIL|REDIS" +``` + +## Updating Environment Variables + +When you need to update environment variables: + +```bash +# 1. SSH to the server +ssh root@alpha.mintel.me + +# 2. Edit the .env file +nano /home/deploy/sites/klz-cables.com/.env + +# 3. Restart the containers to pick up changes +cd /home/deploy/sites/klz-cables.com +docker-compose down +docker-compose up -d + +# 4. Verify the changes +docker-compose logs -f app +``` + +**Note**: Changes to `NEXT_PUBLIC_*` variables require rebuilding the Docker image, as they are baked into the client bundle at build time. + +## Troubleshooting + +### .env File Not Found + +```bash +# Check if file exists +ls -la /home/deploy/sites/klz-cables.com/.env + +# If missing, create it from template +cp .env.production /home/deploy/sites/klz-cables.com/.env +# Then edit with actual values +``` + +### Permission Denied + +```bash +# Fix ownership +chown -R deploy:deploy /home/deploy/sites/klz-cables.com + +# Fix .env permissions +chmod 600 /home/deploy/sites/klz-cables.com/.env +``` + +### Container Won't Start + +```bash +# Check logs +docker-compose logs app + +# Check if .env is loaded +docker-compose config + +# Verify environment variables +docker-compose exec app env +``` + +### Network Issues + +```bash +# Verify infra network exists +docker network ls | grep infra + +# If missing, create it +docker network create infra +``` + +## Security Checklist + +- [ ] `.env` file has `600` permissions (readable only by owner) +- [ ] `.env` file is owned by `deploy:deploy` +- [ ] `.env` file is NOT in git repository +- [ ] All sensitive credentials are filled in +- [ ] SSH keys are properly secured +- [ ] Firewall rules are configured +- [ ] HTTPS is enforced via Traefik +- [ ] Regular backups of `.env` file are maintained + +## Backup + +Create a secure backup of the environment file: + +```bash +# Backup .env file +cp /home/deploy/sites/klz-cables.com/.env \ + /home/deploy/backups/klz-cables.env.$(date +%Y%m%d) + +# Set proper permissions on backup +chmod 600 /home/deploy/backups/klz-cables.env.* +``` + +## Additional Resources + +- [Deployment Guide](./DEPLOYMENT.md) +- [Docker Compose Documentation](https://docs.docker.com/compose/) +- [Traefik Documentation](https://doc.traefik.io/traefik/)