From 96818c7ae8de0d8fd478cd901ea25a3272eee85d Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Mon, 4 May 2026 09:04:38 +0200 Subject: [PATCH] fix(docker): install system ffmpeg on Alpine, drop broken bundled binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Video uploads on production fail with "missing ffmpeg" because the backend container ships nothing usable for the video pipeline. Two compounding causes: 1. **Alpine + glibc mismatch.** The npm `@ffmpeg-installer/ffmpeg` dependency added with the video-support PR (commit 68a9dc5) ships per-platform binaries via optionalDependencies. The Linux binaries are built against glibc, but the backend image runs on `node:22-alpine` (musl libc) — known to either fail to execute or fail on shared-library lookups on Alpine. 2. **`ffprobe` missing entirely.** `@ffmpeg-installer/ffmpeg` bundles only the `ffmpeg` binary. There's a separate `@ffprobe-installer/ffprobe` package that the codebase never depended on. But `videoProcessor.js:21` calls `ffmpeg.ffprobe(videoPath, …)` — the very first step of the video pipeline shells out to a `ffprobe` binary that doesn't exist in the image. Even if (1) worked, every video upload would 500 here. The fix is to install Alpine's `ffmpeg` package via apk. It ships both `ffmpeg` and `ffprobe` built natively against musl, ~70MB extra image size, single line in the Dockerfile, no per-arch handling needed (apk pulls the right binary for both linux/amd64 and linux/arm64 — works with the multi-arch infra from #349). - `backend/Dockerfile`: add `ffmpeg` to the apk install line. - `backend/Dockerfile.dev`: same for dev parity. - `backend/src/services/videoProcessor.js`: remove the `setFfmpegPath(require('@ffmpeg-installer/ffmpeg').path)` line — without removing it, fluent-ffmpeg would prefer the broken bundled binary over the working apk one. Letting fluent-ffmpeg fall back to PATH lookup picks up the apk binary in the container and the developer's locally-installed binary on dev hosts (Homebrew on macOS, apt on Debian). - `backend/package.json`: drop the now-unused `@ffmpeg-installer/ffmpeg` dependency. `npm install` removes 2 packages from the lockfile. Verified: `videoProcessor.js` still loads cleanly (`node -e "require('./src/services/videoProcessor')"`); lint clean. --- backend/Dockerfile | 9 +- backend/Dockerfile.dev | 6 +- backend/package-lock.json | 131 +------------------------ backend/package.json | 1 - backend/src/services/videoProcessor.js | 9 +- 5 files changed, 19 insertions(+), 137 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index e4ad7745..9a35453a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -34,8 +34,13 @@ RUN apk upgrade --no-cache # Pin to 10.x to stay compatible with Node 22 Alpine (npm 11.x has dependency issues) RUN npm install -g npm@10 -# Install dumb-init for proper signal handling and postgresql-client for database checks -RUN apk add --no-cache dumb-init postgresql-client +# Install dumb-init for proper signal handling, postgresql-client for database +# checks, and ffmpeg for video upload support. Alpine's ffmpeg package ships +# both `ffmpeg` and `ffprobe` built natively against musl libc — the npm +# `@ffmpeg-installer/ffmpeg` binary is glibc-built and (a) doesn't reliably +# run on Alpine and (b) only includes ffmpeg, not ffprobe (which the video +# pipeline calls via fluent-ffmpeg.ffprobe()). +RUN apk add --no-cache dumb-init postgresql-client ffmpeg # Create non-root user RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 8054bdab..2f056807 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -5,8 +5,10 @@ WORKDIR /app # Upgrade all packages to fix security vulnerabilities (BusyBox CVEs) RUN apk upgrade --no-cache -# Install dumb-init for proper signal handling -RUN apk add --no-cache dumb-init +# Install dumb-init for proper signal handling and ffmpeg for video uploads. +# Alpine's ffmpeg ships both ffmpeg + ffprobe built natively against musl; +# the npm-bundled binary doesn't run reliably on Alpine. Match production. +RUN apk add --no-cache dumb-init ffmpeg # Copy package files COPY package*.json ./ diff --git a/backend/package-lock.json b/backend/package-lock.json index aa148a96..7405ed14 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,17 +1,16 @@ { "name": "picpeak-backend", - "version": "3.28.3-beta.0", + "version": "3.34.1-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "picpeak-backend", - "version": "3.28.3-beta.0", + "version": "3.34.1-beta.0", "dependencies": { "@aws-sdk/client-s3": "^3.850.0", "@aws-sdk/lib-storage": "^3.850.0", "@aws-sdk/s3-request-presigner": "^3.850.0", - "@ffmpeg-installer/ffmpeg": "^1.1.0", "adm-zip": "^0.5.16", "archiver": "^5.3.1", "axios": "1.14.0", @@ -1607,132 +1606,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@ffmpeg-installer/darwin-arm64": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz", - "integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==", - "cpu": [ - "arm64" - ], - "hasInstallScript": true, - "license": "https://git.ffmpeg.org/gitweb/ffmpeg.git/blob_plain/HEAD:/LICENSE.md", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@ffmpeg-installer/darwin-x64": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz", - "integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==", - "cpu": [ - "x64" - ], - "hasInstallScript": true, - "license": "LGPL-2.1", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@ffmpeg-installer/ffmpeg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz", - "integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==", - "license": "LGPL-2.1", - "optionalDependencies": { - "@ffmpeg-installer/darwin-arm64": "4.1.5", - "@ffmpeg-installer/darwin-x64": "4.1.0", - "@ffmpeg-installer/linux-arm": "4.1.3", - "@ffmpeg-installer/linux-arm64": "4.1.4", - "@ffmpeg-installer/linux-ia32": "4.1.0", - "@ffmpeg-installer/linux-x64": "4.1.0", - "@ffmpeg-installer/win32-ia32": "4.1.0", - "@ffmpeg-installer/win32-x64": "4.1.0" - } - }, - "node_modules/@ffmpeg-installer/linux-arm": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz", - "integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==", - "cpu": [ - "arm" - ], - "hasInstallScript": true, - "license": "GPLv3", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@ffmpeg-installer/linux-arm64": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz", - "integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==", - "cpu": [ - "arm64" - ], - "hasInstallScript": true, - "license": "GPLv3", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@ffmpeg-installer/linux-ia32": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz", - "integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==", - "cpu": [ - "ia32" - ], - "hasInstallScript": true, - "license": "GPLv3", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@ffmpeg-installer/linux-x64": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz", - "integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==", - "cpu": [ - "x64" - ], - "hasInstallScript": true, - "license": "GPLv3", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@ffmpeg-installer/win32-ia32": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz", - "integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==", - "cpu": [ - "ia32" - ], - "license": "GPLv3", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@ffmpeg-installer/win32-x64": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz", - "integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==", - "cpu": [ - "x64" - ], - "license": "GPLv3", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", diff --git a/backend/package.json b/backend/package.json index f06e83e2..de617eb6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,7 +17,6 @@ "@aws-sdk/client-s3": "^3.850.0", "@aws-sdk/lib-storage": "^3.850.0", "@aws-sdk/s3-request-presigner": "^3.850.0", - "@ffmpeg-installer/ffmpeg": "^1.1.0", "adm-zip": "^0.5.16", "archiver": "^5.3.1", "axios": "1.14.0", diff --git a/backend/src/services/videoProcessor.js b/backend/src/services/videoProcessor.js index 885e568e..f699d284 100644 --- a/backend/src/services/videoProcessor.js +++ b/backend/src/services/videoProcessor.js @@ -1,5 +1,4 @@ const ffmpeg = require('fluent-ffmpeg'); -const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path; const path = require('path'); const fs = require('fs').promises; const fsSync = require('fs'); @@ -8,8 +7,12 @@ const crypto = require('crypto'); const logger = require('../utils/logger'); const { getStorage } = require('./storage'); -// Set FFmpeg path -ffmpeg.setFfmpegPath(ffmpegPath); +// Use system ffmpeg/ffprobe (apk-installed in the Docker image, brew/apt on +// dev hosts). The npm `@ffmpeg-installer/ffmpeg` binary is glibc-built and +// (a) doesn't run reliably on Alpine and (b) only ships ffmpeg, not ffprobe +// — but `ffmpeg.ffprobe()` below needs both. Letting fluent-ffmpeg fall +// back to PATH lookup picks up the apk-installed binaries inside the +// container and the developer's locally-installed ones outside it. /** * Extract video metadata using FFmpeg