diff --git a/.env.example b/.env.example index 67db7d5..e4358ef 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,13 @@ -# .env.example -# Copy this file to .env and update the values below - ## Domain / Timezone DOMAIN=your-domain.com -TZ=Your/Timezone +DOMAIN_PREFIX=gate + +TZ=Africa/Cairo ## ACME (Let's Encrypt) # Your email address for Let's Encrypt certificate notifications ACME_EMAIL=admin@your-domain.com -## Basic Authentication -# Generate password hash with: echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g -# Or use online generator: https://hostingcanada.org/htpasswd-generator/ -# Format: username:$$2y$$10$$hashed_password -BASIC_AUTH_USERS=admin:$$2y$$05$$HIIpotyLuLRMBX3sfUs6E.YM3lP9cpF7hK7sHaSXGs6mw/RS6MXCa - ## Umami (PostgreSQL) # Database user for Umami analytics UMAMI_DB_USER=umami @@ -24,8 +17,6 @@ UMAMI_DB_PASS=your_strong_database_password UMAMI_DB_NAME=umami # Random 64-character secret for Umami app security UMAMI_APP_SECRET=your_64_character_random_secret_here -# Website ID from Umami dashboard (use placeholder initially, update after setup) -UMAMI_WEBSITE_ID=placeholder-website-id ## pgAdmin # Default email for pgAdmin login @@ -33,8 +24,26 @@ PGADMIN_DEFAULT_EMAIL=admin@your-domain.com # Strong password for pgAdmin login PGADMIN_DEFAULT_PASSWORD=your_strong_pgadmin_password -## Grafana -# Admin username for Grafana dashboard -GRAFANA_ADMIN_USER=admin -# Strong password for Grafana admin user -GRAFANA_ADMIN_PASS=your_strong_grafana_password +## Authelia +# Database password for Authelia PostgreSQL +AUTHELIA_DB_PASSWORD=your_authelia_db_password +# JWT secret for Authelia (64+ character random string) +AUTHELIA_JWT_SECRET=your_64_character_jwt_secret_here +# Session secret for Authelia (64+ character random string) +AUTHELIA_SESSION_SECRET=your_64_character_session_secret_here +# Storage encryption key for Authelia (20+ character string) +AUTHELIA_STORAGE_ENCRYPTION_KEY=your_storage_encryption_key_here +# SMTP password for Authelia notifier +AUTHELIA_NOTIFIER_SMTP_PASSWORD=your_smtp_password_here + +## Gitea +# Database user for Gitea +GITEA_DB_USER=gitea +# Database password for Gitea PostgreSQL +GITEA_DB_PASSWORD=your_gitea_db_password_here +# Database name for Gitea +GITEA_DB_NAME=gitea +# Secret key for Gitea (64+ character random string - generate with: openssl rand -hex 32) +GITEA_SECRET_KEY=your_64_character_gitea_secret_key_here +# Internal token for Gitea (generate with: docker run -it --rm docker.gitea.com/gitea:1 gitea generate secret INTERNAL_TOKEN) +GITEA_INTERNAL_TOKEN=your_gitea_internal_token_here diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 661625a..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,60 +0,0 @@ -# .github/workflows/deploy.yml -name: Deploy Gateway to VPS - -on: - push: - branches: [ "disbaled" ] - paths: - - "docker-compose.yml" - - ".github/workflows/deploy.yml" - workflow_dispatch: - -env: - REMOTE_DIR: ${{ secrets.REMOTE_DIR }} - -concurrency: - group: deploy-prod - cancel-in-progress: true - -jobs: - deploy: - name: Ship to VPS - runs-on: ubuntu-latest - environment: production - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts - - - name: Login to GitHub Container Registry on VPS - run: | - ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \ - "echo '${{ secrets.GITHUB_TOKEN }}' | docker login ghcr.io -u '${{ github.actor }}' --password-stdin" - - - name: Deploy (pull, up, prune) - run: | - ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \ - "export REMOTE_DIR='${{ secrets.REMOTE_DIR }}'; bash -se" <<'EOF' - set -euo pipefail - cd "$REMOTE_DIR" - - git pull origin main - docker compose pull - docker compose up -d --remove-orphans - docker image prune -af || true - EOF - - - name: Post-deploy smoke checks - run: | - echo "Deployed to ${{ secrets.SSH_HOST }}:${{ secrets.SSH_PORT }} → ${REMOTE_DIR}" - echo "Traefik: https://traefik.gate.${{ secrets.DOMAIN }}" - echo "Portainer: https://portainer.gate.${{ secrets.DOMAIN }}" - echo "Uptime Kuma: https://uptime.gate.${{ secrets.DOMAIN }}" - echo "Umami: https://umami.gate.${{ secrets.DOMAIN }}" - echo "pgAdmin: https://pgadmin.gate.${{ secrets.DOMAIN }}" diff --git a/README.md b/README.md index a6ff217..1f5ad43 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,35 @@ # Launchpad Gateway -A production-ready Traefik-based reverse proxy gateway with automatic SSL/TLS, analytics, monitoring, and container management. +A production-ready Traefik-based reverse proxy gateway with automatic SSL/TLS, analytics, monitoring, authentication, and container management. ## 🚀 Features - **Automatic SSL/TLS** certificates via Let's Encrypt - **Reverse Proxy** with Traefik v3.1 +- **Authentication** with Authelia (2FA, password reset) - **Web Analytics** with Umami and PostgreSQL - **Container Management** via Portainer - **Uptime Monitoring** with Uptime Kuma -- **Security Headers** and Basic Authentication +- **Server Monitoring** with Beszel +- **Git Repository Hosting** with Gitea +- **Security Headers** and Flexible Routing - **Flexible Domain Routing** (subdomains, paths, custom rules) ## 🏗️ Architecture ``` -Internet → Traefik (Port 80/443) → Internal Services (traefik_proxy network) +Internet → Traefik (Port 80/443) → Authelia (Auth) → Internal Services (traefik_proxy network) ``` ### Current Services -- **Traefik Dashboard**: `traefik.gate.${DOMAIN}` - Reverse proxy management -- **Portainer**: `portainer.gate.${DOMAIN}` - Docker container management -- **Uptime Kuma**: `uptime.gate.${DOMAIN}` - Service monitoring -- **Umami Analytics**: `umami.gate.${DOMAIN}` - Web analytics dashboard +- **Traefik Dashboard**: `traefik.${DOMAIN_PREFIX}.${DOMAIN}` - Reverse proxy management +- **Authelia**: `auth.${DOMAIN_PREFIX}.${DOMAIN}` - Authentication portal +- **Portainer**: `portainer.${DOMAIN_PREFIX}.${DOMAIN}` - Docker container management +- **Uptime Kuma**: `uptime.${DOMAIN_PREFIX}.${DOMAIN}` - Service monitoring +- **Umami Analytics**: `umami.${DOMAIN_PREFIX}.${DOMAIN}` - Web analytics dashboard +- **pgAdmin**: `pgadmin.${DOMAIN_PREFIX}.${DOMAIN}` - PostgreSQL administration +- **Beszel**: `beszel.${DOMAIN_PREFIX}.${DOMAIN}` - Server monitoring +- **Gitea**: `git.${DOMAIN_PREFIX}.${DOMAIN}` - Self-hosted Git service (SSH on port 222) ## 🛠️ Quick Start @@ -49,19 +56,31 @@ Update `.env` with your settings: ```bash # Domain and timezone DOMAIN=your-domain.com -TZ=Your/Timezone +DOMAIN_PREFIX=test +TZ=Africa/Cairo # Let's Encrypt email ACME_EMAIL=admin@your-domain.com -# Basic auth (generate with: htpasswd -nB admin) -BASIC_AUTH_USERS=admin:$$2y$$05$$your_hashed_password - # Database credentials UMAMI_DB_USER=umami UMAMI_DB_PASS=your_secure_password UMAMI_DB_NAME=umami UMAMI_APP_SECRET=your_64_character_secret + +# Authelia secrets (generate with openssl rand -hex 32) +AUTHELIA_DB_PASSWORD=your_secure_password +AUTHELIA_JWT_SECRET=your_64_char_secret +AUTHELIA_SESSION_SECRET=your_64_char_secret +AUTHELIA_STORAGE_ENCRYPTION_KEY=your_64_char_secret +AUTHELIA_NOTIFIER_SMTP_PASSWORD=your_smtp_password + +# Gitea secrets +GITEA_DB_USER=gitea +GITEA_DB_PASSWORD=your_secure_password +GITEA_DB_NAME=gitea +GITEA_SECRET_KEY=your_64_char_secret +GITEA_INTERNAL_TOKEN=your_internal_token ``` @@ -71,10 +90,14 @@ docker compose up -d ``` ### 4. Access Services -- **Traefik Dashboard**: `https://traefik.gate.your-domain.com` -- **Portainer**: `https://portainer.gate.your-domain.com` -- **Uptime Kuma**: `https://uptime.gate.your-domain.com` -- **Umami Analytics**: `https://umami.gate.your-domain.com` +- **Traefik Dashboard**: `https://traefik.${DOMAIN_PREFIX}.${DOMAIN}` +- **Authelia**: `https://auth.${DOMAIN_PREFIX}.${DOMAIN}` +- **Portainer**: `https://portainer.${DOMAIN_PREFIX}.${DOMAIN}` +- **Uptime Kuma**: `https://uptime.${DOMAIN_PREFIX}.${DOMAIN}` +- **Umami Analytics**: `https://umami.${DOMAIN_PREFIX}.${DOMAIN}` +- **pgAdmin**: `https://pgadmin.${DOMAIN_PREFIX}.${DOMAIN}` +- **Beszel**: `https://beszel.${DOMAIN_PREFIX}.${DOMAIN}` +- **Gitea**: `https://git.${DOMAIN_PREFIX}.${DOMAIN}` (SSH: port 222) ## 📋 Adding New Services @@ -231,16 +254,15 @@ services: ### Available Middlewares - **`security-headers`**: HSTS, XSS protection, content type sniffing prevention -- **`basic-auth`**: HTTP Basic Authentication for admin interfaces -- **`umami-analytics`**: Automatic web analytics tracking +- **`authelia`**: Authentication and authorization with 2FA support ### Usage Examples ```yaml # Public application with analytics -- traefik.http.routers.app.middlewares=umami-analytics,security-headers +- traefik.http.routers.app.middlewares=security-headers # Admin interface with authentication -- traefik.http.routers.admin.middlewares=basic-auth,security-headers +- traefik.http.routers.admin.middlewares=authelia@docker,security-headers # API endpoint (security headers only) - traefik.http.routers.api.middlewares=security-headers @@ -274,6 +296,7 @@ services: ### Built-in Monitoring - **Uptime Kuma**: Service availability monitoring +- **Beszel**: Server resource monitoring (CPU, RAM, disk, network) - **Traefik Dashboard**: Traffic and routing metrics - **Umami Analytics**: Web traffic analytics (privacy-focused) diff --git a/alertmanager.yml b/alertmanager.yml deleted file mode 100644 index 5684b39..0000000 --- a/alertmanager.yml +++ /dev/null @@ -1,17 +0,0 @@ -global: - smtp_smarthost: 'smtp.example.com:587' - smtp_from: 'alertmanager@example.com' - smtp_auth_username: 'alertmanager' - smtp_auth_password: 'dummy_password' - -route: - group_by: ['alertname'] - group_wait: 10s - group_interval: 10s - repeat_interval: 1h - receiver: 'email' - -receivers: - - name: 'email' - email_configs: - - to: 'admin@example.com' \ No newline at end of file diff --git a/authelia/configuration.template.yml b/authelia/configuration.template.yml new file mode 100644 index 0000000..7431e16 --- /dev/null +++ b/authelia/configuration.template.yml @@ -0,0 +1,60 @@ +--- +# Authelia configuration +# This is a minimal configuration for getting started with Authelia + +server: + address: 'tcp://:9091' + endpoints: + authz: + forward-auth: + implementation: 'ForwardAuth' + +authentication_backend: + file: + path: '/config/users_database.yml' + +access_control: + default_policy: 'one_factor' + rules: + - domain: 'pgadmin.${DOMAIN_PREFIX}.${DOMAIN}' + policy: 'two_factor' + - domain: 'beszel.${DOMAIN_PREFIX}.${DOMAIN}' + policy: 'two_factor' + - domain: 'traefik.${DOMAIN_PREFIX}.${DOMAIN}' + policy: 'two_factor' + - domain: '*.${DOMAIN_PREFIX}.${DOMAIN}' + policy: 'one_factor' + +session: + name: 'authelia_session' + cookies: + - domain: '${DOMAIN}' + authelia_url: 'https://auth.${DOMAIN_PREFIX}.${DOMAIN}' + default_redirection_url: 'https://portainer.${DOMAIN_PREFIX}.${DOMAIN}' + +storage: + postgres: + address: 'tcp://authelia-db:5432' + database: 'authelia' + username: 'authelia' + +notifier: + disable_startup_check: true + # Configure SMTP for production email notifications + # For testing, you can use filesystem notifier instead: + # filesystem: + # filename: /config/notification.txt + smtp: + address: 'submissions://smtp.gmail.com:465' + username: 'your-email@gmail.com' + sender: 'Authelia ' + # For Gmail, use an App Password (not your regular password) + # Generate at: https://myaccount.google.com/apppasswords + +identity_validation: + reset_password: {} + +regulation: + max_retries: 3 + find_time: 120 + ban_time: 300 diff --git a/authelia/configuration.yml b/authelia/configuration.yml index 3c0a237..24e0f6b 100644 --- a/authelia/configuration.yml +++ b/authelia/configuration.yml @@ -3,7 +3,7 @@ # This is a minimal configuration for getting started with Authelia server: - port: 9091 + address: 'tcp://:9091' endpoints: authz: forward-auth: @@ -14,45 +14,37 @@ authentication_backend: path: '/config/users_database.yml' access_control: - default_policy: 'deny' + default_policy: 'one_factor' rules: - - domain: 'pgadmin.gate.${DOMAIN}' + - domain: 'pgadmin.test.3launchpad.com' policy: 'two_factor' - - domain: '*.gate.${DOMAIN}' + - domain: '*.test.3launchpad.com' policy: 'one_factor' session: name: 'authelia_session' - secret: '${AUTHELIA_SESSION_SECRET}' cookies: - - domain: 'gate.${DOMAIN}' - authelia_url: 'https://auth.gate.${DOMAIN}' - default_redirection_url: 'https://login.gate.${DOMAIN}' + - domain: '3launchpad.com' + authelia_url: 'https://auth.test.3launchpad.com' + default_redirection_url: 'https://portainer.test.3launchpad.com' storage: postgres: - host: 'authelia-db' - port: 5432 + address: 'tcp://authelia-db:5432' database: 'authelia' username: 'authelia' - password: '${AUTHELIA_DB_PASSWORD}' notifier: + disable_startup_check: true smtp: + address: 'smtp://localhost:25' username: 'authelia' - password: 'dummy_password' - host: 'smtp.example.com' - port: 587 sender: 'authelia@example.com' -jwt_secret: '${AUTHELIA_JWT_SECRET}' +identity_validation: + reset_password: {} -api: - endpoints: - reset_password: - disable: false - -regulations: +regulation: max_retries: 3 find_time: 120 - ban_time: 300 \ No newline at end of file + ban_time: 300 diff --git a/authelia/users_database.yml b/authelia/users_database.yml index d0bf72f..b66e7d5 100644 --- a/authelia/users_database.yml +++ b/authelia/users_database.yml @@ -1,10 +1,17 @@ --- # Authelia users database -# This is a simple file-based user database for testing +# This is a simple file-based user database +# +# To generate a new password hash, run: +# docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'YOUR_PASSWORD' +# +# IMPORTANT: Change the default password before production use! users: admin: displayname: "Administrator" - password: "$argon2id$v=19$m=65536,t=3,p=4$abcdefghijklmnopqrstuvwx$abcdefghijklmnopqrstuvwxabcdefghijklmnopqrstuvwx" # Dummy hash for 'password' - email: admin@example.com - groups: [] \ No newline at end of file + # Default password: "Admin@123456" - CHANGE THIS! + password: "$argon2id$v=19$m=65536,t=3,p=4$O1Qjq7AB4/xJ7Qk1dUqp/g$PhVqFWEqyQTJeSnCeiCC3lrcWcpw37kYttw4Xh/qUsk" + email: admin@3launchpad.com + groups: + - admins \ No newline at end of file diff --git a/beszel_agent_data/fingerprint b/beszel_agent_data/fingerprint new file mode 100644 index 0000000..7d9c86f --- /dev/null +++ b/beszel_agent_data/fingerprint @@ -0,0 +1 @@ +c17e1b1086d55cc7d4bfacbfa6b9472ca975863413fb629f \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index bdf20bb..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,337 +0,0 @@ -######################## -# Networks & Volumes -######################## -networks: - traefik_proxy: - name: traefik_proxy - -volumes: - traefik_letsencrypt: - traefik_logs: - portainer_data: - uptime_kuma_data: - umami_data: - pgadmin_data: - authelia_config: - authelia_db_data: - grafana_data: - prometheus_data: - alertmanager_data: - -######################## -# Services -######################## -services: - - ## ───────────────────────────────────────────── - ## Traefik — edge router + ACME (HTTP-01) - ## ───────────────────────────────────────────── - traefik: - image: traefik:latest - container_name: traefik - restart: unless-stopped - ports: - - "80:80" - - "443:443" - # Mail protocol ports for MailCow integration - - "25:25" # SMTP - - "465:465" # SMTPS - - "587:587" # Submission - - "143:143" # IMAP - - "993:993" # IMAPS - - "110:110" # POP3 - - "995:995" # POP3S - - "4190:4190" # ManageSieve - networks: [traefik_proxy] - environment: - TZ: "${TZ}" - command: - # Experimental plugins - - --experimental.plugins.traefik-umami-plugin.modulename=github.com/1cedsoda/traefik-umami-plugin - - --experimental.plugins.traefik-umami-plugin.version=v1.0.3 - - # Providers - - --providers.docker=true - - --providers.docker.exposedbydefault=false - - # Entrypoints - - --entrypoints.web.address=:80 - - --entrypoints.web.http.redirections.entrypoint.to=websecure - - --entrypoints.web.http.redirections.entrypoint.scheme=https - - --entrypoints.websecure.address=:443 - - --entrypoints.web.forwardedheaders.insecure=true - - --entrypoints.websecure.forwardedheaders.insecure=true - - # Mail protocol entrypoints for MailCow integration - - --entrypoints.smtp.address=:25 - - --entrypoints.smtps.address=:465 - - --entrypoints.submission.address=:587 - - --entrypoints.imap.address=:143 - - --entrypoints.imaps.address=:993 - - --entrypoints.pop3.address=:110 - - --entrypoints.pop3s.address=:995 - - --entrypoints.sieve.address=:4190 - - # Dashboard/API (internal) - - --api=true - - --api.dashboard=true - - # ACME via HTTP-01 (no registrar API needed) - - --certificatesresolvers.le.acme.email=${ACME_EMAIL} - - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json - - --certificatesresolvers.le.acme.httpchallenge=true - - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web - # (Alt) Use TLS-ALPN-01 if port 80 is blocked: - # - --certificatesresolvers.le.acme.tlschallenge=true - - # Global timeouts for slow backends - - --serversTransport.forwardingTimeouts.dialTimeout=30s - - --serversTransport.forwardingTimeouts.responseHeaderTimeout=60s - - --serversTransport.forwardingTimeouts.idleConnTimeout=180s - - # Logs - - --accesslog.filepath=/var/log/traefik/access.log - - --accesslog.bufferingsize=100 - - --log.level=INFO - - --metrics.prometheus=true - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - traefik_letsencrypt:/letsencrypt - - traefik_logs:/var/log/traefik - labels: - - traefik.enable=true - - # Reusable security headers - - traefik.http.middlewares.security-headers.headers.stsSeconds=31536000 - - traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true - - traefik.http.middlewares.security-headers.headers.stsPreload=true - - traefik.http.middlewares.security-headers.headers.browserXssFilter=true - - traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true - - traefik.http.middlewares.security-headers.headers.referrerPolicy=no-referrer-when-downgrade - - # # Basic Auth middleware - # - traefik.http.middlewares.basic-auth.basicauth.users=${BASIC_AUTH_USERS} - - # Authelia middleware - - traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth - - traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true - - traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Email,Remote-Name - - # Traefik dashboard (protected) - - traefik.http.routers.traefik.rule=Host(`traefik.test.${DOMAIN}`) - - traefik.http.routers.traefik.entrypoints=websecure - - traefik.http.routers.traefik.tls.certresolver=le - - traefik.http.routers.traefik.service=api@internal - - traefik.http.routers.traefik.middlewares=authelia@docker,security-headers - - ## ───────────────────────────────────────────── - ## Authelia — authentication and authorization - ## ───────────────────────────────────────────── - authelia: - image: authelia/authelia:latest - container_name: authelia - restart: unless-stopped - networks: [traefik_proxy] - volumes: - - authelia_config:/config - environment: - TZ: "${TZ}" - AUTHELIA_DB_PASSWORD: "${AUTHELIA_DB_PASSWORD}" - AUTHELIA_JWT_SECRET: "${AUTHELIA_JWT_SECRET}" - AUTHELIA_SESSION_SECRET: "${AUTHELIA_SESSION_SECRET}" - depends_on: - - authelia-db - labels: - - traefik.enable=true - - traefik.http.routers.authelia.rule=Host(`auth.test.${DOMAIN}`) - - traefik.http.routers.authelia.entrypoints=websecure - - traefik.http.routers.authelia.tls.certresolver=le - - traefik.http.services.authelia.loadbalancer.server.port=9091 - - ## ───────────────────────────────────────────── - ## Authelia Database — PostgreSQL - ## ───────────────────────────────────────────── - authelia-db: - image: postgres:15-alpine - container_name: authelia-db - restart: unless-stopped - networks: [traefik_proxy] - environment: - POSTGRES_DB: authelia - POSTGRES_USER: authelia - POSTGRES_PASSWORD: ${AUTHELIA_DB_PASSWORD} - volumes: - - authelia_db_data:/var/lib/postgresql/data - - ## ───────────────────────────────────────────── - ## Portainer — Docker control plane - ## ───────────────────────────────────────────── - portainer: - image: portainer/portainer-ce:latest - container_name: portainer - restart: unless-stopped - networks: [traefik_proxy] - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - portainer_data:/data - labels: - - traefik.enable=true - - traefik.http.routers.portainer.rule=Host(`portainer.test.${DOMAIN}`) - - traefik.http.routers.portainer.entrypoints=websecure - - traefik.http.routers.portainer.tls.certresolver=le - - traefik.http.routers.portainer.middlewares=security-headers - - traefik.http.services.portainer.loadbalancer.server.port=9000 - - ## ───────────────────────────────────────────── - ## Uptime Kuma — status page / checks - ## ───────────────────────────────────────────── - uptime-kuma: - image: louislam/uptime-kuma:latest - container_name: uptime-kuma - restart: unless-stopped - volumes: - - uptime_kuma_data:/app/data - networks: [traefik_proxy] - labels: - - traefik.enable=true - - traefik.http.routers.kuma.rule=Host(`uptime.test.${DOMAIN}`) - - traefik.http.routers.kuma.entrypoints=websecure - - traefik.http.routers.kuma.tls.certresolver=le - - traefik.http.routers.kuma.middlewares=security-headers - - traefik.http.services.kuma.loadbalancer.server.port=3001 - - ## ───────────────────────────────────────────── - ## Umami — web analytics - ## ───────────────────────────────────────────── - umami: - image: ghcr.io/umami-software/umami:postgresql-latest - container_name: umami - restart: unless-stopped - networks: [traefik_proxy] - environment: - DATABASE_URL: postgresql://${UMAMI_DB_USER}:${UMAMI_DB_PASS}@umami-db:5432/${UMAMI_DB_NAME} - DATABASE_TYPE: postgresql - APP_SECRET: ${UMAMI_APP_SECRET} - depends_on: - - umami-db - labels: - - traefik.enable=true - - traefik.http.routers.umami.rule=Host(`umami.test.${DOMAIN}`) - - traefik.http.routers.umami.entrypoints=websecure - - traefik.http.routers.umami.tls.certresolver=le - - traefik.http.routers.umami.middlewares=security-headers - - traefik.http.services.umami.loadbalancer.server.port=3000 - - ## ───────────────────────────────────────────── - ## Umami Database — PostgreSQL - ## ───────────────────────────────────────────── - umami-db: - image: postgres:15-alpine - container_name: umami-db - restart: unless-stopped - networks: [traefik_proxy] - environment: - POSTGRES_DB: ${UMAMI_DB_NAME} - POSTGRES_USER: ${UMAMI_DB_USER} - POSTGRES_PASSWORD: ${UMAMI_DB_PASS} - volumes: - - umami_data:/var/lib/postgresql/data - - # ───────────────────────────────────────────── - # pgAdmin — PostgreSQL administration - # ───────────────────────────────────────────── - pgadmin: - image: dpage/pgadmin4:latest - container_name: pgadmin - restart: unless-stopped - networks: [traefik_proxy] - environment: - PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} - PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} - PGADMIN_CONFIG_SERVER_MODE: 'True' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - # Fix CSRF issues behind reverse proxy - PGADMIN_CONFIG_WTF_CSRF_CHECK_DEFAULT: 'False' - PGADMIN_CONFIG_WTF_CSRF_TIME_LIMIT: 'None' - PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: 'False' - # Trust proxy headers - PGADMIN_CONFIG_PROXY_X_HOST_COUNT: '1' - PGADMIN_CONFIG_PROXY_X_PREFIX_COUNT: '1' - volumes: - - pgadmin_data:/var/lib/pgadmin - labels: - - traefik.enable=true - - traefik.http.routers.pgadmin.rule=Host(`pgadmin.test.${DOMAIN}`) - - traefik.http.routers.pgadmin.entrypoints=websecure - - traefik.http.routers.pgadmin.tls.certresolver=le - - traefik.http.services.pgadmin.loadbalancer.server.port=80 - - ## ───────────────────────────────────────────── - ## Prometheus — monitoring - ## ───────────────────────────────────────────── - prometheus: - image: prom/prometheus:latest - container_name: prometheus - restart: unless-stopped - networks: [traefik_proxy] - volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml - - ./rules.yml:/etc/prometheus/rules.yml - - prometheus_data:/prometheus - - /var/run/docker.sock:/var/run/docker.sock:ro - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - - '--web.console.libraries=/etc/prometheus/console_libraries' - - '--web.console.templates=/etc/prometheus/consoles' - - '--storage.tsdb.retention.time=200h' - - '--web.enable-lifecycle' - labels: - - traefik.enable=true - - traefik.http.routers.prometheus.rule=Host(`prometheus.test.${DOMAIN}`) - - traefik.http.routers.prometheus.entrypoints=websecure - - traefik.http.routers.prometheus.tls.certresolver=le - - traefik.http.routers.prometheus.middlewares=authelia@docker,security-headers - - traefik.http.services.prometheus.loadbalancer.server.port=9090 - - ## ───────────────────────────────────────────── - ## Grafana — visualization - ## ───────────────────────────────────────────── - grafana: - image: grafana/grafana:latest - container_name: grafana - restart: unless-stopped - networks: [traefik_proxy] - environment: - GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD} - volumes: - - grafana_data:/var/lib/grafana - labels: - - traefik.enable=true - - traefik.http.routers.grafana.rule=Host(`grafana.test.${DOMAIN}`) - - traefik.http.routers.grafana.entrypoints=websecure - - traefik.http.routers.grafana.tls.certresolver=le - - traefik.http.routers.grafana.middlewares=authelia@docker,security-headers - - traefik.http.services.grafana.loadbalancer.server.port=3000 - - ## ───────────────────────────────────────────── - ## Alertmanager — alert handling - ## ───────────────────────────────────────────── - alertmanager: - image: prom/alertmanager:latest - container_name: alertmanager - restart: unless-stopped - networks: [traefik_proxy] - volumes: - - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml - - alertmanager_data:/alertmanager - command: - - '--config.file=/etc/alertmanager/alertmanager.yml' - - '--storage.path=/alertmanager' - labels: - - traefik.enable=true - - traefik.http.routers.alertmanager.rule=Host(`alertmanager.test.${DOMAIN}`) - - traefik.http.routers.alertmanager.entrypoints=websecure - - traefik.http.routers.alertmanager.tls.certresolver=le - - traefik.http.routers.alertmanager.middlewares=authelia@docker,security-headers - - traefik.http.services.alertmanager.loadbalancer.server.port=9093 diff --git a/docker-compose.yml b/docker-compose.yml index 3f03f0b..d64cdb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,11 +12,10 @@ volumes: uptime_kuma_data: umami_data: pgadmin_data: - authelia_config: authelia_db_data: - grafana_data: - prometheus_data: - alertmanager_data: + beszel_data: + gitea_data: + gitea_db_data: ######################## # Services @@ -46,6 +45,9 @@ services: environment: TZ: "${TZ}" command: + # Enable ping endpoint for health checks + - --ping=true + # Experimental plugins - --experimental.plugins.traefik-umami-plugin.modulename=github.com/1cedsoda/traefik-umami-plugin - --experimental.plugins.traefik-umami-plugin.version=v1.0.3 @@ -118,11 +120,17 @@ services: - traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Email,Remote-Name # Traefik dashboard (protected) - - traefik.http.routers.traefik.rule=Host(`traefik.gate.${DOMAIN}`) + - traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN_PREFIX}.${DOMAIN}`) - traefik.http.routers.traefik.entrypoints=websecure - traefik.http.routers.traefik.tls.certresolver=le - traefik.http.routers.traefik.service=api@internal - traefik.http.routers.traefik.middlewares=authelia@docker,security-headers + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s ## ───────────────────────────────────────────── ## Authelia — authentication and authorization @@ -133,17 +141,27 @@ services: restart: unless-stopped networks: [traefik_proxy] volumes: - - authelia_config:/config + - ./authelia:/config + entrypoint: /bin/sh + command: + - -c + - | + eval "echo \"$(cat /config/configuration.template.yml)\"" > /config/configuration.yml + exec /app/entrypoint.sh environment: TZ: "${TZ}" - AUTHELIA_DB_PASSWORD: "${AUTHELIA_DB_PASSWORD}" - AUTHELIA_JWT_SECRET: "${AUTHELIA_JWT_SECRET}" - AUTHELIA_SESSION_SECRET: "${AUTHELIA_SESSION_SECRET}" + DOMAIN: "${DOMAIN}" + DOMAIN_PREFIX: "${DOMAIN_PREFIX}" + AUTHELIA_SESSION_SECRET: '${AUTHELIA_SESSION_SECRET}' + AUTHELIA_STORAGE_ENCRYPTION_KEY: '${AUTHELIA_STORAGE_ENCRYPTION_KEY}' + AUTHELIA_STORAGE_POSTGRES_PASSWORD: '${AUTHELIA_DB_PASSWORD}' + AUTHELIA_NOTIFIER_SMTP_PASSWORD: '${AUTHELIA_NOTIFIER_SMTP_PASSWORD}' + AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: '${AUTHELIA_JWT_SECRET}' depends_on: - authelia-db labels: - traefik.enable=true - - traefik.http.routers.authelia.rule=Host(`auth.gate.${DOMAIN}`) + - traefik.http.routers.authelia.rule=Host(`auth.${DOMAIN_PREFIX}.${DOMAIN}`) - traefik.http.routers.authelia.entrypoints=websecure - traefik.http.routers.authelia.tls.certresolver=le - traefik.http.services.authelia.loadbalancer.server.port=9091 @@ -162,6 +180,12 @@ services: POSTGRES_PASSWORD: ${AUTHELIA_DB_PASSWORD} volumes: - authelia_db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U authelia -d authelia"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s ## ───────────────────────────────────────────── ## Portainer — Docker control plane @@ -171,12 +195,13 @@ services: container_name: portainer restart: unless-stopped networks: [traefik_proxy] + command: --host unix:///var/run/docker.sock volumes: - /var/run/docker.sock:/var/run/docker.sock - portainer_data:/data labels: - traefik.enable=true - - traefik.http.routers.portainer.rule=Host(`portainer.gate.${DOMAIN}`) + - traefik.http.routers.portainer.rule=Host(`portainer.${DOMAIN_PREFIX}.${DOMAIN}`) - traefik.http.routers.portainer.entrypoints=websecure - traefik.http.routers.portainer.tls.certresolver=le - traefik.http.routers.portainer.middlewares=security-headers @@ -194,7 +219,7 @@ services: networks: [traefik_proxy] labels: - traefik.enable=true - - traefik.http.routers.kuma.rule=Host(`uptime.gate.${DOMAIN}`) + - traefik.http.routers.kuma.rule=Host(`uptime.${DOMAIN_PREFIX}.${DOMAIN}`) - traefik.http.routers.kuma.entrypoints=websecure - traefik.http.routers.kuma.tls.certresolver=le - traefik.http.routers.kuma.middlewares=security-headers @@ -216,11 +241,17 @@ services: - umami-db labels: - traefik.enable=true - - traefik.http.routers.umami.rule=Host(`umami.gate.${DOMAIN}`) + - traefik.http.routers.umami.rule=Host(`umami.${DOMAIN_PREFIX}.${DOMAIN}`) - traefik.http.routers.umami.entrypoints=websecure - traefik.http.routers.umami.tls.certresolver=le - traefik.http.routers.umami.middlewares=security-headers - traefik.http.services.umami.loadbalancer.server.port=3000 + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s ## ───────────────────────────────────────────── ## Umami Database — PostgreSQL @@ -236,6 +267,12 @@ services: POSTGRES_PASSWORD: ${UMAMI_DB_PASS} volumes: - umami_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${UMAMI_DB_USER} -d ${UMAMI_DB_NAME}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s # ───────────────────────────────────────────── # pgAdmin — PostgreSQL administration @@ -250,88 +287,113 @@ services: PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} PGADMIN_CONFIG_SERVER_MODE: 'True' PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - # Fix CSRF issues behind reverse proxy PGADMIN_CONFIG_WTF_CSRF_CHECK_DEFAULT: 'False' PGADMIN_CONFIG_WTF_CSRF_TIME_LIMIT: 'None' PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: 'False' - # Trust proxy headers PGADMIN_CONFIG_PROXY_X_HOST_COUNT: '1' PGADMIN_CONFIG_PROXY_X_PREFIX_COUNT: '1' volumes: - pgadmin_data:/var/lib/pgadmin labels: - traefik.enable=true - - traefik.http.routers.pgadmin.rule=Host(`pgadmin.gate.${DOMAIN}`) + - traefik.http.routers.pgadmin.rule=Host(`pgadmin.${DOMAIN_PREFIX}.${DOMAIN}`) - traefik.http.routers.pgadmin.entrypoints=websecure - traefik.http.routers.pgadmin.tls.certresolver=le + - traefik.http.routers.pgadmin.middlewares=security-headers - traefik.http.services.pgadmin.loadbalancer.server.port=80 + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s ## ───────────────────────────────────────────── - ## Prometheus — monitoring + ## Beszel Hub — lightweight server monitoring ## ───────────────────────────────────────────── - prometheus: - image: prom/prometheus:latest - container_name: prometheus + beszel: + image: henrygd/beszel:latest + container_name: beszel restart: unless-stopped networks: [traefik_proxy] volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml - - ./rules.yml:/etc/prometheus/rules.yml - - prometheus_data:/prometheus - - /var/run/docker.sock:/var/run/docker.sock:ro - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - - '--web.console.libraries=/etc/prometheus/console_libraries' - - '--web.console.templates=/etc/prometheus/consoles' - - '--storage.tsdb.retention.time=200h' - - '--web.enable-lifecycle' + - beszel_data:/beszel_data labels: - traefik.enable=true - - traefik.http.routers.prometheus.rule=Host(`prometheus.gate.${DOMAIN}`) - - traefik.http.routers.prometheus.entrypoints=websecure - - traefik.http.routers.prometheus.tls.certresolver=le - - traefik.http.routers.prometheus.middlewares=authelia@docker,security-headers - - traefik.http.services.prometheus.loadbalancer.server.port=9090 + - traefik.http.routers.beszel.rule=Host(`beszel.${DOMAIN_PREFIX}.${DOMAIN}`) + - traefik.http.routers.beszel.entrypoints=websecure + - traefik.http.routers.beszel.tls.certresolver=le + - traefik.http.routers.beszel.middlewares=security-headers + - traefik.http.services.beszel.loadbalancer.server.port=8090 + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8090"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s ## ───────────────────────────────────────────── - ## Grafana — visualization + ## Gitea — self-hosted Git service ## ───────────────────────────────────────────── - grafana: - image: grafana/grafana:latest - container_name: grafana + gitea: + image: docker.gitea.com/gitea:latest + container_name: gitea restart: unless-stopped networks: [traefik_proxy] environment: - GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD} + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=gitea-db:5432 + - GITEA__database__NAME=${GITEA_DB_NAME} + - GITEA__database__USER=${GITEA_DB_USER} + - GITEA__database__PASSWD=${GITEA_DB_PASSWORD} + - GITEA__server__DOMAIN=git.${DOMAIN_PREFIX}.${DOMAIN} + - GITEA__server__SSH_DOMAIN=git.${DOMAIN_PREFIX}.${DOMAIN} + - GITEA__server__ROOT_URL=https://git.${DOMAIN_PREFIX}.${DOMAIN}/ + - GITEA__server__SSH_PORT=222 + - GITEA__server__SSH_LISTEN_PORT=22 + - GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY} + - GITEA__security__INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN} volumes: - - grafana_data:/var/lib/grafana + - gitea_data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "222:22" + depends_on: + - gitea-db labels: - traefik.enable=true - - traefik.http.routers.grafana.rule=Host(`grafana.gate.${DOMAIN}`) - - traefik.http.routers.grafana.entrypoints=websecure - - traefik.http.routers.grafana.tls.certresolver=le - - traefik.http.routers.grafana.middlewares=authelia@docker,security-headers - - traefik.http.services.grafana.loadbalancer.server.port=3000 + - traefik.http.routers.gitea.rule=Host(`git.${DOMAIN_PREFIX}.${DOMAIN}`) + - traefik.http.routers.gitea.entrypoints=websecure + - traefik.http.routers.gitea.tls.certresolver=le + - traefik.http.routers.gitea.middlewares=security-headers + - traefik.http.services.gitea.loadbalancer.server.port=3000 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s ## ───────────────────────────────────────────── - ## Alertmanager — alert handling + ## Gitea Database — PostgreSQL ## ───────────────────────────────────────────── - alertmanager: - image: prom/alertmanager:latest - container_name: alertmanager + gitea-db: + image: postgres:15-alpine + container_name: gitea-db restart: unless-stopped networks: [traefik_proxy] + environment: + POSTGRES_DB: ${GITEA_DB_NAME} + POSTGRES_USER: ${GITEA_DB_USER} + POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD} volumes: - - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml - - alertmanager_data:/alertmanager - command: - - '--config.file=/etc/alertmanager/alertmanager.yml' - - '--storage.path=/alertmanager' - labels: - - traefik.enable=true - - traefik.http.routers.alertmanager.rule=Host(`alertmanager.gate.${DOMAIN}`) - - traefik.http.routers.alertmanager.entrypoints=websecure - - traefik.http.routers.alertmanager.tls.certresolver=le - - traefik.http.routers.alertmanager.middlewares=authelia@docker,security-headers - - traefik.http.services.alertmanager.loadbalancer.server.port=9093 + - gitea_db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${GITEA_DB_USER} -d ${GITEA_DB_NAME}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s diff --git a/prometheus.yml b/prometheus.yml deleted file mode 100644 index eb57ec1..0000000 --- a/prometheus.yml +++ /dev/null @@ -1,43 +0,0 @@ -global: - scrape_interval: 15s - evaluation_interval: 15s - -rule_files: - - rules.yml - -alerting: - alertmanagers: - - static_configs: - - targets: - - alertmanager:9093 - -scrape_configs: - - job_name: 'prometheus' - static_configs: - - targets: ['localhost:9090'] - - - job_name: 'traefik' - static_configs: - - targets: ['traefik:8080'] - metrics_path: /metrics - - - job_name: 'authelia' - static_configs: - - targets: ['authelia:9091'] - - - job_name: 'grafana' - static_configs: - - targets: ['grafana:3000'] - - - job_name: 'docker' - docker_sd_configs: - - host: unix:///var/run/docker.sock - relabel_configs: - - source_labels: [__meta_docker_container_name] - regex: '/(.*)' - target_label: container_name - - source_labels: [__meta_docker_container_label_com_docker_compose_service] - target_label: service - - action: keep - source_labels: [__meta_docker_container_label_com_docker_compose_service] - regex: '.*' \ No newline at end of file diff --git a/rules.yml b/rules.yml deleted file mode 100644 index a96694d..0000000 --- a/rules.yml +++ /dev/null @@ -1,36 +0,0 @@ -groups: - - name: recording_rules - rules: - - record: job:up:sum - expr: sum(up) by (job) - - record: job:up:count - expr: count(up) by (job) - - - name: alerting_rules - rules: - - alert: InstanceDown - expr: up == 0 - for: 5m - labels: - severity: critical - annotations: - summary: "Instance {{ $labels.instance }} down" - description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes." - - - alert: TraefikDown - expr: up{job="traefik"} == 0 - for: 2m - labels: - severity: warning - annotations: - summary: "Traefik is down" - description: "Traefik has been down for more than 2 minutes." - - - alert: AutheliaDown - expr: up{job="authelia"} == 0 - for: 5m - labels: - severity: critical - annotations: - summary: "Authelia is down" - description: "Authelia authentication service is unavailable." \ No newline at end of file