diff --git a/ROADMAP.md b/ROADMAP.md index e10e825..9f5890a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -178,10 +178,37 @@ es weiterhin keine API. ### ⬜ Als Nächstes -**2 · Dockerfile und Compose.** Ein Abbild, das Server und gebaute Oberfläche -ausliefert; ein Volume für `data/`; ein Port für NPM. In Portainer aus diesem -Repo deploybar — **`Compose path` nimmt EINE Datei**, Ergänzungen werden -stillschweigend ignoriert. Das hat uns bei Lite einen Nachmittag gekostet. +**2 · Dockerfile und Compose — geschrieben, aber NIE GEBAUT.** +`server/Dockerfile` und `docker-compose.media.yml` stehen. Auf der Maschine, +auf der sie entstanden sind, gibt es weder Docker noch WSL — das Abbild ist +also ungebaut und der Container nie gelaufen. Das ist der nächste Handgriff, +und er gehört auf eine Maschine mit Docker. + +Was daran trotzdem geprüft ist: + +- **Node 22 ist festgenagelt, mit Grund.** `better-sqlite3` 11.10.0 liefert + fertige Binärdateien nur für ABI 108 (Node 20), 115, 127 (Node 22) und 131. + **Node 24 hat ABI 137 und fehlt** — der naheliegende Griff zur neuesten + Version fällt auf `node-gyp` zurück und braucht python3, make und g++ im + Abbild. Aus den Veröffentlichungen des Projekts abgelesen, nicht geraten. +- **`node dist/index.js`** — der Startbefehl des Abbilds — läuft und antwortet + auf `/health`. Bis dahin war der Dienst nur je über `tsx` gestartet. +- **Der Healthcheck** gibt Exitcode 0 bei laufendem und 1 bei totem Dienst. + +Ungeprüft bleibt alles, wofür es Docker braucht: ob `npm ci` im Abbild die +Binärdatei zieht, ob das Volume greift, ob der Bau überhaupt durchläuft. + +> **Beim Ausprobieren:** `NODE_ENV=production` setzt das Sitzungs-Cookie auf +> `Secure`. Wer den Container ohne HTTPS aufruft, bekommt auf die Anmeldung +> **200 — und ist trotzdem nicht angemeldet**, weil der Browser das Cookie +> still verwirft; die nächste Anfrage ist 401. Im Browser gegen eine +> LAN-Adresse nachgemessen. Die naheliegende Gegenprobe über `localhost` +> führt in die Irre: das gilt Browsern als sicherer Kontext und funktioniert. + +In Portainer aus diesem Repo deploybar — **`Compose path` nimmt EINE Datei**, +Ergänzungen werden stillschweigend ignoriert. Das hat uns bei Lite einen +Nachmittag gekostet. Host-Port ist 9102, solange der Lite-Stack 9100/9101 +belegt. **3 · `server/upload.lua` im Fotostudio.** `Upload.put(pfad, bytes) → url`. Token in `config.upload.lua`, server-only, gitignored, mit committetem diff --git a/docker-compose.media.yml b/docker-compose.media.yml new file mode 100644 index 0000000..4debd6b --- /dev/null +++ b/docker-compose.media.yml @@ -0,0 +1,94 @@ +# d4rk_media — unser eigener Medien-Dienst. EIN Container. +# +# In Portainer: Stacks -> Add stack -> Repository +# +# Repository URL https://git.d4rkst3r.de/D4rkst3r/d4rk_media +# Compose path docker-compose.media.yml +# +# EINE Datei, und das ist wichtig: Portainer nimmt unter "Compose path" genau +# eine. Ergaenzungen werden stillschweigend uebergangen -- alle Container +# gesund, alles 502. Das hat beim Lite-Anlauf einen Nachmittag gekostet. +# +# Die Werte gehoeren in die STACK-VARIABLEN, nicht in eine Datei im Repo. + +services: + media: + build: + context: ./server + image: d4rk-media:latest + container_name: d4rk-media + restart: unless-stopped + + environment: + # Der Ursprung, unter dem der Dienst oeffentlich steht -- OHNE Pfad + # und ohne abschliessenden Schraegstrich: + # https://media.d4rkst3r.de + # + # Das /f/ haengt der Dienst selbst an. Wer es hier mitschreibt, + # bekommt Adressen mit /f/f/, und das faellt erst auf, wenn das + # erste Bild fehlt. + # + # Das :? ist Absicht: fehlt die Variable, bricht der Stack ab, + # statt mit einer Adresse hochzufahren, die niemand erreicht. + PUBLIC_URL: ${PUBLIC_URL:?PUBLIC_URL fehlt, z.B. https://media.d4rkst3r.de} + + # Wird beim ERSTEN Start als Benutzer "admin" angelegt. Ein spaeter + # geaenderter Wert setzt das bestehende Passwort nicht zurueck. + ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD fehlt} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + + # EIN NAME (leer, Standard): Dashboard und API an der Wurzel, + # Dateien unter /f/. + # + # ZWEI NAMEN: hier den Hostnamen eintragen, unter dem es dann + # ausschliesslich Dateien gibt -- kein Dashboard, keine API, nichts + # anzumelden. Die URLs kommen dann ohne /f/ aus, und PUBLIC_URL ist + # der Datei-Name. Beide Namen muessen im Proxy auf diesen Port + # zeigen. Welche Betriebsart laeuft, sagt der Dienst beim Start. + FILES_HOST: ${FILES_HOST:-} + + MAX_UPLOAD_MB: ${MAX_UPLOAD_MB:-64} + SESSION_DAYS: ${SESSION_DAYS:-30} + + # NODE_ENV=production setzt das Sitzungs-Cookie auf "Secure". Hinter + # NPM mit TLS ist das richtig. + # + # WER OHNE HTTPS AUSPROBIERT, SPERRT SICH AUS -- und zwar auf die + # unangenehme Art. Nachgemessen im Browser gegen + # http://192.168.2.33:8096: + # + # POST /api/dash/auth/login -> 200, sieht nach Erfolg aus + # document.cookie -> leer, still verworfen + # GET /api/dash/auth/me -> 401 "nicht angemeldet" + # + # Die Anmeldung MELDET Erfolg und ist trotzdem keine. Genau die + # Sorte Rueckmeldung, wegen der wir hier neu bauen. + # + # Und die naheliegende Gegenprobe fuehrt in die Irre: ueber + # http://localhost geht es, weil Browser localhost als sicheren + # Kontext behandeln. Also entweder ueber den Proxy mit HTTPS + # testen -- oder zum Ausprobieren NODE_ENV weglassen. + NODE_ENV: production + + volumes: + # Bilder UND media.db in einem Volume. Eine Sicherung ist damit + # genau ein Kopiervorgang. + - media-data:/data + + ports: + # Links der Host-Port, rechts der im Container. Nur die LINKE Zahl + # muss auf diesem Server einmalig sein. + # + # Belegt sind 80, 81, 443, 2224, 3000, 3080, 3478, 8000, 8080, + # 8090, 9000, 9443, 11000 -- und 9100/9101, solange der Lite-Stack + # als Vergleichsmassstab weiterlaeuft. Deshalb 9102. + # + # Gebunden an ALLE Adressen, nicht an 127.0.0.1: NPM liegt im + # Standard-Bridge-Netz und kommt ueber die Docker-Bruecke herein, + # nicht ueber Loopback. Ein an 127.0.0.1 gebundener Port naehme die + # Anfrage nicht an. ZU MACHT IHN DIE FIREWALL, nicht die Bindung -- + # 9102 gehoert von aussen dicht, oeffentlich ist nur NPM auf 80/443. + - '${BIND_ADDR:-0.0.0.0}:${HOST_PORT:-9102}:8080' + +volumes: + media-data: diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..a717f2f --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,10 @@ +# Was nicht ins Abbild gehoert. node_modules besonders: die enthalten eine +# native Binaerdatei fuer WINDOWS, und die im Abbild waere unbrauchbar. +node_modules/ +dist/ + +# Die Ablage des Dienstes. Gehoert ins Volume, nie ins Abbild. +data/ + +# Das echte Geheimnis. Die Werte kommen als Stack-Variablen, nicht als Datei. +.env diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..47f7e14 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,68 @@ +# Das Abbild: ein Prozess, eine Platte, sonst nichts. +# +# NODE 22 IST FESTGENAGELT, und das ist kein Zufall. better-sqlite3 ist ein +# nativer Baustein und laedt beim Installieren eine fertige Binaerdatei +# herunter (`prebuild-install || node-gyp rebuild`). Fuer 11.10.0 gibt es die +# nur fuer diese ABI-Versionen: +# +# Node 20 -> ABI 115 vorhanden +# Node 22 -> ABI 127 vorhanden <- wir +# Node 24 -> ABI 137 FEHLT +# +# Wer auf 24 hochzieht, faellt auf node-gyp zurueck, und das braucht python3, +# make und g++ im Abbild. Der Bau bricht dann ab -- laut und sofort, immerhin. +# Vor einem Versionssprung also nachsehen, ob es die ABI als Binaerdatei gibt: +# https://github.com/WiseLibs/better-sqlite3/releases +# +# bookworm-slim und nicht alpine: musl-Binaerdateien gibt es zwar auch, aber +# der glibc-Weg ist der ausgetretene. Das Abbild ist dafuer groesser -- bei +# einem Container auf dem eigenen Server ist das der guenstigere Tausch. + +# ------------------------------------------------------------ Abhaengigkeiten +FROM node:22-bookworm-slim AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +# Nur was der Betrieb braucht. Hier laeuft prebuild-install und holt die +# native Binaerdatei fuer diese Node-Version. +RUN npm ci --omit=dev + +# -------------------------------------------------------------------- Bauen +FROM node:22-bookworm-slim AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# ------------------------------------------------------------------ Betrieb +FROM node:22-bookworm-slim AS runtime +ENV NODE_ENV=production +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY package.json ./ + +# Die gebaute Oberflaeche kaeme nach ./web -- der Dienst liefert sie aus, wenn +# sie da ist, und sagt sonst, dass sie fehlt (siehe index.ts). Solange es sie +# nicht gibt, bleibt der Ordner leer statt zu fehlen. +RUN mkdir -p /app/web + +# Ein Ordner fuer Bilder UND media.db, damit eine Sicherung aus genau einem +# Kopiervorgang ueber ein Volume besteht. +ENV DATA_DIR=/data +RUN mkdir -p /data && chown -R node:node /data /app + +# Nicht als root. Der Dienst nimmt Dateien von aussen entgegen und schreibt sie +# auf die Platte -- das ist die Stelle, an der man das nicht will. +USER node + +ENV PORT=8080 +EXPOSE 8080 + +# Ohne curl im Abbild: Node kann das selbst, fetch ist ab 18 eingebaut. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||8080)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +CMD ["node", "dist/index.js"]