Add initial configuration files for deployment and services setup

This commit is contained in:
Mohmmed Elfateh Sabry
2025-08-10 23:58:57 +03:00
commit 8fd3496e05
6 changed files with 625 additions and 0 deletions

26
.env Normal file
View File

@@ -0,0 +1,26 @@
# .env
## Domain / Timezone
DOMAIN=3launchpad.com
TZ=Africa/Cairo
## ACME (Let's Encrypt)
ACME_EMAIL=admin@3launchpad.com
## Namecheap DNS API (whitelist your VPS IP in Namecheap API settings)
NAMECHEAP_API_USER=your_namecheap_username
NAMECHEAP_API_KEY=your_namecheap_api_key
## CrowdSec
# Generate with: docker exec -it crowdsec cscli bouncers add traefik-bouncer
CROWDSEC_BOUNCER_KEY=change_this_long_random_key
## Umami (PostgreSQL)
UMAMI_DB_USER=umami
UMAMI_DB_PASS=umami_strong_pass
UMAMI_DB_NAME=umami
UMAMI_APP_SECRET=generate_a_random_64char_secret
## Grafana
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASS=change_me_admin

118
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
# .github/workflows/deploy.yml
name: Deploy Gateway to VPS
on:
push:
branches: [ "main" ]
paths:
- "docker-compose.yml"
- "authelia/**"
- "crowdsec/**"
- "prometheus/**"
- "grafana/**"
- ".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: Create target dir
run: |
ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" "sudo mkdir -p '${REMOTE_DIR}' && sudo chown -R \$USER:\$USER '${REMOTE_DIR}'"
- name: Sync repo to VPS (rsync)
run: |
rsync -az --delete \
-e "ssh -p ${{ secrets.SSH_PORT }}" \
--exclude ".git" \
--exclude ".github" \
./ "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${REMOTE_DIR}/"
- name: Write .env on VPS (from GitHub Secrets)
run: |
ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" "bash -se" <<'EOF'
set -euo pipefail
cd "${REMOTE_DIR}"
cat > .env <<'ENVVARS'
# --- Domain / Timezone ---
DOMAIN=${{ secrets.DOMAIN }}
TZ=${{ secrets.TZ }}
# --- ACME / Let's Encrypt (email only) ---
ACME_EMAIL=${{ secrets.ACME_EMAIL }}
# --- Namecheap DNS API ---
NAMECHEAP_API_USER=${{ secrets.NAMECHEAP_API_USER }}
NAMECHEAP_API_KEY=${{ secrets.NAMECHEAP_API_KEY }}
# --- CrowdSec ---
CROWDSEC_BOUNCER_KEY=${{ secrets.CROWDSEC_BOUNCER_KEY }}
# --- Umami (PostgreSQL) ---
UMAMI_DB_USER=${{ secrets.UMAMI_DB_USER }}
UMAMI_DB_PASS=${{ secrets.UMAMI_DB_PASS }}
UMAMI_DB_NAME=${{ secrets.UMAMI_DB_NAME }}
UMAMI_APP_SECRET=${{ secrets.UMAMI_APP_SECRET }}
# --- Grafana ---
GRAFANA_ADMIN_USER=${{ secrets.GRAFANA_ADMIN_USER }}
GRAFANA_ADMIN_PASS=${{ secrets.GRAFANA_ADMIN_PASS }}
ENVVARS
EOF
- name: Pre-flight checks (docker + compose)
run: |
ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" "
docker version && docker compose version
"
- name: Deploy (pull, up, prune)
run: |
ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" "bash -se" <<'EOF'
set -euo pipefail
cd "${REMOTE_DIR}"
# Warm up networks/volumes and pull images
docker compose pull
# Bring up (idempotent), remove old orphans
docker compose up -d --remove-orphans
# Light cleanup of old images (keeps running ones)
docker image prune -af || true
echo
echo '--- Running containers ---'
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
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 "Status (Kuma): https://status.gate.${{ secrets.DOMAIN }}"
echo "Grafana: https://grafana.gate.${{ secrets.DOMAIN }}"
echo "Prometheus: https://prometheus.gate.${{ secrets.DOMAIN }}"
echo "Umami: https://umami.gate.${{ secrets.DOMAIN }}"

152
authelia/configuration.yml Normal file
View File

@@ -0,0 +1,152 @@
# authelia/configuration.yml
# Authelia v4 configuration for: auth.gate.3launchpad.com
# Behind Traefik (forward-auth), Redis for sessions, SQLite storage.
# ⚠️ Replace all "changeme_*" values or (better) override via env vars:
# AUTHELIA_JWT_SECRET, AUTHELIA_SESSION_SECRET, AUTHELIA_STORAGE_ENCRYPTION_KEY
#########################################################
# Server & Logging
#########################################################
server:
address: "tcp://0.0.0.0:9091" # Traefik talks to this
buffers:
read: 4096
write: 4096
log:
level: info
theme: auto
# Where to send users if they hit a protected resource without a Referer
default_redirection_url: "https://traefik.gate.3launchpad.com/"
#########################################################
# Secrets (use env vars in production)
#########################################################
# Prefer setting via Docker env:
# AUTHELIA_JWT_SECRET, AUTHELIA_SESSION_SECRET, AUTHELIA_STORAGE_ENCRYPTION_KEY
jwt_secret: "changeme_jwt_secret"
#########################################################
# Authentication Backend (local file)
#########################################################
authentication_backend:
file:
path: /config/users_database.yml
# New hashes should be argon2id. Use: `authelia crypto hash generate argon2`
password:
algorithm: argon2id
iterations: 3
memory: 64
parallelism: 4
salt_length: 16
key_length: 32
#########################################################
# Access Control (who can access what)
#########################################################
access_control:
default_policy: deny
rules:
# Public status page
- domain: "status.gate.3launchpad.com"
policy: bypass
# Admin-only, require 2FA
- domain: "traefik.gate.3launchpad.com"
subject: ["group:admins"]
policy: two_factor
- domain: "portainer.gate.3launchpad.com"
subject: ["group:admins"]
policy: two_factor
# Admin/Devs with 1FA for these tools
- domain_regex: "(grafana|prometheus|umami)\\.gate\\.3launchpad\\.com"
subject:
- "group:admins"
- "group:devs"
policy: one_factor
# Catch-all for any other subdomain under *.gate.3launchpad.com -> authenticated users
- domain: "*.gate.3launchpad.com"
subject:
- "group:users"
- "group:admins"
- "group:devs"
policy: one_factor
#########################################################
# Session (cookies + Redis)
#########################################################
session:
name: authelia_session
domain: "gate.3launchpad.com" # cookie scope (covers *.gate.3launchpad.com)
same_site: Lax
expiration: 1h
inactivity: 30m
remember_me_duration: 1M
# secret can be overridden by env AUTHELIA_SESSION_SECRET
secret: "changeme_session_secret"
redis:
host: redis
port: 6379
# tls: false
#########################################################
# Regulation (anti-bruteforce)
#########################################################
regulation:
max_retries: 3
find_time: 2m
ban_time: 10m
#########################################################
# Storage (SQLite on persistent volume)
#########################################################
storage:
encryption_key: "changeme_storage_key" # override via AUTHELIA_STORAGE_ENCRYPTION_KEY
local:
path: /config/db.sqlite3
#########################################################
# Notifier (choose one)
#########################################################
# For testing/dev: writes emails to a file
notifier:
filesystem:
filename: /config/notification.txt
# For production, comment the block above and use SMTP:
# notifier:
# smtp:
# address: "smtp.gmail.com:587"
# username: "no-reply@3launchpad.com"
# # password via env: AUTHELIA_NOTIFIER_SMTP_PASSWORD
# sender: "3Launchpad Auth <no-reply@3launchpad.com>"
# subject: "[3Launchpad] {title}"
# startup_check_address: "you@3launchpad.com"
# disable_require_tls: false
# tls:
# server_name: "smtp.gmail.com"
# skip_verify: false
#########################################################
# TOTP / Duo / WebAuthn (2FA)
#########################################################
totp:
issuer: "3launchpad.com"
period: 30
skew: 1
webauthn:
disable: false
timeout: 60s
display_name: "3Launchpad Gateway"
relying_party_id: "gate.3launchpad.com"
# If you plan to use Duo Push in the future:
# duo_api:
# hostname: api-XXXXXXXX.duosecurity.com
# integration_key: YOUR_IKEY
# # secret_key via env: AUTHELIA_DUO_API_SECRET_KEY

4
crowdsec/acquis.yaml Normal file
View File

@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik

309
docker-compose.yml Normal file
View File

@@ -0,0 +1,309 @@
# docker-compose.yml
version: "3.9"
########################
# Networks & Volumes
########################
networks:
traefik_proxy:
name: traefik_proxy
monitoring:
name: monitoring
internal:
name: internal
volumes:
traefik_letsencrypt:
traefik_logs:
portainer_data:
umami_db_data:
authelia_data:
crowdsec_data:
prometheus_data:
grafana_data:
uptime_kuma_data:
########################
# Services
########################
services:
## ─────────────────────────────────────────────
## Traefik — edge router + ACME (Namecheap DNS)
## ─────────────────────────────────────────────
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks: [traefik_proxy, monitoring]
environment:
# Namecheap DNS challenge auth
NAMECHEAP_API_USER: "${NAMECHEAP_API_USER}"
NAMECHEAP_API_KEY: "${NAMECHEAP_API_KEY}"
TZ: "${TZ}"
command:
# 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
# Dashboard/API (internal)
- --api.dashboard=true
# ACME via DNS-01 (wildcard for *.gate.${DOMAIN})
- --certificatesresolvers.le.acme.email=${ACME_EMAIL}
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.dnschallenge=true
- --certificatesresolvers.le.acme.dnschallenge.provider=namecheap
# Optional: if DNS propagation is slow, uncomment:
# - --certificatesresolvers.le.acme.dnschallenge.disablepropagationcheck=true
# Metrics (Prometheus)
- --metrics.prometheus=true
- --metrics.prometheus.addrouterslabels=true
# Logs
- --accesslog.filepath=/var/log/traefik/access.log
- --accesslog.bufferingsize=100
- --log.level=INFO
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
- traefik.http.middlewares.security-headers.headers.frameDeny=true
# Authelia ForwardAuth (reusable)
- traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.gate.${DOMAIN}
- traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true
- traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email
# CrowdSec forward-auth (reusable)
- traefik.http.middlewares.crowdsec.forwardauth.address=http://traefik-bouncer:8080/api/v1/forwardAuth
- traefik.http.middlewares.crowdsec.forwardauth.trustForwardHeader=true
# Traefik dashboard (protected)
- traefik.http.routers.traefik.rule=Host(`traefik.gate.${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=crowdsec,authelia,security-headers
## ─────────────────────────────────────────────
## 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.gate.${DOMAIN}`)
- traefik.http.routers.portainer.entrypoints=websecure
- traefik.http.routers.portainer.tls.certresolver=le
- traefik.http.routers.portainer.middlewares=crowdsec,authelia,security-headers
- traefik.http.services.portainer.loadbalancer.server.port=9000
## ─────────────────────────────────────────────
## Umami + PostgreSQL — privacy analytics
## ─────────────────────────────────────────────
umami-db:
image: postgres:16
container_name: umami-db
restart: unless-stopped
environment:
POSTGRES_USER: ${UMAMI_DB_USER}
POSTGRES_PASSWORD: ${UMAMI_DB_PASS}
POSTGRES_DB: ${UMAMI_DB_NAME}
TZ: "${TZ}"
volumes:
- umami_db_data:/var/lib/postgresql/data
networks: [internal]
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
restart: unless-stopped
depends_on: [umami-db]
environment:
DATABASE_URL: postgresql://${UMAMI_DB_USER}:${UMAMI_DB_PASS}@umami-db:5432/${UMAMI_DB_NAME}
APP_SECRET: ${UMAMI_APP_SECRET}
TRACKER_SCRIPT_NAME: umami
TZ: "${TZ}"
networks: [traefik_proxy, internal]
labels:
- traefik.enable=true
- traefik.http.routers.umami.rule=Host(`umami.gate.${DOMAIN}`)
- traefik.http.routers.umami.entrypoints=websecure
- traefik.http.routers.umami.tls.certresolver=le
- traefik.http.routers.umami.middlewares=crowdsec,authelia,security-headers
- traefik.http.services.umami.loadbalancer.server.port=3000
## ─────────────────────────────────────────────
## Authelia + Redis — SSO/MFA
## ─────────────────────────────────────────────
authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
depends_on: [redis]
environment:
TZ: "${TZ}"
volumes:
# Provide your config at ./authelia/configuration.yml
- ./authelia/configuration.yml:/config/configuration.yml:ro
- authelia_data:/config
networks: [traefik_proxy, internal]
labels:
- traefik.enable=true
- traefik.http.routers.authelia.rule=Host(`auth.gate.${DOMAIN}`)
- traefik.http.routers.authelia.entrypoints=websecure
- traefik.http.routers.authelia.tls.certresolver=le
- traefik.http.routers.authelia.middlewares=crowdsec,security-headers
- traefik.http.services.authelia.loadbalancer.server.port=9091
redis:
image: redis:7-alpine
container_name: authelia-redis
restart: unless-stopped
networks: [internal]
## ─────────────────────────────────────────────
## CrowdSec (LAPI) + Traefik bouncer (forwardAuth)
## ─────────────────────────────────────────────
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
TZ: "${TZ}"
GID: "0"
COLLECTIONS: "crowdsecurity/traefik crowdsecurity/linux"
volumes:
- crowdsec_data:/var/lib/crowdsec/data
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- traefik_logs:/var/log/traefik:ro
networks: [traefik_proxy]
# Auto-register the bouncer once (uses CROWDSEC_BOUNCER_KEY from .env)
crowdsec-init:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec-init
depends_on: [crowdsec]
entrypoint: sh -c "cscli bouncers add traefik-bouncer -k ${CROWDSEC_BOUNCER_KEY} || true && sleep 2"
networks: [traefik_proxy]
restart: "no"
traefik-bouncer:
image: crowdsecurity/traefik-bouncer:latest
container_name: traefik-bouncer
restart: unless-stopped
depends_on: [crowdsec, crowdsec-init]
environment:
CROWDSEC_BOUNCER_API_KEY: "${CROWDSEC_BOUNCER_KEY}"
CROWDSEC_AGENT_HOST: crowdsec:8080
GIN_MODE: release
networks: [traefik_proxy]
## ─────────────────────────────────────────────
## Uptime Kuma — status page / checks
## ─────────────────────────────────────────────
uptime-kuma:
image: louislam/uptime-kuma:1
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(`status.gate.${DOMAIN}`)
- traefik.http.routers.kuma.entrypoints=websecure
- traefik.http.routers.kuma.tls.certresolver=le
- traefik.http.routers.kuma.middlewares=crowdsec,authelia,security-headers
- traefik.http.services.kuma.loadbalancer.server.port=3001
## ─────────────────────────────────────────────
## Prometheus + exporters + Grafana
## ─────────────────────────────────────────────
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
networks: [monitoring, traefik_proxy]
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
labels:
- traefik.enable=true
- traefik.http.routers.prom.rule=Host(`prometheus.gate.${DOMAIN}`)
- traefik.http.routers.prom.entrypoints=websecure
- traefik.http.routers.prom.tls.certresolver=le
- traefik.http.routers.prom.middlewares=crowdsec,authelia,security-headers
- traefik.http.services.prom.loadbalancer.server.port=9090
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
networks: [monitoring]
devices:
- /dev/kmsg:/dev/kmsg
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
networks: [monitoring]
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command: ["--path.rootfs=/rootfs"]
grafana:
image: grafana/grafana-oss:latest
container_name: grafana
restart: unless-stopped
networks: [traefik_proxy, monitoring]
environment:
GF_SECURITY_ADMIN_USER: "${GRAFANA_ADMIN_USER}"
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASS}"
GF_SERVER_ROOT_URL: https://grafana.gate.${DOMAIN}
TZ: "${TZ}"
volumes:
- grafana_data:/var/lib/grafana
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=crowdsec,authelia,security-headers
- traefik.http.services.grafana.loadbalancer.server.port=3000

16
prometheus/prometheus.yml Normal file
View File

@@ -0,0 +1,16 @@
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['prometheus:9090']
- job_name: 'traefik'
metrics_path: /metrics
static_configs:
- targets: ['traefik:8080']
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']