fix: Produktions-Crash durch TypeORM-synchronize beheben
Build and Push Multi-Platform Images / build-and-push (push) Successful in 44s

NODE_ENV=production deaktiviert synchronize (zerstörerischer ADD/DROP-COLUMN-
Churn auf MariaDB, der die 8126-Byte-Zeilengröße sprengte) und aktiviert
migrationsRun. Neue data-source.ts als einzige Konfigquelle (Laufzeit + CLI),
Migrations-Workflow (generate/run/revert) inkl. dotenv ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 09:27:04 +02:00
parent ed57477324
commit 41eed1871e
8 changed files with 143 additions and 72 deletions
+7
View File
@@ -7,6 +7,13 @@
# Produktion: VITE_API_URL leer lassen (nginx Reverse-Proxy leitet /api weiter)
# =============================================================================
# --- Umgebung ---
# Produktion: NODE_ENV=production -> KEIN TypeORM-synchronize (Schema via Migrationen),
# Migrationen werden beim Start automatisch ausgeführt (migrationsRun).
# Aktiviert zudem CORS-Schutz (siehe CORS_ORIGIN weiter unten).
# Entwicklung: NODE_ENV leer lassen -> synchronize ON (Schema folgt den Entities).
NODE_ENV=production
# --- Ports ---
BACKEND_PORT=7601
FRONTEND_PORT=7600
+27 -1
View File
@@ -78,11 +78,37 @@ Use `@Public()` to bypass auth guards entirely.
### Database
- TypeORM with MySQL 8+, UTF8MB4, `synchronize: true` (schema auto-migrates)
- TypeORM with MySQL/MariaDB, UTF8MB4
- Connection config lives in `src/database/data-source.ts` (single source of truth,
shared by the NestJS runtime and the TypeORM CLI). `database.module.ts` consumes it.
- **Schema strategy depends on `NODE_ENV`:**
- Dev (`NODE_ENV` unset): `synchronize: true` — schema auto-migrates from entities.
- Production (`NODE_ENV=production`): `synchronize: false` + `migrationsRun: true`
pending migrations in `src/database/migrations/` are applied automatically on boot.
**Never run `synchronize` against the production DB** — on MariaDB it issues
destructive `ADD`/`DROP COLUMN` churn every boot (see caveat below).
- 23 entities in `src/database/entities/`
- JSON columns use transformers to normalize empty arrays/objects to `null`
- Key entities: `InboxDocument`, `Task`, `Email`, `Attachment`, `Postprocessing`, `BarcodeTemplate`, `LabelPrintJob`, `ApiKey`, `Setting`
#### Migrations workflow
```bash
npm run migration:generate -- src/database/migrations/<Name> # diff entities → DB
npm run migration:run # apply pending
npm run migration:revert # roll back last
```
The existing production schema is the implicit baseline (no baseline migration); only
future changes get migration files.
**Caveat — MariaDB reports `json` as `longtext`:** every `@Column({ type: 'json' })`
is stored as `longtext ... CHECK (json_valid(...))`, so `migration:generate` always
emits spurious no-op `CHANGE` statements (json↔longtext, nullable/default re-declares).
**Hand-trim generated migrations** down to the real change before committing. (This same
false diff is exactly why `synchronize` must stay off in production — left unchecked it
accumulates `ALGORITHM=INSTANT` drops until a table trips the 8126-byte row-size limit.)
### Document Processing Pipeline
**Preprocessing** (`preprocessing/document-pipeline.service.ts`):
+2
View File
@@ -8,6 +8,8 @@ services:
ports:
- "${BACKEND_PORT:-7601}:3100"
environment:
- NODE_ENV=production
- CORS_ORIGIN=${CORS_ORIGIN:-}
- PORT=3100
- DB_HOST=${DB_HOST:-db}
- DB_PORT=${DB_PORT:-3306}
+16 -3
View File
@@ -24,6 +24,7 @@
"axios": "^1.14.0",
"basic-ftp": "^5.2.1",
"chokidar": "^4.0.3",
"dotenv": "^17.4.2",
"form-data": "^4.0.5",
"imapflow": "^1.3.2",
"jsqr": "^1.4.0",
@@ -3023,6 +3024,18 @@
"rxjs": "^7.1.0"
}
},
"node_modules/@nestjs/config/node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/core": {
"version": "11.1.17",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.17.tgz",
@@ -6291,9 +6304,9 @@
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
+6 -1
View File
@@ -17,7 +17,11 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "typeorm-ts-node-commonjs -d ./src/database/data-source.ts",
"migration:generate": "npm run typeorm -- migration:generate",
"migration:run": "npm run typeorm -- migration:run",
"migration:revert": "npm run typeorm -- migration:revert"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
@@ -35,6 +39,7 @@
"axios": "^1.14.0",
"basic-ftp": "^5.2.1",
"chokidar": "^4.0.3",
"dotenv": "^17.4.2",
"form-data": "^4.0.5",
"imapflow": "^1.3.2",
"jsqr": "^1.4.0",
@@ -0,0 +1,81 @@
import 'reflect-metadata';
import { join } from 'path';
import { config as loadEnv } from 'dotenv';
import { DataSource, DataSourceOptions } from 'typeorm';
import {
Client,
DocumentType,
DocumentField,
Task,
Postprocessing,
PostprocessingAction,
PostprocessingLog,
ExportTarget,
Setting,
Kontonummer,
Document,
UserClient,
Email,
Attachment,
Content,
ApiKey,
CorrespondentSetting,
BarcodeTemplate,
InboxDocument,
InboxPostprocessingAction,
CorrespondentEmailMapping,
UserSettings,
LabelPrintJob,
} from './entities';
// CLI-Kontext: .env laden (Laufzeit im Container liefert die Variablen via Docker,
// dotenv ist dort ein No-Op). dotenv überschreibt bereits gesetzte Variablen nicht.
loadEnv();
loadEnv({ path: join(process.cwd(), '..', '.env') });
export const entities = [
Client,
DocumentType,
DocumentField,
Task,
Postprocessing,
PostprocessingAction,
PostprocessingLog,
ExportTarget,
Setting,
Kontonummer,
Document,
UserClient,
Email,
Attachment,
Content,
ApiKey,
CorrespondentSetting,
BarcodeTemplate,
InboxDocument,
InboxPostprocessingAction,
CorrespondentEmailMapping,
UserSettings,
LabelPrintJob,
];
const isProduction = process.env.NODE_ENV === 'production';
export const dataSourceOptions: DataSourceOptions = {
type: 'mysql',
host: process.env.DB_HOST ?? 'localhost',
port: Number(process.env.DB_PORT ?? 3306),
username: process.env.DB_USERNAME ?? 'root',
password: process.env.DB_PASSWORD ?? '',
database: process.env.DB_DATABASE ?? 'paperlessadd',
charset: 'utf8mb4',
entities,
migrations: [join(__dirname, 'migrations', '*.{ts,js}')],
// In Produktion: kein synchronize (zerstörerischer ALTER-Churn), Migrationen
// werden beim Start automatisch ausgeführt. In Dev: synchronize wie bisher.
synchronize: !isProduction,
migrationsRun: isProduction,
};
// Wird von der TypeORM-CLI verwendet (migration:generate / :run / :revert).
export default new DataSource(dataSourceOptions);
@@ -1,75 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
Client,
DocumentType,
DocumentField,
Task,
Postprocessing,
PostprocessingAction,
PostprocessingLog,
ExportTarget,
Setting,
Kontonummer,
Document,
UserClient,
Email,
Attachment,
Content,
ApiKey,
CorrespondentSetting,
BarcodeTemplate,
InboxDocument,
InboxPostprocessingAction,
CorrespondentEmailMapping,
UserSettings,
LabelPrintJob,
} from './entities';
const entities = [
Client,
DocumentType,
DocumentField,
Task,
Postprocessing,
PostprocessingAction,
PostprocessingLog,
ExportTarget,
Setting,
Kontonummer,
Document,
UserClient,
Email,
Attachment,
Content,
ApiKey,
CorrespondentSetting,
BarcodeTemplate,
InboxDocument,
InboxPostprocessingAction,
CorrespondentEmailMapping,
UserSettings,
LabelPrintJob,
];
import { dataSourceOptions, entities } from './data-source';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'mysql' as const,
host: config.get<string>('DB_HOST', 'localhost'),
port: config.get<number>('DB_PORT', 3306),
username: config.get<string>('DB_USERNAME', 'root'),
password: config.get<string>('DB_PASSWORD', ''),
database: config.get<string>('DB_DATABASE', 'paperlessadd'),
entities,
synchronize: config.get<string>('NODE_ENV') !== 'production',
charset: 'utf8mb4',
}),
}),
// Eine einzige Konfigurationsquelle (data-source.ts), geteilt zwischen
// NestJS-Laufzeit und TypeORM-CLI (Migrationen).
TypeOrmModule.forRoot(dataSourceOptions),
TypeOrmModule.forFeature(entities),
],
exports: [TypeOrmModule],