From f3938b7fdb2384e674903fc7295a298ceba01640 Mon Sep 17 00:00:00 2001 From: "Claude Opus 4.8" Date: Fri, 10 Jul 2026 17:25:42 +0200 Subject: [PATCH] Never crash boot on plugin dropzone setup; default PLUGINS_DIR in image (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 --- apps/api/Dockerfile | 5 ++++- .../api/src/plugins/plugin-watcher.service.ts | 22 ++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 21bba61..c5291dc 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -25,7 +25,10 @@ RUN pnpm install --frozen-lockfile --filter @dorfteich/api... \ FROM node:22.15-alpine 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 COPY --from=build --chown=node:node /out /app # Generate the Prisma client for this image's platform. diff --git a/apps/api/src/plugins/plugin-watcher.service.ts b/apps/api/src/plugins/plugin-watcher.service.ts index 1f12ef9..56bdae3 100644 --- a/apps/api/src/plugins/plugin-watcher.service.ts +++ b/apps/api/src/plugins/plugin-watcher.service.ts @@ -39,12 +39,22 @@ export class PluginWatcherService implements OnModuleInit, OnModuleDestroy { async onModuleInit(): Promise { if (this.config.env.NODE_ENV === 'test') return; - await this.storage.ensureServiceDirs(); - this.watcher = watch(this.storage.dropzoneDir, (_event, filename) => { - if (!filename || !filename.endsWith('.zip')) return; - this.schedule(filename.toString()); - }); - this.logger.info({ dir: this.storage.dropzoneDir }, 'watching plugin dropzone'); + // 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(); + this.watcher = watch(this.storage.dropzoneDir, (_event, filename) => { + if (!filename || !filename.endsWith('.zip')) return; + this.schedule(filename.toString()); + }); + 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 {