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..3403f18 --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,11 @@ +# Le Tribunal — Docker deployment + +# --- Domain --- +APP_DOMAIN=acropole.alxczl.fr + +# --- Traefik --- +TRAEFIK_NETWORK=proxy + +# --- 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/.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..6136d24 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +services: + app: + 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}`)" + - "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 + + 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 + name: ${TRAEFIK_NETWORK} 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() 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/**", }, ],