# .github/workflows/deploy.yml name: Deploy Gateway to VPS on: push: branches: [ "main" ] 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: 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: Reset any local changes on VPS 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" # Force reset any local changes and pull latest if [ -d ".git" ]; then echo "Git repository detected, resetting and pulling..." git reset --hard HEAD git clean -fd git pull origin main else echo "No git repository found, creating backup of modified files..." # Backup any existing files that might conflict [ -f "docker-compose.yml" ] && cp docker-compose.yml docker-compose.yml.backup.$(date +%s) || true fi EOF - name: Sync repo to VPS (rsync) run: | rsync -az --delete \ -e "ssh -p ${{ secrets.SSH_PORT }}" \ --exclude ".git" \ --exclude ".github" \ --exclude ".env" \ ./ "${{ 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 }} # --- Basic Authentication --- BASIC_AUTH_USERS=${{ secrets.BASIC_AUTH_USERS }} # --- 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 }}" \ "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 "Umami: https://umami.gate.${{ secrets.DOMAIN }}"