exulu-example repository with your own configuration. @exulu/backend installs from the public npm registry during the build — no token is required.
Base image
Both the backend and worker usenode:22.18.0-slim (Debian). Alpine is not supported — the ONNX runtime dependencies used by document processing are not available in Alpine.
System packages
Theapt-get layer in both Dockerfiles installs:
OpenGL/GLib libraries (
libgl1, libglib2.0-0, libgomp1, etc.) are also installed as runtime requirements for libreoffice and document-processing libraries.
Global npm packages
Both images run:pm2— process manager that runs the backend/worker as a supervised daemon inside the container.tsx— used in dev mode (NODE_ENV=dev) to run TypeScript directly.docx— required by the built-in DOCX skill at runtime; skill scriptsrequire('docx')from the globalnode_modules.
Python venv
Afternpm ci, both Dockerfiles run:
ee/python/.venv and installs LiteLLM, WhisperX, and other Python dependencies. The venv is bundled into the image — no internet access is required at container startup.
Ports
The worker container does not expose an HTTP endpoint. Port
9002 is defined as ENV PORT and EXPOSE 9002 in Dockerfile.worker and is read by the PORT variable, but the worker process (worker.ts) never binds an HTTP server — it only creates BullMQ workers. Do not probe exulu-worker:9002 for health; the worker has no HTTP listener.Start scripts
On container startup, theCMD calls start-backend.sh or start-worker.sh. Both scripts:
- Run
npm run utils:initdb— idempotent database initialisation (see Database). - Build the TypeScript dist (production mode only).
- Check whether a pm2 process named
exulu-server/exulu-workeris already running; restart it if so, start it fresh if not. - Tail pm2 logs.
NODE_ENV=dev the scripts skip the pm2 path and invoke tsx watch directly.
start-worker.sh does not set NODE_OPTIONS. To raise the V8 heap limit for memory-intensive knowledge processing, add it to the compose environment block yourself:Skill sandbox
IMP uses bubblewrap (bwrap) to run skill bash commands inside a user-namespace container. This is the same isolation primitive used by Flatpak.
bwrap needs to call unshare(CLONE_NEWUSER) and mount(). Docker’s default seccomp profile and the default AppArmor profile on Ubuntu 23.10+ / 24.04+ hosts both block these syscalls. The result: bwrap cannot create user namespaces, and sandboxed skill execution fails.
Both docker-compose.backend.yml and docker-compose.worker.yml include:
bwrap then enforces its own sandbox — it is defense-in-depth rather than defense-in-depth-removed. The IMP container itself has no elevated capabilities; only bwrap inside it can perform the namespace operations it needs, and only for the duration of a skill invocation.
What this means for your security posture:
- The container runs without a seccomp filter. Any Linux kernel syscall is reachable from inside the container process. This is the same posture as many development containers that need
ptraceorperf. bwrapstill creates its own sandbox for each skill call: read-only bind mounts, a private/tmp, no network access to the host.- If you operate in a hardened environment where
seccomp:unconfinedis not acceptable, you can write a custom seccomp profile that allows onlyunshareandclonewith theCLONE_NEWUSERflag, and apply that profile instead.
Disabling the sandbox
By default, ifbwrap cannot create namespaces, IMP degrades gracefully: skill bash commands run without kernel sandboxing, but the JS-layer file restrictions still apply. To make this degradation a hard failure instead, set:
Health checks
IMP’s backend container does not configure a Docker health check. The only HTTP route that is always available is/ (the root path), which returns 200 OK. You can use this path for an external health probe: