Never crash boot on plugin dropzone setup; default PLUGINS_DIR in image (#71)
All checks were successful
CD / Build and push images (push) Successful in 2m41s
CI / Lint, typecheck, test (push) Successful in 3m20s
CI / Auth e2e pack (push) Successful in 4m9s
CI / Import/export fidelity gate (push) Successful in 54s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Has been skipped

The Test stage crash-looped: PluginWatcherService.onModuleInit did `mkdir`
on the default `./data/plugins` (→ /app/data, not writable by the non-root
user) and an unhandled EACCES aborted bootstrap. Two fixes:

- Harden the watcher: its dropzone is an optional convenience over the GUI
  upload, so a setup failure now logs a warning and disables drop-to-install
  instead of taking down the api.
- Bake writable defaults (UPLOADS_DIR/PLUGINS_DIR=/data/…) into the api image
  so it works out of the box even where compose does not set them; compose
  still mounts named volumes there for persistence.

Migrations applied cleanly ("No pending migrations"); this was purely the
boot-time directory permission.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
This commit is contained in:
Claude Opus 4.8 2026-07-10 17:25:42 +02:00
parent d7aa1fb6da
commit f3938b7fdb
2 changed files with 20 additions and 7 deletions

View File

@ -25,7 +25,10 @@ RUN pnpm install --frozen-lockfile --filter @dorfteich/api... \
FROM node:22.15-alpine FROM node:22.15-alpine
ARG APP_VERSION=0.0.0-dev ARG APP_VERSION=0.0.0-dev
ENV NODE_ENV=production APP_VERSION=${APP_VERSION} # Default the data dirs to the writable, node-owned locations created below, so
# the image works out of the box even where compose does not set them; compose
# still mounts named volumes here for persistence (UPLOADS_DIR/PLUGINS_DIR).
ENV NODE_ENV=production APP_VERSION=${APP_VERSION} UPLOADS_DIR=/data/uploads PLUGINS_DIR=/data/plugins
WORKDIR /app WORKDIR /app
COPY --from=build --chown=node:node /out /app COPY --from=build --chown=node:node /out /app
# Generate the Prisma client for this image's platform. # Generate the Prisma client for this image's platform.

View File

@ -39,12 +39,22 @@ export class PluginWatcherService implements OnModuleInit, OnModuleDestroy {
async onModuleInit(): Promise<void> { async onModuleInit(): Promise<void> {
if (this.config.env.NODE_ENV === 'test') return; if (this.config.env.NODE_ENV === 'test') return;
// The dropzone is an optional convenience over the GUI upload. If its
// directory cannot be created or watched (e.g. PLUGINS_DIR is not writable
// on this deployment), log and carry on — it must never take down the api.
try {
await this.storage.ensureServiceDirs(); await this.storage.ensureServiceDirs();
this.watcher = watch(this.storage.dropzoneDir, (_event, filename) => { this.watcher = watch(this.storage.dropzoneDir, (_event, filename) => {
if (!filename || !filename.endsWith('.zip')) return; if (!filename || !filename.endsWith('.zip')) return;
this.schedule(filename.toString()); this.schedule(filename.toString());
}); });
this.logger.info({ dir: this.storage.dropzoneDir }, 'watching plugin dropzone'); this.logger.info({ dir: this.storage.dropzoneDir }, 'watching plugin dropzone');
} catch (error) {
this.logger.warn(
{ err: error, dir: this.storage.dropzoneDir },
'plugin dropzone unavailable; drop-to-install disabled (GUI upload still works)',
);
}
} }
onModuleDestroy(): void { onModuleDestroy(): void {