diff --git a/.env.docker.example b/.env.docker.example index c3a6a40..3403f18 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -1,4 +1,4 @@ -# Le Tribunal — Docker deployment with managed Supabase +# Le Tribunal — Docker deployment # --- Domain --- APP_DOMAIN=acropole.alxczl.fr @@ -6,7 +6,6 @@ APP_DOMAIN=acropole.alxczl.fr # --- Traefik --- TRAEFIK_NETWORK=proxy -# --- Supabase (managed) --- -# From your Supabase project: Settings -> API -SUPABASE_URL= -SUPABASE_ANON_KEY= +# --- Webhook (auto-deploy on push) --- +# openssl rand -hex 16 +WEBHOOK_SECRET= diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..777afd4 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,35 @@ +name: Build and deploy +run-name: ${{ gitea.actor }} is deploying Le Tribunal +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - run: npm ci + + - name: Build + run: | + npm run build + cp -r .next/static .next/standalone/.next/static + cp -r public .next/standalone/public + env: + NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} + + - name: Deploy + run: | + tar -czf - -C .next/standalone . | \ + curl -sf \ + -H "X-Webhook-Secret: ${{ secrets.WEBHOOK_SECRET }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @- \ + "${{ secrets.DEPLOY_WEBHOOK }}" diff --git a/docker-compose.yml b/docker-compose.yml index 60ad884..6136d24 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,16 @@ services: app: - build: - context: . - args: - NEXT_PUBLIC_SUPABASE_URL: ${SUPABASE_URL} - NEXT_PUBLIC_SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY} + image: node:22-alpine restart: unless-stopped + working_dir: /app + command: ["/entrypoint.sh"] + environment: + NODE_ENV: production + PORT: 3000 + HOSTNAME: 0.0.0.0 + volumes: + - app-data:/app + - ./docker/entrypoint.sh:/entrypoint.sh:ro labels: - "traefik.enable=true" - "traefik.http.routers.tribunal.rule=Host(`${APP_DOMAIN}`)" @@ -16,6 +21,27 @@ services: networks: - proxy + webhook: + build: ./docker/webhook + restart: unless-stopped + environment: + WEBHOOK_SECRET: ${WEBHOOK_SECRET} + volumes: + - app-data:/data + labels: + - "traefik.enable=true" + - "traefik.http.routers.tribunal-webhook.rule=Host(`containers01.lan.alxczl.fr`) && PathPrefix(`/tribunal-webhook`)" + - "traefik.http.routers.tribunal-webhook.tls.certresolver=voidca" + - "traefik.http.routers.tribunal-webhook.entrypoints=https" + - "traefik.http.routers.tribunal-webhook.middlewares=hsts-headers@file,tribunal-webhook-strip" + - "traefik.http.middlewares.tribunal-webhook-strip.stripprefix.prefixes=/tribunal-webhook" + - "traefik.http.services.tribunal-webhook.loadbalancer.server.port=9000" + networks: + - proxy + +volumes: + app-data: + networks: proxy: external: true diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..fa65d64 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh +while [ ! -f /app/server.js ]; do sleep 2; done + +while true; do + STAMP=$(cat /app/.deploy-timestamp 2>/dev/null) + node /app/server.js & + PID=$! + while [ "$(cat /app/.deploy-timestamp 2>/dev/null)" = "$STAMP" ]; do + sleep 2 + done + kill $PID + wait $PID 2>/dev/null +done diff --git a/docker/webhook/Dockerfile b/docker/webhook/Dockerfile new file mode 100644 index 0000000..0536669 --- /dev/null +++ b/docker/webhook/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3-alpine +COPY receive.py /receive.py +CMD ["python", "/receive.py"] diff --git a/docker/webhook/receive.py b/docker/webhook/receive.py new file mode 100644 index 0000000..d053d47 --- /dev/null +++ b/docker/webhook/receive.py @@ -0,0 +1,34 @@ +from http.server import HTTPServer, BaseHTTPRequestHandler +import os, tarfile, io, shutil, time + +SECRET = os.environ["WEBHOOK_SECRET"] + + +class Handler(BaseHTTPRequestHandler): + def do_POST(self): + if self.headers.get("X-Webhook-Secret") != SECRET: + self.send_response(401) + self.end_headers() + return + + body = self.rfile.read(int(self.headers["Content-Length"])) + + for item in os.listdir("/data"): + path = os.path.join("/data", item) + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + + with tarfile.open(fileobj=io.BytesIO(body), mode="r:gz") as tar: + tar.extractall("/data", filter="data") + + with open("/data/.deploy-timestamp", "w") as f: + f.write(str(time.time())) + + self.send_response(200) + self.end_headers() + self.wfile.write(b"OK") + + +HTTPServer(("0.0.0.0", 9000), Handler).serve_forever()