From e9a5eef05143f6a40a909dd52fee2c7e95cc309a Mon Sep 17 00:00:00 2001 From: Alexandre CHAZAL Date: Sun, 26 Jul 2026 17:37:23 +0200 Subject: [PATCH 1/2] feat(docker): containerise the setup --- .dockerignore | 9 +++++++++ .env.docker.example | 12 ++++++++++++ .gitignore | 1 + Dockerfile | 28 ++++++++++++++++++++++++++++ docker-compose.yml | 22 ++++++++++++++++++++++ next.config.ts | 7 ++++++- 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .env.docker.example create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..35abbaf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +.next +.git +.env* +!.env.local.example +!.env.docker.example +docker +supabase +*.md diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..c3a6a40 --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,12 @@ +# Le Tribunal — Docker deployment with managed Supabase + +# --- Domain --- +APP_DOMAIN=acropole.alxczl.fr + +# --- Traefik --- +TRAEFIK_NETWORK=proxy + +# --- Supabase (managed) --- +# From your Supabase project: Settings -> API +SUPABASE_URL= +SUPABASE_ANON_KEY= diff --git a/.gitignore b/.gitignore index b721bff..a0b0be5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* !.env.local.example +!.env.docker.example # vercel .vercel diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4658b3a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:22-alpine AS base +WORKDIR /app + +FROM base AS deps +COPY package.json package-lock.json ./ +RUN npm ci + +FROM base AS build +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ARG NEXT_PUBLIC_SUPABASE_URL +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY +ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL +ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY +RUN npm run build + +FROM base AS runner +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs +COPY --from=build /app/public ./public +COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..60ad884 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + app: + build: + context: . + args: + NEXT_PUBLIC_SUPABASE_URL: ${SUPABASE_URL} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY} + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.http.routers.tribunal.rule=Host(`${APP_DOMAIN}`)" + - "traefik.http.routers.tribunal.entrypoints=https" + - "traefik.http.routers.tribunal.tls.certresolver=letsencrypt" + - "traefik.http.routers.tribunal.middlewares=compression@file,hsts-headers@file" + - "traefik.http.services.tribunal.loadbalancer.server.port=3000" + networks: + - proxy + +networks: + proxy: + external: true + name: ${TRAEFIK_NETWORK} diff --git a/next.config.ts b/next.config.ts index 6e35e0d..3e93ab6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,11 +1,16 @@ import type { NextConfig } from "next"; +const supabaseHostname = process.env.NEXT_PUBLIC_SUPABASE_URL + ? new URL(process.env.NEXT_PUBLIC_SUPABASE_URL).hostname + : "localhost"; + const nextConfig: NextConfig = { + output: "standalone", images: { remotePatterns: [ { protocol: "https", - hostname: "kuichrzrbdwydixbdfon.supabase.co", + hostname: supabaseHostname, pathname: "/storage/v1/object/public/**", }, ], From eff6f4842575d5981825f5ff78bbecd582d3253f Mon Sep 17 00:00:00 2001 From: Alexandre CHAZAL Date: Sun, 26 Jul 2026 18:07:49 +0200 Subject: [PATCH 2/2] feat(docker): add webhook to auto-redeploy on master commit, remove old docker-related files --- .env.docker.example | 9 ++++----- .gitea/workflows/deploy.yml | 35 +++++++++++++++++++++++++++++++++++ docker-compose.yml | 36 +++++++++++++++++++++++++++++++----- docker/entrypoint.sh | 13 +++++++++++++ docker/webhook/Dockerfile | 3 +++ docker/webhook/receive.py | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 .gitea/workflows/deploy.yml create mode 100755 docker/entrypoint.sh create mode 100644 docker/webhook/Dockerfile create mode 100644 docker/webhook/receive.py 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()