feat: Dockerfile und Compose -- geschrieben, aber nie gebaut

Auf dieser Maschine gibt es weder Docker noch WSL. Das Abbild ist also
ungebaut und der Container nie gelaufen; das gehoert auf eine Maschine
mit Docker, bevor jemand darauf baut.

Was trotzdem geprueft ist:

Node 22 ist festgenagelt, und das ist kein Zufall. better-sqlite3 11.10.0
liefert fertige Binaerdateien nur fuer ABI 108 (Node 20), 115, 127
(Node 22) und 131 -- Node 24 hat ABI 137 und fehlt. Der naheliegende
Griff zur neuesten Version faellt damit auf node-gyp zurueck und braucht
python3, make und g++ im Abbild. Aus den Veroeffentlichungen des Projekts
abgelesen, nicht aus dem Gedaechtnis.

node dist/index.js -- der Startbefehl des Abbilds -- laeuft und antwortet
auf /health. Bis hierher war der Dienst nur je ueber tsx gestartet
worden. Der Healthcheck gibt Exitcode 0 bei laufendem und 1 bei totem
Dienst.

Dazu eine Falle, die im Compose steht, weil sie teuer ist:
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 naechste Anfrage ist 401. Im Browser gegen eine LAN-Adresse
nachgemessen. Die naheliegende Gegenprobe ueber localhost fuehrt in die
Irre: das gilt Browsern als sicherer Kontext und funktioniert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 16:11:25 +02:00
co-authored by Claude Opus 5
parent 5edbe6c45f
commit 70210c0363
4 changed files with 203 additions and 4 deletions
+31 -4
View File
@@ -178,10 +178,37 @@ es weiterhin keine API.
### ⬜ Als Nächstes ### ⬜ Als Nächstes
**2 · Dockerfile und Compose.** Ein Abbild, das Server und gebaute Oberfläche **2 · Dockerfile und Compose — geschrieben, aber NIE GEBAUT.**
ausliefert; ein Volume für `data/`; ein Port für NPM. In Portainer aus diesem `server/Dockerfile` und `docker-compose.media.yml` stehen. Auf der Maschine,
Repo deploybar — **`Compose path` nimmt EINE Datei**, Ergänzungen werden auf der sie entstanden sind, gibt es weder Docker noch WSL — das Abbild ist
stillschweigend ignoriert. Das hat uns bei Lite einen Nachmittag gekostet. 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`. **3 · `server/upload.lua` im Fotostudio.** `Upload.put(pfad, bytes) → url`.
Token in `config.upload.lua`, server-only, gitignored, mit committetem Token in `config.upload.lua`, server-only, gitignored, mit committetem
+94
View File
@@ -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:
+10
View File
@@ -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
+68
View File
@@ -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"]