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:
+31
-4
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user