From 84def3f5580af9b696f2467a2400b2ad2fc2ac22 Mon Sep 17 00:00:00 2001 From: nicokampf Date: Mon, 6 Jul 2026 08:49:31 +0200 Subject: [PATCH 1/2] feat(directus): mount per-project extensions folder into Directus service Add an absolute bind mount `${projectsDir}/${container.id}/extensions -> /directus/extensions` to the generated Directus service compose so custom Directus extensions can be provided per project without a custom image. - Absolute path because Swarm bind mounts do not resolve relative paths. - Docker auto-creates the (empty, root-owned) folder on deploy; an empty folder means "no extensions" = unchanged behaviour for existing services. - Directus loads extensions read-only, so root ownership is fine for that. To let the Directus Marketplace write installs here, chown the folder to 1000:1000 (the node/directus container user). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../modules/container/types/service/directus/compose.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/api/src/server/modules/container/types/service/directus/compose.ts b/projects/api/src/server/modules/container/types/service/directus/compose.ts index 5ad1588..ca17805 100644 --- a/projects/api/src/server/modules/container/types/service/directus/compose.ts +++ b/projects/api/src/server/modules/container/types/service/directus/compose.ts @@ -1,4 +1,5 @@ import { Container } from "../../../container.model"; +import envConfig from "../../../../../../config.env"; export function getDirectus(container: Container): string { // Additional networks configuration @@ -43,8 +44,11 @@ ${extraNetworkDefinitions ? '\n' + extraNetworkDefinitions : ''} image: directus/directus:${container.buildImage || 'latest'} volumes: - uploads:/directus/uploads - # If you want to load extensions from the host - # - ./extensions:/directus/extensions + # Load host-provided extensions from the project's extensions folder. + # Absolute path (Swarm bind mounts require it); the folder is created + # automatically (root-owned, empty = no extensions = default behaviour). + # To let the Directus Marketplace write here, chown it to 1000:1000. + - ${envConfig.projectsDir}/${container.id}/extensions:/directus/extensions depends_on: - cache - database From 9b76b0318810a93d30392e7a99006ea217bca670 Mon Sep 17 00:00:00 2001 From: nicokampf Date: Mon, 6 Jul 2026 10:57:22 +0200 Subject: [PATCH 2/2] fix(directus): create extensions folder before deploy so Swarm doesn't reject Docker Swarm does NOT auto-create a missing bind-mount source (unlike standalone `docker run`); it rejects the task with "invalid mount config ... bind source path does not exist". Verified in a local single-node swarm. createDockerComposeFile() now mkdir's `${projectsDir}/${container.id}/extensions` before deploy, so the Directus extensions bind mount added in the previous commit does not break existing/new Directus services on redeploy. Also corrects the compose comment (the folder is created by deploy.party, not automatically by Docker) and documents that the Marketplace needs the folder chowned to 1000:1000 (verified: uid 1000 gets "Permission denied" otherwise). Co-Authored-By: Claude Opus 4.8 (1M context) --- projects/api/src/server/common/services/docker.service.ts | 3 +++ .../modules/container/types/service/directus/compose.ts | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/projects/api/src/server/common/services/docker.service.ts b/projects/api/src/server/common/services/docker.service.ts index d1ff054..8961654 100644 --- a/projects/api/src/server/common/services/docker.service.ts +++ b/projects/api/src/server/common/services/docker.service.ts @@ -118,6 +118,9 @@ export class DockerService { await this.containerService.updateForce(container.id, { env: container.env }); await this.createEnvFile(container); } + // Swarm rejects a service whose bind-mount source path is missing, so the + // per-project extensions folder (see getDirectus) must exist before deploy. + await fs.mkdir(`${envConfig.projectsDir}/${container.id}/extensions`, { recursive: true }); compose = getDirectus(container); break; case ServiceType.ADMINER: diff --git a/projects/api/src/server/modules/container/types/service/directus/compose.ts b/projects/api/src/server/modules/container/types/service/directus/compose.ts index ca17805..e0024eb 100644 --- a/projects/api/src/server/modules/container/types/service/directus/compose.ts +++ b/projects/api/src/server/modules/container/types/service/directus/compose.ts @@ -45,9 +45,11 @@ ${extraNetworkDefinitions ? '\n' + extraNetworkDefinitions : ''} volumes: - uploads:/directus/uploads # Load host-provided extensions from the project's extensions folder. - # Absolute path (Swarm bind mounts require it); the folder is created - # automatically (root-owned, empty = no extensions = default behaviour). - # To let the Directus Marketplace write here, chown it to 1000:1000. + # Absolute path (Swarm bind mounts require it). Swarm does NOT create a + # missing bind source (it rejects the task), so createDockerComposeFile() + # mkdir's this folder before deploy. Empty folder = no extensions = + # unchanged behaviour. To let the Directus Marketplace write installs + # here, chown the folder to 1000:1000 (the directus container user). - ${envConfig.projectsDir}/${container.id}/extensions:/directus/extensions depends_on: - cache