ci: Auto-deploy on push via SSH

- Gitea Actions workflow now syncs repo to server, builds images, restarts containers, and runs health checks
- Removed all hardcoded secrets from scripts/deploy.sh
- Added CI/CD documentation and ignored .env.deploy

NOTE: Existing secrets previously committed must be rotated.
This commit is contained in:
2025-12-21 15:23:04 +01:00
parent 13334f6cdd
commit d170d6f729
4 changed files with 253 additions and 46 deletions

View File

@ -1,22 +1,194 @@
name: Pounce CI
name: Deploy Pounce (Auto)
on:
push:
branches: [main]
jobs:
info:
deploy:
runs-on: ubuntu-latest
steps:
- name: Build Info
- name: Checkout
uses: actions/checkout@v4
- name: Install deploy tooling
run: |
apt-get update
apt-get install -y --no-install-recommends openssh-client rsync ca-certificates
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
- name: Sync repository to server
run: |
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes" \
--exclude ".git" \
--exclude "frontend/node_modules" \
--exclude "frontend/.next" \
--exclude "**/__pycache__" \
--exclude "**/*.pyc" \
./ \
"${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/"
- name: Deploy (build + restart + health check)
env:
DEPLOY_SUDO_PASSWORD: ${{ secrets.DEPLOY_SUDO_PASSWORD }}
# App secrets (used to generate backend env file on server)
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GH_OAUTH_SECRET: ${{ secrets.GH_OAUTH_SECRET }}
CZDS_USERNAME: ${{ secrets.CZDS_USERNAME }}
CZDS_PASSWORD: ${{ secrets.CZDS_PASSWORD }}
run: |
ssh -i ~/.ssh/deploy_key "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" << 'DEPLOY_EOF'
set -euo pipefail
# Use sudo non-interactively (password supplied via env)
sudo_cmd() {
printf '%s\n' "$DEPLOY_SUDO_PASSWORD" | sudo -S "$@"
}
# Ensure dirs
sudo_cmd mkdir -p /data/pounce/env /data/pounce/zones
sudo_cmd chmod -R 755 /data/pounce || true
# Generate backend env file from pipeline-provided secrets (never echo values)
sudo_cmd python3 - <<'PY'
import os
from pathlib import Path
env = {
# Core
"ENVIRONMENT": "production",
"ENABLE_SCHEDULER": "true",
"COOKIE_SECURE": "true",
"CORS_ORIGINS": "https://pounce.ch,https://www.pounce.ch",
"SITE_URL": "https://pounce.ch",
"FRONTEND_URL": "https://pounce.ch",
# Data dirs
"CZDS_DATA_DIR": "/data/czds",
"SWITCH_DATA_DIR": "/data/switch",
"ZONE_RETENTION_DAYS": "3",
# DB/Redis
"DATABASE_URL": os.environ["DATABASE_URL"],
"REDIS_URL": "redis://pounce-redis:6379/0",
# Auth
"SECRET_KEY": os.environ["SECRET_KEY"],
"JWT_SECRET": os.environ["SECRET_KEY"],
# SMTP
"SMTP_HOST": "smtp.zoho.eu",
"SMTP_PORT": "465",
"SMTP_USER": "hello@pounce.ch",
"SMTP_PASSWORD": os.environ["SMTP_PASSWORD"],
"SMTP_FROM_EMAIL": "hello@pounce.ch",
"SMTP_FROM_NAME": "pounce",
"SMTP_USE_TLS": "false",
"SMTP_USE_SSL": "true",
# Stripe
"STRIPE_SECRET_KEY": os.environ["STRIPE_SECRET_KEY"],
"STRIPE_PUBLISHABLE_KEY": "pk_live_51ScLbjCtFUamNRpNeFugrlTIYhszbo8GovSGiMnPwHpZX9p3SGtgG8iRHYRIlAtg9M9sl3mvT5r8pwXP3mOsPALG00Wk3j0wH4",
"STRIPE_PRICE_TRADER": "price_1ScRlzCtFUamNRpNQdMpMzxV",
"STRIPE_PRICE_TYCOON": "price_1SdwhSCtFUamNRpNEXTSuGUc",
"STRIPE_WEBHOOK_SECRET": os.environ["STRIPE_WEBHOOK_SECRET"],
# OAuth
"GOOGLE_CLIENT_ID": "865146315769-vi7vcu91d3i7huv8ikjun52jo9ob7spk.apps.googleusercontent.com",
"GOOGLE_CLIENT_SECRET": os.environ["GOOGLE_CLIENT_SECRET"],
"GOOGLE_REDIRECT_URI": "https://pounce.ch/api/v1/oauth/google/callback",
"GITHUB_CLIENT_ID": "Ov23liBjROk39vYXi3G5",
"GITHUB_CLIENT_SECRET": os.environ["GH_OAUTH_SECRET"],
"GITHUB_REDIRECT_URI": "https://pounce.ch/api/v1/oauth/github/callback",
# CZDS
"CZDS_USERNAME": os.environ["CZDS_USERNAME"],
"CZDS_PASSWORD": os.environ["CZDS_PASSWORD"],
}
lines = []
for k, v in env.items():
if v is None:
continue
lines.append(f"{k}={v}")
path = Path("/data/pounce/env/backend.env")
path.write_text("\n".join(lines) + "\n")
PY
# Build images from synced repo
cd "${{ secrets.DEPLOY_PATH }}"
sudo_cmd docker build -t pounce-backend:latest backend
sudo_cmd docker build \
--build-arg NEXT_PUBLIC_API_URL=https://api.pounce.ch \
--build-arg BACKEND_URL=http://pounce-backend:8000 \
-t pounce-frontend:latest \
frontend
# Deploy backend
sudo_cmd docker stop pounce-backend 2>/dev/null || true
sudo_cmd docker rm pounce-backend 2>/dev/null || true
sudo_cmd docker run -d \
--name pounce-backend \
--network coolify \
--restart unless-stopped \
--shm-size=8g \
--env-file /data/pounce/env/backend.env \
-v /data/pounce/zones:/data \
-l "traefik.enable=true" \
-l "traefik.http.routers.pounce-api.rule=Host(\`api.pounce.ch\`)" \
-l "traefik.http.routers.pounce-api.entryPoints=https" \
-l "traefik.http.routers.pounce-api.tls=true" \
-l "traefik.http.routers.pounce-api.tls.certresolver=letsencrypt" \
-l "traefik.http.services.pounce-api.loadbalancer.server.port=8000" \
pounce-backend:latest
sudo_cmd docker network connect n0488s44osgoow4wgo04ogg0 pounce-backend 2>/dev/null || true
# Deploy frontend
sudo_cmd docker stop pounce-frontend 2>/dev/null || true
sudo_cmd docker rm pounce-frontend 2>/dev/null || true
sudo_cmd docker run -d \
--name pounce-frontend \
--network coolify \
--restart unless-stopped \
-l "traefik.enable=true" \
-l "traefik.http.routers.pounce-web.rule=Host(\`pounce.ch\`) || Host(\`www.pounce.ch\`)" \
-l "traefik.http.routers.pounce-web.entryPoints=https" \
-l "traefik.http.routers.pounce-web.tls=true" \
-l "traefik.http.routers.pounce-web.tls.certresolver=letsencrypt" \
-l "traefik.http.services.pounce-web.loadbalancer.server.port=3000" \
pounce-frontend:latest
sudo_cmd docker network connect n0488s44osgoow4wgo04ogg0 pounce-frontend 2>/dev/null || true
# Health check
sleep 15
curl -sf https://api.pounce.ch/api/v1/health >/dev/null
curl -sf https://pounce.ch >/dev/null
# Cleanup
sudo_cmd docker image prune -f >/dev/null 2>&1 || true
echo "✅ Deploy finished"
DEPLOY_EOF
- name: Summary
run: |
echo "=========================================="
echo "POUNCE BUILD INFO"
echo "🎉 AUTO DEPLOY COMPLETED"
echo "=========================================="
echo "Commit: ${{ github.sha }}"
echo "Branch: ${{ github.ref_name }}"
echo "Time: $(date)"
echo ""
echo "To deploy, run locally:"
echo " ./scripts/deploy.sh"
echo "Backend: https://api.pounce.ch"
echo "Web: https://pounce.ch"
echo "=========================================="

1
.gitignore vendored
View File

@ -26,6 +26,7 @@ dist/
.env
.env.local
.env.*.local
.env.deploy
*.log
# Deployment env files (MUST NOT be committed)

42
ops/CI_CD.md Normal file
View File

@ -0,0 +1,42 @@
# CI/CD (Gitea Actions) Auto Deploy
## Goal
Every push to `main` should:
- sync the repo to the production server
- build Docker images on the server
- restart containers
- run health checks
This repository uses a **remote SSH deployment** from Gitea Actions.
## Required Gitea Actions Secrets
Configure these in Gitea: **Repo → Settings → Actions → Secrets**
### Deployment (SSH)
- `DEPLOY_HOST` production server IP/hostname
- `DEPLOY_USER` SSH user (e.g. `administrator`)
- `DEPLOY_PATH` absolute path where the repo is synced on the server (e.g. `/home/administrator/pounce`)
- `DEPLOY_SSH_KEY` private key for SSH access
- `DEPLOY_SUDO_PASSWORD` sudo password for `DEPLOY_USER` (used non-interactively)
### App Secrets (Backend)
Used to generate `/data/pounce/env/backend.env` on the server.
- `DATABASE_URL`
- `SECRET_KEY`
- `SMTP_PASSWORD`
- `STRIPE_SECRET_KEY`
- `STRIPE_WEBHOOK_SECRET`
- `GOOGLE_CLIENT_SECRET`
- `GH_OAUTH_SECRET`
- `CZDS_USERNAME`
- `CZDS_PASSWORD`
## Server Requirements
- `sudo` installed
- `docker` installed
- `DEPLOY_USER` must be able to run docker via `sudo` (pipeline uses `sudo -S docker ...`)
## Notes
- Secrets are written to `/data/pounce/env/backend.env` on the server with restricted permissions.
- Frontend build args are supplied in the workflow (`NEXT_PUBLIC_API_URL`, `BACKEND_URL`).

View File

@ -17,6 +17,8 @@ SERVER="185.142.213.170"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/pounce_server}"
SSH_USER="administrator"
REMOTE_TMP="/tmp/pounce"
REMOTE_REPO="/home/administrator/pounce"
REMOTE_ENV_DIR="/data/pounce/env"
# Colors
RED='\033[0;31m'
@ -33,6 +35,10 @@ if [ ! -f "$SSH_KEY" ]; then
error "SSH key not found: $SSH_KEY"
fi
if [ -z "${DEPLOY_SUDO_PASSWORD:-}" ]; then
error "DEPLOY_SUDO_PASSWORD is required (export it locally, do not commit it)."
fi
# What to deploy
DEPLOY_BACKEND=true
DEPLOY_FRONTEND=true
@ -57,15 +63,26 @@ if [ "$DEPLOY_BACKEND" = true ]; then
--exclude '.git' \
--exclude 'venv' \
backend/ \
${SSH_USER}@${SERVER}:${REMOTE_TMP}-backend/
${SSH_USER}@${SERVER}:${REMOTE_REPO}/backend/
log "Building backend image..."
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} \
"echo 'u4R6tgCv*c8Fyc1ee' | sudo -S docker build -t pounce-backend:latest ${REMOTE_TMP}-backend/" || error "Backend build failed"
"printf '%s\n' \"${DEPLOY_SUDO_PASSWORD}\" | sudo -S docker build -t pounce-backend:latest ${REMOTE_REPO}/backend/" || error "Backend build failed"
log "Deploying backend container..."
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} << 'BACKEND_DEPLOY'
echo 'u4R6tgCv*c8Fyc1ee' | sudo -S bash -c '
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} << BACKEND_DEPLOY
printf '%s\n' "${DEPLOY_SUDO_PASSWORD}" | sudo -S bash -c '
set -e
mkdir -p "${REMOTE_ENV_DIR}" /data/pounce/zones
chmod -R 755 /data/pounce || true
# Backend env must exist on server (created by CI or manually)
if [ ! -f "${REMOTE_ENV_DIR}/backend.env" ]; then
echo "Missing ${REMOTE_ENV_DIR}/backend.env"
exit 1
fi
docker stop pounce-backend 2>/dev/null || true
docker rm pounce-backend 2>/dev/null || true
@ -73,33 +90,7 @@ docker run -d \
--name pounce-backend \
--network coolify \
--shm-size=8g \
-e DATABASE_URL="postgresql+asyncpg://pounce:PounceDB2024!@supabase-db-n0488s44osgoow4wgo04ogg0:5432/pounce" \
-e REDIS_URL="redis://pounce-redis:6379" \
-e ENABLE_SCHEDULER="true" \
-e SECRET_KEY="super-secret-key-change-me-in-production-please" \
-e ENVIRONMENT="production" \
-e CORS_ORIGINS="https://pounce.ch,https://www.pounce.ch" \
-e COOKIE_SECURE="true" \
-e SITE_URL="https://pounce.ch" \
-e CZDS_DATA_DIR="/data/czds" \
-e CZDS_USERNAME="Gugger99@gmx.ch" \
-e CZDS_PASSWORD="Icann@2024!" \
-e SMTP_HOST="mail.infomaniak.com" \
-e SMTP_PORT="587" \
-e SMTP_USER="hello@pounce.ch" \
-e SMTP_PASSWORD="xVP4x#q1s78C" \
-e SMTP_FROM_EMAIL="hello@pounce.ch" \
-e STRIPE_SECRET_KEY="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" \
-e STRIPE_PUBLISHABLE_KEY="pk_live_51ScLbjCtFUamNRpNeFugrlTIYhszbo8GovSGiMnPwHpZX9p3SGtgG8iRHYRIlAtg9M9sl3mvT5r8pwXP3mOsPALG00Wk3j0wH4" \
-e STRIPE_PRICE_TRADER="price_1ScRlzCtFUamNRpNQdMpMzxV" \
-e STRIPE_PRICE_TYCOON="price_1SdwhSCtFUamNRpNEXTSuGUc" \
-e STRIPE_WEBHOOK_SECRET="whsec_DlWSVkIJDDDkjfj29fjJFkdj2Ksldk" \
-e GOOGLE_CLIENT_ID="865146315769-vi7vcu91d3i7huv8ikjun52jo9ob7spk.apps.googleusercontent.com" \
-e GOOGLE_CLIENT_SECRET="" \
-e GOOGLE_REDIRECT_URI="https://pounce.ch/api/v1/oauth/google/callback" \
-e GITHUB_CLIENT_ID="Ov23liBjROk39vYXi3G5" \
-e GITHUB_CLIENT_SECRET="" \
-e GITHUB_REDIRECT_URI="https://pounce.ch/api/v1/oauth/github/callback" \
--env-file "${REMOTE_ENV_DIR}/backend.env" \
-v /data/pounce/zones:/data \
--label "traefik.enable=true" \
--label "traefik.http.routers.pounce-backend.rule=Host(\`api.pounce.ch\`)" \
@ -127,15 +118,16 @@ if [ "$DEPLOY_FRONTEND" = true ]; then
--exclude '.next' \
--exclude '.git' \
frontend/ \
${SSH_USER}@${SERVER}:${REMOTE_TMP}-frontend/
${SSH_USER}@${SERVER}:${REMOTE_REPO}/frontend/
log "Building frontend image..."
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} \
"echo 'u4R6tgCv*c8Fyc1ee' | sudo -S docker build --build-arg NEXT_PUBLIC_API_URL=https://api.pounce.ch --build-arg BACKEND_URL=http://pounce-backend:8000 -t pounce-frontend:latest ${REMOTE_TMP}-frontend/" || error "Frontend build failed"
"printf '%s\n' \"${DEPLOY_SUDO_PASSWORD}\" | sudo -S docker build --build-arg NEXT_PUBLIC_API_URL=https://api.pounce.ch --build-arg BACKEND_URL=http://pounce-backend:8000 -t pounce-frontend:latest ${REMOTE_REPO}/frontend/" || error "Frontend build failed"
log "Deploying frontend container..."
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} << 'FRONTEND_DEPLOY'
echo 'u4R6tgCv*c8Fyc1ee' | sudo -S bash -c '
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} << FRONTEND_DEPLOY
printf '%s\n' "${DEPLOY_SUDO_PASSWORD}" | sudo -S bash -c '
set -e
docker stop pounce-frontend 2>/dev/null || true
docker rm pounce-frontend 2>/dev/null || true
@ -166,7 +158,7 @@ curl -sf https://pounce.ch -o /dev/null && log "Frontend: ✅ Healthy"
# Cleanup
log "Cleaning up..."
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} \
"echo 'u4R6tgCv*c8Fyc1ee' | sudo -S docker image prune -f" > /dev/null 2>&1
"printf '%s\n' \"${DEPLOY_SUDO_PASSWORD}\" | sudo -S docker image prune -f" > /dev/null 2>&1
log "=========================================="
log "🎉 DEPLOYMENT SUCCESSFUL!"