From af6bdbfff45ba9733e02f7ec4d1edf3416473a26 Mon Sep 17 00:00:00 2001 From: Alex Gaillard Date: Mon, 6 Jul 2026 16:22:09 -0400 Subject: [PATCH 1/5] Add hardened images section to deployments page --- content/self-hosting/6.hardened-images.md | 86 +++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 content/self-hosting/6.hardened-images.md diff --git a/content/self-hosting/6.hardened-images.md b/content/self-hosting/6.hardened-images.md new file mode 100644 index 00000000..bb74443b --- /dev/null +++ b/content/self-hosting/6.hardened-images.md @@ -0,0 +1,86 @@ +--- +title: Hardened Images +description: Learn how to run Directus using a Docker Hardened Image for a smaller, more secure production footprint. +--- + +Alongside the standard `directus/directus` image, Directus publishes a Docker Hardened Image (DHI) variant built for security-conscious production deployments. This page explains what a hardened image is and how to run Directus with it. + +## What is a Hardened Image? + +A hardened image is a container image that has been stripped down and locked down to reduce its attack surface. Instead of shipping a full operating system with shells, package managers, and general-purpose utilities, a hardened image includes only what the application needs to run. + +Docker Hardened Images are built on a distroless base, which means they do not include a shell, a package manager, or other tools an attacker could use to move around inside a compromised container. Compared to a standard image, a hardened image typically offers: + +- **A smaller attack surface** - removing shells, `npm`, `npx`, and other binaries means there are fewer components to exploit. +- **Fewer vulnerabilities** - a minimal set of packages results in fewer reported CVEs to track and patch. +- **A non-root runtime** - the container runs as an unprivileged user by default. +- **A smaller image size** - fewer packages means faster pulls and less to store. + +These same properties also introduce constraints. Because a hardened image has no shell or package manager, you cannot open an interactive shell inside the container or install extensions at runtime. Any customization, such as [including extensions](/self-hosting/including-extensions), must be done at build time. + +## Running npm and npx Commands with a Multi-Stage Build + +The hardened image removes `npm` and `npx` from the runtime, so anything that depends on them, such as installing an extension from npm or running a one-off `directus` CLI command, cannot run inside the final container. To work around this, use a [multi-stage Dockerfile](https://docs.docker.com/build/building/multi-stage/). + +A multi-stage build lets you run those commands in an earlier stage that uses the standard Directus image, which still includes `npm`, `npx`, and `pnpm`. You then copy only the resulting files into a final stage based on the hardened image. The tools you need for the build stay out of the image you ship. + +This is the recommended approach when you need to: + +- Install extensions from npm. +- Run `directus` CLI commands, such as `directus bootstrap` or database migrations, as part of your build. +- Run any other tooling that relies on `npm` or `npx`. + +### Example Dockerfile + +The following `Dockerfile` uses the standard image as a `build` stage to install an extension, then copies the result into a final stage based on the hardened image: + +```dockerfile +# Build stage - the standard image includes npm, npx, and pnpm +FROM directus/directus:12.1.1 AS build + +USER root +RUN corepack enable +USER node + +# Install an extension from npm, or run any other npm/npx command here +RUN pnpm install @directus-labs/spreadsheet-layout + +# Final stage - the hardened image, without npm or npx +FROM directus/directus:12.1.1-dhi + +# Copy the installed extension into the extensions directory +COPY --from=build /directus/node_modules/@directus-labs/spreadsheet-layout /directus/extensions/spreadsheet-layout +``` + +The `build` stage runs your `npm` or `npx` commands, and the `COPY --from=build` instruction carries the built extension into the [extensions directory](/self-hosting/including-extensions) of the hardened final stage, where Directus loads it on startup. Only the final stage is published as your image, so the build tooling is never part of your runtime. + +Add a `COPY` line for each extension you install. Each copied extension should be a directory containing a `package.json` file and a `dist` directory, matching the structure Directus expects in the extensions directory. + +Pin both stages to the same Directus version to keep the build and runtime consistent. Build the image with `docker compose build` and start it with `docker compose up` as normal, following the same steps described in [Including Extensions](/self-hosting/including-extensions). + +## Running the Directus CLI + +On the standard image, you run the Directus CLI through `npx directus `. The hardened image has no `npx`, but the CLI itself is still present in the container. You can call it directly with `node`, pointing at the CLI entrypoint at `/directus/cli.js`: + +```bash +docker compose exec directus node /directus/cli.js +``` + +This runs against a container that is already up, so use it for one-off tasks against a running deployment. For example, to apply the latest database migrations: + +```bash +docker compose exec directus node /directus/cli.js database migrate:latest +``` + +Any command you would normally pass to the Directus CLI works the same way: + +```bash +# Bootstrap the project (run migrations and create the initial admin user) +docker compose exec directus node /directus/cli.js bootstrap + +# Roll the database back one migration +docker compose exec directus node /directus/cli.js database migrate:down + +# Reset a user's password +docker compose exec directus node /directus/cli.js users passwd --email admin@example.com --password new-password +``` From 1c873a2d1b5a90f23017762809cc9be2c796a371 Mon Sep 17 00:00:00 2001 From: daedalus <44623501+ComfortablyCoding@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:10:09 -0400 Subject: [PATCH 2/5] adjusments --- .../self-hosting/5.including-extensions.md | 24 ++++++--- content/self-hosting/6.hardened-images.md | 54 +++++++++++-------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/content/self-hosting/5.including-extensions.md b/content/self-hosting/5.including-extensions.md index 394ab32d..fe44e90e 100644 --- a/content/self-hosting/5.including-extensions.md +++ b/content/self-hosting/5.including-extensions.md @@ -12,7 +12,11 @@ By default, App extensions and API using our [Sandbox SDK](/guides/extensions/ap ## npm Packages -To install extensions, you will need to build a custom image of Directus. +To install extensions, you will need to build a custom image of Directus. + +::callout{icon="i-lucide-book-open" color="primary" to="/self-hosting/hardened-images"} +For the distroless `-dhi` image or running the Directus CLI on a hardened image, see [Hardened Images](/self-hosting/hardened-images). +:: ### 1. Modify Docker Compose File @@ -25,19 +29,25 @@ build: ### 2. Create a Dockerfile -At the root of your project, create a file called `Dockerfile`, if one doesn't already exist, and add the following: +At the root of your project, create a file called `Dockerfile`, if one doesn't already exist. The Directus image doesn't include `npm`, `npx`, or Corepack, so install the extension in a separate build stage and copy the result into the final image: ```dockerfile -FROM directus/directus:latest +# Build stage - a Node.js Alpine image with npm and Corepack +FROM node:22-alpine AS build -USER root RUN corepack enable -USER node -RUN pnpm install @directus-labs/spreadsheet-layout +WORKDIR /extension-build +RUN pnpm init +RUN pnpm add @directus-labs/spreadsheet-layout + +# Final stage +FROM directus/directus:latest + +COPY --from=build /extension-build/node_modules/@directus-labs/spreadsheet-layout /directus/extensions/spreadsheet-layout ``` -This is an example `Dockerfile` that installs the Spreadsheet Layout via npm. You can change the `RUN pnpm install` line to install any extension published on npm, and add multiple lines for each extension you want to install. +This example installs the Spreadsheet Layout. Change the `pnpm add` line to install any extension published on npm, and add a `COPY` line for each one. ### 3. Build Docker Image diff --git a/content/self-hosting/6.hardened-images.md b/content/self-hosting/6.hardened-images.md index bb74443b..ba697e8e 100644 --- a/content/self-hosting/6.hardened-images.md +++ b/content/self-hosting/6.hardened-images.md @@ -1,28 +1,31 @@ --- title: Hardened Images -description: Learn how to run Directus using a Docker Hardened Image for a smaller, more secure production footprint. +description: Understand Directus's hardened and distroless Docker images, and how to install extensions and run the CLI with them. --- -Alongside the standard `directus/directus` image, Directus publishes a Docker Hardened Image (DHI) variant built for security-conscious production deployments. This page explains what a hardened image is and how to run Directus with it. +Directus publishes Docker images hardened for production: the standard `directus/directus` image and a distroless Docker Hardened Image (DHI) variant tagged with a `-dhi` suffix. This page explains what they do and how to run Directus with them. ## What is a Hardened Image? -A hardened image is a container image that has been stripped down and locked down to reduce its attack surface. Instead of shipping a full operating system with shells, package managers, and general-purpose utilities, a hardened image includes only what the application needs to run. +A hardened image is a container image that has been stripped down and locked down to reduce its attack surface. It includes only what the application needs to run, so there are fewer components an attacker could use to move around inside a compromised container. -Docker Hardened Images are built on a distroless base, which means they do not include a shell, a package manager, or other tools an attacker could use to move around inside a compromised container. Compared to a standard image, a hardened image typically offers: +Directus hardens its images at two levels: -- **A smaller attack surface** - removing shells, `npm`, `npx`, and other binaries means there are fewer components to exploit. +- **The standard `directus/directus` image** applies OS-level patches at build time and removes `npm` and `npx` from the runtime. It still includes a shell, so you can open an interactive shell inside the container. +- **The `-dhi` variant** is a Docker Hardened Image built on a distroless base. On top of the standard image's hardening, it removes the shell, package manager, and other general-purpose utilities entirely. + +These images offer: + +- **A smaller attack surface** - removing `npm`, `npx`, shells, and other binaries means there are fewer components to exploit. - **Fewer vulnerabilities** - a minimal set of packages results in fewer reported CVEs to track and patch. - **A non-root runtime** - the container runs as an unprivileged user by default. - **A smaller image size** - fewer packages means faster pulls and less to store. -These same properties also introduce constraints. Because a hardened image has no shell or package manager, you cannot open an interactive shell inside the container or install extensions at runtime. Any customization, such as [including extensions](/self-hosting/including-extensions), must be done at build time. +These same properties introduce constraints. Because neither image ships `npm` or `npx`, you cannot install extensions at runtime on either one. The `-dhi` variant has no shell at all, so you also cannot open an interactive shell inside it. Any customization, such as [including extensions](/self-hosting/including-extensions), must be done at build time. ## Running npm and npx Commands with a Multi-Stage Build -The hardened image removes `npm` and `npx` from the runtime, so anything that depends on them, such as installing an extension from npm or running a one-off `directus` CLI command, cannot run inside the final container. To work around this, use a [multi-stage Dockerfile](https://docs.docker.com/build/building/multi-stage/). - -A multi-stage build lets you run those commands in an earlier stage that uses the standard Directus image, which still includes `npm`, `npx`, and `pnpm`. You then copy only the resulting files into a final stage based on the hardened image. The tools you need for the build stay out of the image you ship. +Since neither image ships `npm` or `npx`, use a [multi-stage Dockerfile](https://docs.docker.com/build/building/multi-stage/) to install extensions or run other build-time commands. Run those commands in an earlier stage based on a `node:*-alpine` image, then copy only the resulting files into a final stage based on the hardened image. The build tooling never becomes part of the image you ship. This is the recommended approach when you need to: @@ -32,35 +35,40 @@ This is the recommended approach when you need to: ### Example Dockerfile -The following `Dockerfile` uses the standard image as a `build` stage to install an extension, then copies the result into a final stage based on the hardened image: +The following `Dockerfile` installs an extension in a `node:*-alpine` build stage, then copies it into a hardened final stage: ```dockerfile -# Build stage - the standard image includes npm, npx, and pnpm -FROM directus/directus:12.1.1 AS build +# Build stage +FROM node:{version}-alpine AS build -USER root RUN corepack enable -USER node -# Install an extension from npm, or run any other npm/npx command here -RUN pnpm install @directus-labs/spreadsheet-layout +WORKDIR /extension-build +RUN pnpm init +RUN pnpm add @directus-labs/spreadsheet-layout -# Final stage - the hardened image, without npm or npx -FROM directus/directus:12.1.1-dhi +# Final stage - the hardened image +FROM directus/directus:{version}-dhi -# Copy the installed extension into the extensions directory -COPY --from=build /directus/node_modules/@directus-labs/spreadsheet-layout /directus/extensions/spreadsheet-layout +COPY --from=build /extension-build/node_modules/@directus-labs/spreadsheet-layout /directus/extensions/spreadsheet-layout ``` -The `build` stage runs your `npm` or `npx` commands, and the `COPY --from=build` instruction carries the built extension into the [extensions directory](/self-hosting/including-extensions) of the hardened final stage, where Directus loads it on startup. Only the final stage is published as your image, so the build tooling is never part of your runtime. +::callout{icon="i-lucide-triangle-alert" color="warning"} +**Match the Alpine version across stages** +Keep the Alpine version of your `node:*-alpine` build stage in sync with the version the runtime image is built on. An extension with native dependencies compiled against a different version can fail to load at runtime. Check the [Directus Dockerfile](https://github.com/directus/directus/blob/main/Dockerfile) for the version currently in use. +:: + +The `COPY --from=build` instruction carries the built extension into the [extensions directory](/self-hosting/including-extensions), where Directus loads it on startup. Only the final stage is published, so the build tooling never ends up in your runtime. + +This example ships the distroless `-dhi` image. Drop the `-dhi` suffix to build on the standard image instead, which is still hardened but not distroless - the process is identical. Add a `COPY` line for each extension you install. Each copied extension should be a directory containing a `package.json` file and a `dist` directory, matching the structure Directus expects in the extensions directory. -Pin both stages to the same Directus version to keep the build and runtime consistent. Build the image with `docker compose build` and start it with `docker compose up` as normal, following the same steps described in [Including Extensions](/self-hosting/including-extensions). +Pin the final stage to the Directus version you want to run. Build the image with `docker compose build` and start it with `docker compose up` as normal, following the same steps described in [Including Extensions](/self-hosting/including-extensions). ## Running the Directus CLI -On the standard image, you run the Directus CLI through `npx directus `. The hardened image has no `npx`, but the CLI itself is still present in the container. You can call it directly with `node`, pointing at the CLI entrypoint at `/directus/cli.js`: +Normally you run the Directus CLI through `npx directus `, but neither image ships `npx`. The CLI itself is still present in the container, so you can call it directly with `node`, pointing at the CLI entrypoint at `/directus/cli.js`: ```bash docker compose exec directus node /directus/cli.js From 6b877ee90584ae8bd4c67dd8cc8aa964bdaebd22 Mon Sep 17 00:00:00 2001 From: daedalus <44623501+ComfortablyCoding@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:14:55 -0400 Subject: [PATCH 3/5] add stable id --- content/self-hosting/6.hardened-images.md | 1 + 1 file changed, 1 insertion(+) diff --git a/content/self-hosting/6.hardened-images.md b/content/self-hosting/6.hardened-images.md index ba697e8e..ca67cc91 100644 --- a/content/self-hosting/6.hardened-images.md +++ b/content/self-hosting/6.hardened-images.md @@ -1,4 +1,5 @@ --- +stableId: afb60369-abe7-4b0c-86bb-a36ccf65e4d3 title: Hardened Images description: Understand Directus's hardened and distroless Docker images, and how to install extensions and run the CLI with them. --- From 41711b35d36ab9577df6e318e0597576c0cd9561 Mon Sep 17 00:00:00 2001 From: Christopher Jennings <1398135+ChristopherJennings@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:33:22 -0400 Subject: [PATCH 4/5] Incorporated @br41nslug's feedback --- content/self-hosting/6.hardened-images.md | 28 +++++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/content/self-hosting/6.hardened-images.md b/content/self-hosting/6.hardened-images.md index ca67cc91..7bed0d05 100644 --- a/content/self-hosting/6.hardened-images.md +++ b/content/self-hosting/6.hardened-images.md @@ -31,7 +31,7 @@ Since neither image ships `npm` or `npx`, use a [multi-stage Dockerfile](https:/ This is the recommended approach when you need to: - Install extensions from npm. -- Run `directus` CLI commands, such as `directus bootstrap` or database migrations, as part of your build. +- Build local extensions or compile other assets. - Run any other tooling that relies on `npm` or `npx`. ### Example Dockerfile @@ -51,23 +51,27 @@ RUN pnpm add @directus-labs/spreadsheet-layout # Final stage - the hardened image FROM directus/directus:{version}-dhi -COPY --from=build /extension-build/node_modules/@directus-labs/spreadsheet-layout /directus/extensions/spreadsheet-layout +COPY --from=build --chown=node:node /extension-build/node_modules/@directus-labs/spreadsheet-layout /directus/extensions/spreadsheet-layout ``` ::callout{icon="i-lucide-triangle-alert" color="warning"} **Match the Alpine version across stages** -Keep the Alpine version of your `node:*-alpine` build stage in sync with the version the runtime image is built on. An extension with native dependencies compiled against a different version can fail to load at runtime. Check the [Directus Dockerfile](https://github.com/directus/directus/blob/main/Dockerfile) for the version currently in use. +Keep the Alpine version of your `node:*-alpine` build stage in sync with the version the runtime image is built on. An extension with native dependencies must be built for the same OS, libc, and runtime environment as the final image, or it can fail to load at runtime. Prefer pure JavaScript extensions where possible. Check the [Directus Dockerfile](https://github.com/directus/directus/blob/main/Dockerfile) for the version currently in use. :: The `COPY --from=build` instruction carries the built extension into the [extensions directory](/self-hosting/including-extensions), where Directus loads it on startup. Only the final stage is published, so the build tooling never ends up in your runtime. +Directus images run as the unprivileged `node` user rather than root, so files copied from a build stage must be readable by that user. Always include `--chown=node:node` when copying extensions or other files into the final stage. + This example ships the distroless `-dhi` image. Drop the `-dhi` suffix to build on the standard image instead, which is still hardened but not distroless - the process is identical. Add a `COPY` line for each extension you install. Each copied extension should be a directory containing a `package.json` file and a `dist` directory, matching the structure Directus expects in the extensions directory. -Pin the final stage to the Directus version you want to run. Build the image with `docker compose build` and start it with `docker compose up` as normal, following the same steps described in [Including Extensions](/self-hosting/including-extensions). +Pin the final stage to a specific Directus version rather than using the `latest` tag, so upgrades are deliberate and builds are reproducible. For the same reason, commit your lockfile and use a frozen install in your build (for example, `pnpm install --frozen-lockfile`) instead of resolving dependency versions at build time. Build the image with `docker compose build` and start it with `docker compose up` as normal, following the same steps described in [Including Extensions](/self-hosting/including-extensions). + +## Database Migrations and Running the Directus CLI -## Running the Directus CLI +Directus applies migrations automatically on startup, exactly as it does with the non-hardened images, and you can also run them manually against a running container with the Directus CLI. Normally you run the Directus CLI through `npx directus `, but neither image ships `npx`. The CLI itself is still present in the container, so you can call it directly with `node`, pointing at the CLI entrypoint at `/directus/cli.js`: @@ -93,3 +97,17 @@ docker compose exec directus node /directus/cli.js database migrate:down # Reset a user's password docker compose exec directus node /directus/cli.js users passwd --email admin@example.com --password new-password ``` + +## Extension Development + +Local extension development is unchanged by the hardened images. You can develop against a local Directus instance or build an extension locally and mount it into the container, just as before. Runtime features such as `EXTENSIONS_AUTO_RELOAD` continue to work, since they rely only on Node.js. The build-time constraints on this page apply to installing extensions *into* the image, not to developing them. + +## Debugging + +Because the `-dhi` image has no shell, you cannot open an interactive session inside the container with commands like `docker exec -it directus sh`. This is by design - hardened containers are intended to run their entrypoint and nothing else, rather than be treated like a virtual machine. + +To troubleshoot a hardened deployment, rely on: + +- **Container logs** via `docker compose logs directus`. +- **Docker-provided tooling** such as `docker stats` for resource usage and health checks for liveness. +- **The Directus CLI** through `node /directus/cli.js` as shown above, for one-off administrative tasks. \ No newline at end of file From 6a12c97d30a0e55608c24d6890dcd4094e5d57b8 Mon Sep 17 00:00:00 2001 From: Alex Gaillard Date: Wed, 8 Jul 2026 10:43:38 -0400 Subject: [PATCH 5/5] Apply Judd suggestion --- content/self-hosting/6.hardened-images.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/self-hosting/6.hardened-images.md b/content/self-hosting/6.hardened-images.md index 7bed0d05..73f273c5 100644 --- a/content/self-hosting/6.hardened-images.md +++ b/content/self-hosting/6.hardened-images.md @@ -71,7 +71,7 @@ Pin the final stage to a specific Directus version rather than using the `latest ## Database Migrations and Running the Directus CLI -Directus applies migrations automatically on startup, exactly as it does with the non-hardened images, and you can also run them manually against a running container with the Directus CLI. +Directus applies migrations automatically on startup, exactly as it did with the previously non-hardened images, and you can also run them manually against a running container with the Directus CLI. Normally you run the Directus CLI through `npx directus `, but neither image ships `npx`. The CLI itself is still present in the container, so you can call it directly with `node`, pointing at the CLI entrypoint at `/directus/cli.js`: @@ -100,7 +100,7 @@ docker compose exec directus node /directus/cli.js users passwd --email admin@ex ## Extension Development -Local extension development is unchanged by the hardened images. You can develop against a local Directus instance or build an extension locally and mount it into the container, just as before. Runtime features such as `EXTENSIONS_AUTO_RELOAD` continue to work, since they rely only on Node.js. The build-time constraints on this page apply to installing extensions *into* the image, not to developing them. +Local extension development is unchanged by the hardened images. You can develop against a local Directus instance or build an extension locally and mount it into the container, just as before. Runtime features such as `EXTENSIONS_AUTO_RELOAD` continue to work, since they rely only on Node.js. The build-time constraints on this page apply to installing extensions _into_ the image, not to developing them. ## Debugging @@ -110,4 +110,4 @@ To troubleshoot a hardened deployment, rely on: - **Container logs** via `docker compose logs directus`. - **Docker-provided tooling** such as `docker stats` for resource usage and health checks for liveness. -- **The Directus CLI** through `node /directus/cli.js` as shown above, for one-off administrative tasks. \ No newline at end of file +- **The Directus CLI** through `node /directus/cli.js` as shown above, for one-off administrative tasks.