diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ea330a6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.next +.git +.env +.env.local +data +docs/screenshots +*.log +test-results +playwright-report diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ee84087 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,71 @@ +# ============================================================================= +# Flimmer — Produktionsimage +# +# Drei Stufen, damit im Endbild weder Quellcode noch Build-Werkzeuge landen: +# deps — Abhängigkeiten installieren (gut cachebar) +# builder — Prisma-Client erzeugen und Next bauen +# runner — nur das Nötige: standalone-Server, statische Dateien, Prisma-CLI +# ============================================================================= + +FROM node:24-alpine AS base +# Nachbau der glibc-Aufrufe, die Prisma-Engines und Next erwarten +RUN apk add --no-cache libc6-compat +ENV NEXT_TELEMETRY_DISABLED=1 + +# --- Abhängigkeiten ---------------------------------------------------------- +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --no-audit --no-fund + +# --- Build ------------------------------------------------------------------- +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Beim Bauen wird keine echte Datenbank angefasst; die Platzhalter verhindern +# nur, dass die Konfigurationsprüfung mangels Werten abbricht. +ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public" +ENV AUTH_SECRET="build-time-placeholder-nicht-verwenden" +ENV NODE_ENV=production + +RUN npx prisma generate && npx next build + +# --- Laufzeit ---------------------------------------------------------------- +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +ENV DATA_DIR=/app/data + +RUN addgroup -g 1001 -S nodejs \ + && adduser -S -u 1001 -G nodejs flimmer + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=flimmer:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=flimmer:nodejs /app/.next/static ./.next/static + +# Prisma-CLI und Schema bleiben im Image, damit der Container beim Start seine +# eigenen Migrationen ausspielen kann (`prisma migrate deploy`). +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/prisma.config.mjs ./prisma.config.mjs +COPY --from=builder /app/node_modules/prisma ./node_modules/prisma +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma + +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh \ + && mkdir -p /app/data/images \ + && chown -R flimmer:nodejs /app/data + +USER flimmer +EXPOSE 3000 +VOLUME ["/app/data"] + +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..41f4ae7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,82 @@ +# ============================================================================= +# Flimmer — Selbsthosting mit Docker Compose +# +# cp .env.example .env # anpassen, vor allem Passwort und AUTH_SECRET +# docker compose up -d +# +# Danach läuft Flimmer auf http://localhost:3000. Für den Betrieb hinter einem +# Reverse Proxy (Caddy, Traefik, nginx) APP_URL auf die öffentliche Adresse +# setzen und /api/health als Healthcheck eintragen. +# ============================================================================= + +services: + app: + # Zum Selberbauen: `docker compose build`. Wer ein fertiges Image nutzt, + # ersetzt `build` durch `image: …`. + build: + context: . + dockerfile: Dockerfile + image: flimmer:latest + container_name: flimmer + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-flimmer}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in der .env}@db:5432/${POSTGRES_DB:-flimmer}?schema=public + APP_URL: ${APP_URL:-http://localhost:3000} + APP_NAME: ${APP_NAME:-Flimmer} + AUTH_SECRET: ${AUTH_SECRET:?AUTH_SECRET fehlt in der .env} + DATA_DIR: /app/data + TZ: ${TZ:-Europe/Berlin} + ALLOW_REGISTRATION: ${ALLOW_REGISTRATION:-true} + + TMDB_API_KEY: ${TMDB_API_KEY:-} + TMDB_ACCESS_TOKEN: ${TMDB_ACCESS_TOKEN:-} + TMDB_LANGUAGE: ${TMDB_LANGUAGE:-de-DE} + TMDB_REGION: ${TMDB_REGION:-DE} + + ENABLE_SCHEDULER: ${ENABLE_SCHEDULER:-true} + SYNC_HOUR: ${SYNC_HOUR:-4} + CRON_SECRET: ${CRON_SECRET:-} + + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_SECURE: ${SMTP_SECURE:-false} + SMTP_FROM: ${SMTP_FROM:-Flimmer } + + OIDC_ISSUER: ${OIDC_ISSUER:-} + OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-} + OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-} + OIDC_SCOPES: ${OIDC_SCOPES:-openid profile email} + OIDC_BUTTON_LABEL: ${OIDC_BUTTON_LABEL:-Mit SSO anmelden} + OIDC_AUTO_REGISTER: ${OIDC_AUTO_REGISTER:-true} + ports: + - "${APP_PORT:-3000}:3000" + volumes: + # Bild-Cache und sonstige Laufzeitdaten + - flimmer-data:/app/data + + db: + image: postgres:17-alpine + container_name: flimmer-db + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-flimmer} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in der .env} + POSTGRES_DB: ${POSTGRES_DB:-flimmer} + TZ: ${TZ:-Europe/Berlin} + volumes: + - flimmer-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-flimmer} -d ${POSTGRES_DB:-flimmer}"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + +volumes: + flimmer-data: + flimmer-db: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..83d3516 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# Startskript des Flimmer-Containers. +# +# Vor dem Start der App werden ausstehende Datenbankmigrationen ausgespielt. +# Das ist bewusst hier und nicht im Image-Build: die Datenbank existiert erst +# zur Laufzeit, und beim Update soll das Schema automatisch nachziehen. +set -e + +if [ -z "$DATABASE_URL" ]; then + echo "Flimmer: DATABASE_URL ist nicht gesetzt — Abbruch." >&2 + exit 1 +fi + +# Warten, bis der Datenbank-Port annimmt. Bewusst nur eine TCP-Verbindung über +# Node-Bordmittel — so hängt der Start nicht an einem zusätzlichen Paket. +echo "Flimmer: warte auf die Datenbank ..." +node -e ' +const net = require("node:net"); +const url = new URL(process.env.DATABASE_URL); +const host = url.hostname; +const port = Number(url.port || 5432); +const deadline = Date.now() + 120000; + +function attempt() { + const socket = net.connect({ host, port }); + socket.setTimeout(3000); + socket.on("connect", () => { socket.end(); process.exit(0); }); + const retry = () => { + socket.destroy(); + if (Date.now() > deadline) { + console.error("Flimmer: Datenbank " + host + ":" + port + " nach 120 s nicht erreichbar."); + process.exit(1); + } + setTimeout(attempt, 2000); + }; + socket.on("error", retry); + socket.on("timeout", retry); +} +attempt(); +' + +echo "Flimmer: spiele Migrationen ein ..." +node node_modules/prisma/build/index.js migrate deploy + +echo "Flimmer: starte Anwendung." +exec "$@"