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
This commit is contained in:
vakabunga
2026-03-14 13:37:34 +03:00
parent b598216d24
commit feb756cfe2
5 changed files with 29 additions and 2 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -7,4 +7,5 @@ export const pool = new Pool({
database: config.db.database,
user: config.db.user,
password: config.db.password,
connectionTimeoutMillis: 5000,
});

View File

@@ -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<StatementFile | PdfConversionError> {
@@ -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 || '';

View File

@@ -89,7 +89,7 @@ export function ImportModal({ onClose, onDone }: Props) {
{result && (
<div className="import-result">
<div className="import-result-icon">&check;</div>
<div className="import-result-icon" aria-hidden="true"></div>
<h3>Импорт завершён</h3>
<table className="import-stats">
<tbody>