# .github/workflows/deploy.yml name: Deploy Gateway to VPS on: push: branches: [ "main" ] paths: - "docker-compose.yml" - "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 }}" \ "export REMOTE_DIR='${{ secrets.REMOTE_DIR }}'; 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 }} # --- Authelia --- AUTHELIA_JWT_SECRET=${{ secrets.AUTHELIA_JWT_SECRET }} AUTHELIA_SESSION_SECRET=${{ secrets.AUTHELIA_SESSION_SECRET }} AUTHELIA_STORAGE_ENCRYPTION_KEY=${{ secrets.AUTHELIA_STORAGE_ENCRYPTION_KEY }} # (Optional SMTP if configured) AUTHELIA_SMTP_HOST=${{ secrets.AUTHELIA_SMTP_HOST }} AUTHELIA_SMTP_PORT=${{ secrets.AUTHELIA_SMTP_PORT }} AUTHELIA_SMTP_USER=${{ secrets.AUTHELIA_SMTP_USER }} AUTHELIA_SMTP_PASS=${{ secrets.AUTHELIA_SMTP_PASS }} AUTHELIA_SMTP_FROM=${{ secrets.AUTHELIA_SMTP_FROM }} 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 }}" \ "export REMOTE_DIR='${{ secrets.REMOTE_DIR }}'; 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 "Uptime Kuma: https://uptime.gate.${{ secrets.DOMAIN }}" echo "Authelia: https://auth.gate.${{ secrets.DOMAIN }}" echo "Grafana: https://grafana.gate.${{ secrets.DOMAIN }}" echo "Prometheus: https://prometheus.gate.${{ secrets.DOMAIN }}" echo "Umami: https://umami.gate.${{ secrets.DOMAIN }}"