fix: ffmpeg-Binary im Container fehlte (ENOENT bei jedem Video-Upload)
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 3m50s

Live-Log zeigte: spawn .../ffmpeg-static/ffmpeg ENOENT bei jedem
Transcode-Versuch, unabhaengig vom Videoformat (.mov war nur zufaellig
der zuletzt getestete Fall). Die ffmpeg-static-Binary aus der
migrator-Docker-Stage kam nicht zuverlaessig im finalen Image an.

Root-Fix: ffmpeg jetzt per "apk add ffmpeg" als System-Paket in der
runner-Stage installiert - liegt fest unter /usr/bin auf PATH,
unabhaengig von jeder Node/pnpm-Kopiermechanik. lib/video-transcode.ts
versucht zuerst "ffmpeg" ueber PATH und faellt nur bei ENOENT (kein
System-ffmpeg vorhanden, typischerweise lokale Windows-/Mac-Entwicklung)
auf die per npm mitgelieferte ffmpeg-static-Binary zurueck, damit
"npm run dev" weiterhin ohne manuelle Einrichtung funktioniert. Beide
Pfade lokal gegen ein synthetisches Testvideo verifiziert.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-12 03:38:06 +02:00
1 parent a366ffb544
commit 902f818c6e
2 files changed
+51 -43

No files matched your search

+10 -15
View File
@@ -27,25 +27,13 @@ ENV DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholde
# Datenbanken wechseln kann, siehe lib/prisma.ts. # Datenbanken wechseln kann, siehe lib/prisma.ts.
RUN pnpm build RUN pnpm build
# --- Migrator-Tools + Laufzeit-Zusatzpakete -------------------------------- # --- Migrator-Tools -----------------------------------------------------
# Eigenstaendiger, schlanker node_modules-Baum nur mit Prisma-CLI, tsx und # Eigenstaendiger, schlanker node_modules-Baum nur mit Prisma-CLI, tsx und
# den paar Laufzeit-Paketen, die prisma/seed.ts importiert — NICHT # den paar Laufzeit-Paketen, die prisma/seed.ts importiert — NICHT
# next/react/sharp/stripe/alle devDependencies, die der volle builder-Stage # next/react/sharp/stripe/alle devDependencies, die der volle builder-Stage
# oben mitbringt. Wird unten in die runner-Stage kopiert, damit die App sich # oben mitbringt. Wird unten in die runner-Stage kopiert, damit die App sich
# beim Start selbst migrieren/seeden kann (kein separates Migrations-Image/ # beim Start selbst migrieren/seeden kann (kein separates Migrations-Image/
# kein Kubernetes-Job mehr noetig). # kein Kubernetes-Job mehr noetig).
#
# ffmpeg-static (Video-Transcoding, siehe lib/video-transcode.ts) landet
# bewusst HIER statt per Next.js' automatischer Standalone-Output-Tracing
# aus der builder-Stage: dessen heruntergeladene ffmpeg-Binary liegt im
# pnpm-Store hinter einem Symlink, den Next.js' Tracer (anders als bei
# sharp, dafuer gibt es eine Sonderbehandlung) nicht zuverlaessig erkennt,
# UND ein direktes COPY einzelner Pakete aus dem pnpm-Store der
# builder-Stage wuerde denselben Symlink lediglich unaufgeloest (und damit
# kaputt) in die runner-Stage kopieren. Diese Stage installiert stattdessen
# jedes benoetigte Laufzeitpaket ueber ein eigenes, in sich geschlossenes
# "pnpm add" neu — genau das Muster, das fuer Prisma/tsx hier bereits
# funktioniert.
FROM base AS migrator FROM base AS migrator
# pnpm-workspace.yaml zuerst kopieren: pnpm v11 blockt Postinstall-Skripte # pnpm-workspace.yaml zuerst kopieren: pnpm v11 blockt Postinstall-Skripte
# (u.a. @prisma/engines, laedt die Schema-Engine-Binary fuer "migrate # (u.a. @prisma/engines, laedt die Schema-Engine-Binary fuer "migrate
@@ -62,12 +50,19 @@ COPY pnpm-workspace.yaml ./
RUN pnpm add --save-prod \ RUN pnpm add --save-prod \
prisma@^7.9.1 tsx@^4 bcryptjs@^3.0.3 dotenv@^17.4.2 \ prisma@^7.9.1 tsx@^4 bcryptjs@^3.0.3 dotenv@^17.4.2 \
@prisma/client@^7.9.1 @prisma/adapter-pg@^7.9.1 \ @prisma/client@^7.9.1 @prisma/adapter-pg@^7.9.1 \
@prisma/adapter-mariadb@^7.9.1 mariadb@^3.5.3 \ @prisma/adapter-mariadb@^7.9.1 mariadb@^3.5.3
ffmpeg-static@^5.3.0
# --- Runtime -------------------------------------------------------------- # --- Runtime --------------------------------------------------------------
FROM base AS runner FROM base AS runner
ENV NODE_ENV=production ENV NODE_ENV=production
# System-ffmpeg statt des npm-Pakets ffmpeg-static (lib/video-transcode.ts
# nutzt es lokal per "npm run dev" trotzdem als Fallback, siehe dort):
# ffmpeg-static laedt seine Binary in den pnpm-Store, dessen Pfad ueber
# mehrere Docker-Build-Stages hinweg (migrator -> runner) per COPY nicht
# zuverlaessig ankam (spawn ... ffmpeg ENOENT im Betrieb beobachtet). Ein
# per apk installiertes System-Paket liegt dagegen fest unter /usr/bin auf
# PATH, unabhaengig von jeder Node/pnpm-Verzeichnisstruktur.
RUN apk add --no-cache ffmpeg
RUN addgroup --system --gid 1001 nodejs \ RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs && adduser --system --uid 1001 nextjs
+41 -28
View File
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import path from "node:path"; import path from "node:path";
import ffmpegPath from "ffmpeg-static"; import ffmpegStaticPath from "ffmpeg-static";
/** /**
* Harte Obergrenze fuer einen einzelnen ffmpeg-Lauf: verhindert, dass ein * Harte Obergrenze fuer einen einzelnen ffmpeg-Lauf: verhindert, dass ein
@@ -40,39 +40,52 @@ const TRANSCODE_TIMEOUT_MS = 5 * 60 * 1000;
* Magic-Bytes am Dateianfang). * Magic-Bytes am Dateianfang).
*/ */
export async function transcodeVideoForWeb(buffer: Buffer): Promise<Buffer> { export async function transcodeVideoForWeb(buffer: Buffer): Promise<Buffer> {
if (!ffmpegPath) {
throw new Error("ffmpeg-Binary (ffmpeg-static) nicht gefunden.");
}
const dir = await mkdtemp(path.join(tmpdir(), "wabenhain-video-")); const dir = await mkdtemp(path.join(tmpdir(), "wabenhain-video-"));
const inputPath = path.join(dir, "input"); const inputPath = path.join(dir, "input");
const outputPath = path.join(dir, "output.mp4"); const outputPath = path.join(dir, "output.mp4");
const args = [
"-y",
"-i",
inputPath,
"-an",
"-vf",
"scale='min(1920,iw)':-2",
"-c:v",
"libx264",
"-preset",
"medium",
"-crf",
"26",
"-maxrate",
"3M",
"-bufsize",
"6M",
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
outputPath,
];
try { try {
await writeFile(inputPath, buffer); await writeFile(inputPath, buffer);
await runFfmpeg(ffmpegPath, [ try {
"-y", // Bevorzugt "ffmpeg" ueber PATH — im Produktions-Image per "apk add
"-i", // ffmpeg" installiert (siehe Dockerfile "runner"-Stage), fest unter
inputPath, // /usr/bin und unabhaengig von jeder Node/pnpm-Verzeichnisstruktur.
"-an", await runFfmpeg("ffmpeg", args);
"-vf", } catch (error: unknown) {
"scale='min(1920,iw)':-2", // ENOENT = Binary ueber PATH nicht gefunden (typischerweise ein
"-c:v", // lokaler Windows-/Mac-Rechner ohne System-ffmpeg) — dann auf die per
"libx264", // npm mitgelieferte ffmpeg-static-Binary ausweichen, damit "npm run
"-preset", // dev" ohne manuelle Einrichtung funktioniert. Jeder andere Fehler
"medium", // (z.B. eine tatsaechlich nicht dekodierbare Datei) soll dagegen
"-crf", // unveraendert durchschlagen, nicht stillschweigend erneut versucht
"26", // werden.
"-maxrate", const isMissingBinary = error instanceof Error && "code" in error && error.code === "ENOENT";
"3M", if (!isMissingBinary || !ffmpegStaticPath) throw error;
"-bufsize", await runFfmpeg(ffmpegStaticPath, args);
"6M", }
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
outputPath,
]);
return await readFile(outputPath); return await readFile(outputPath);
} finally { } finally {
await rm(dir, { recursive: true, force: true }); await rm(dir, { recursive: true, force: true });