From feb756cfe289bcf00e6523b8a47406884383656c Mon Sep 17 00:00:00 2001 From: vakabunga Date: Sat, 14 Mar 2026 13:37:34 +0300 Subject: [PATCH] fix(docker): prevent backend crash loop caused by pdf-parse native deps pdf-parse@2.4.5 pulls in @napi-rs/canvas (native Skia binary) which crashes on import in Alpine containers. Moved to lazy require() so the app starts normally and pdf-parse loads only when PDF conversion is actually requested. - Lazy-load pdf-parse in pdfToStatement to avoid startup crash - Add libc6-compat, fontconfig, freetype to Alpine runner stage - Increase npm fetch timeouts in both Dockerfiles for slow networks - Add connectionTimeoutMillis to pg Pool for faster failure detection --- Dockerfile.backend | 9 +++++++++ Dockerfile.frontend | 5 +++++ backend/src/db/pool.ts | 1 + backend/src/services/pdfToStatement.ts | 14 +++++++++++++- frontend/src/components/ImportModal.tsx | 2 +- 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Dockerfile.backend b/Dockerfile.backend index 6229d9a..e866de5 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -5,6 +5,11 @@ COPY package.json package-lock.json* ./ COPY shared ./shared COPY backend ./backend +# Увеличенные таймауты для нестабильной сети +RUN npm config set fetch-retry-mintimeout 20000 && \ + npm config set fetch-retry-maxtimeout 120000 && \ + npm config set fetch-timeout 300000 + RUN npm install FROM node:20-alpine AS build @@ -22,6 +27,10 @@ FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production +# @napi-rs/canvas (dep of pdf-parse) ships a musl pre-built binary that +# needs these compatibility / font libraries at runtime. +RUN apk add --no-cache libc6-compat fontconfig freetype + COPY --from=build /app/backend/dist ./dist COPY --from=build /app/backend/package.json ./package.json COPY --from=build /app/node_modules ./node_modules diff --git a/Dockerfile.frontend b/Dockerfile.frontend index 0b3f1b1..3fdd469 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -9,6 +9,11 @@ COPY package.json package-lock.json* tsconfig.json* ./ COPY shared ./shared COPY frontend ./frontend +# Увеличиваем таймауты и повторы для нестабильной сети +RUN npm config set fetch-retry-mintimeout 20000 && \ + npm config set fetch-retry-maxtimeout 120000 && \ + npm config set fetch-timeout 300000 + # Устанавливаем зависимости из корня RUN npm install diff --git a/backend/src/db/pool.ts b/backend/src/db/pool.ts index 2d5895c..8d64f43 100644 --- a/backend/src/db/pool.ts +++ b/backend/src/db/pool.ts @@ -7,4 +7,5 @@ export const pool = new Pool({ database: config.db.database, user: config.db.user, password: config.db.password, + connectionTimeoutMillis: 5000, }); diff --git a/backend/src/services/pdfToStatement.ts b/backend/src/services/pdfToStatement.ts index def9b65..3f1e16a 100644 --- a/backend/src/services/pdfToStatement.ts +++ b/backend/src/services/pdfToStatement.ts @@ -1,4 +1,3 @@ -import { PDFParse } from 'pdf-parse'; import OpenAI from 'openai'; import { config } from '../config'; import type { StatementFile } from '@family-budget/shared'; @@ -64,6 +63,18 @@ export function isPdfConversionError(r: unknown): r is PdfConversionError { ); } +// Lazy-loaded to avoid crashing the app at startup — pdf-parse pulls in +// @napi-rs/canvas (native Skia binary) which may fail on Alpine. +let _PDFParse: typeof import('pdf-parse')['PDFParse'] | undefined; +function loadPDFParse() { + if (!_PDFParse) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const mod = require('pdf-parse') as typeof import('pdf-parse'); + _PDFParse = mod.PDFParse; + } + return _PDFParse; +} + export async function convertPdfToStatement( buffer: Buffer, ): Promise { @@ -77,6 +88,7 @@ export async function convertPdfToStatement( let text: string; try { + const PDFParse = loadPDFParse(); const parser = new PDFParse({ data: buffer }); const result = await parser.getText(); text = result.text || ''; diff --git a/frontend/src/components/ImportModal.tsx b/frontend/src/components/ImportModal.tsx index 8bc7208..107ffda 100644 --- a/frontend/src/components/ImportModal.tsx +++ b/frontend/src/components/ImportModal.tsx @@ -89,7 +89,7 @@ export function ImportModal({ onClose, onDone }: Props) { {result && (
-
+

Импорт завершён