Supporting custom Docker Compose looked simple when I first thought about it:
- let the user paste a
docker-compose.yml; - save it on the VPS;
- run
docker compose up -d; - celebrate. 🎉
The problem is that a Compose file is not just a list of containers.
It can mount the host filesystem, join the host network, access devices, add Linux capabilities, reuse another application's network, publish arbitrary ports, or mount the Docker socket—which is effectively control over the Docker host.
So the real workflow cannot be this:
Compose file ──────────────────────────► Docker daemon It needs a boundary in the middle:
Compose file │ ▼ ┌──────────────────────────────┐ │ Parse → Normalize → Inspect │ │ Transform → Resolve → Check │ └──────────────────────────────┘ │ │ │ accepted └────► refused with a reason ▼ Docker daemon This is the problem behind the Compose safety policy in Nitrobase.
Nitrobase has only recently launched and is still in Beta. The policy is not a container sandbox and does not make arbitrary images trustworthy. Its narrower job is to prevent a pasted or upstream Compose file from quietly crossing the infrastructure boundaries Nitrobase is supposed to protect.
The innocent-looking file
Consider this simplified application:
services: web: image: example/app:latest ports: - "8080:8080" volumes: - ./data:/app/data It has one service, one public port, and one local data directory. There is nothing surprising here.
Now compare it with this:
services: web: image: example/app:latest privileged: true network_mode: host volumes: - /:/host - /var/run/docker.sock:/var/run/docker.sock Both files are valid Compose. Their consequences are completely different.
The second service can see the host filesystem, talk directly to Docker, escape normal network separation, and run with elevated privileges. If a platform treats both files as equally ordinary input, it has delegated the security of the server to whatever Compose file happened to be pasted.
My baseline rule: custom Compose gets no exceptions
Nitrobase supports two broad sources of Compose files:
- maintained application templates;
- a custom file pasted by the server owner.
Maintained templates may occasionally need an elevated permission. When that happens, the permission must be explicitly declared in the template, accompanied by a reason, displayed as a warning, and confirmed before deployment.
Custom Compose is different. It does not get a hidden allowlist.
The security section remains empty, so the following capabilities are refused:
| Compose capability | Why it needs scrutiny |
|---|---|
privileged: true | Removes much of the container isolation boundary |
| Docker socket mount | Gives the container control of the Docker daemon |
network_mode: host | Bypasses the application's isolated network |
| Host PID/IPC/UTS namespaces | Exposes host-level process or namespace state |
cap_add and devices | Adds kernel capabilities or direct device access |
| Host paths outside the app folder | Lets one app read or modify unrelated host data |
| External networks and volumes | Can cross application ownership boundaries |
This is intentionally restrictive. A PaaS cannot promise application separation while silently accepting configuration that removes it.
Published ports are transformed, not copied
A pasted Compose file may contain:
ports: - "8080:8080" Nitrobase removes ordinary published ports from the generated application Compose file. HTTP traffic is routed through Traefik using the service and container port selected during setup.
Internet │ ▼ Traefik :443 │ route: app.example.com ▼ web:8080 on a private Docker network This avoids giving every application an unrelated public entrypoint beside the managed domain route.
When an application genuinely needs a raw TCP port—for example, a database connection—the port must be declared and enabled explicitly. Nitrobase then checks for reserved ports, ports already used by another application, and ports already listening on the server.
The distinction is useful:
“The Compose file contains a port” ≠ “That port should be exposed on the public host” Relative paths should stay relative to the application
Relative bind mounts are convenient:
volumes: - ./config:/app/config:ro - ./data:/app/data Nitrobase pins these paths inside the application's own folder. A path that escapes with .. is rejected.
# refused volumes: - ../../another-app/data:/app/data The check also needs to consider symlinks. A path can look like it stays inside the application directory while a symlink on the host points somewhere else.
That is why Nitrobase checks the policy twice:
- against the source Compose model before deployment;
- against the fully resolved
docker compose config --format jsonmodel, then verifies bind sources on the server.
./data ──► /opt/apps/my-app/data ✅ ./data ──► symlink ──► /etc ❌ Text inspection alone is not enough when the final path depends on the target filesystem.
Reserved labels are not application-owned
Traefik routing depends on Docker labels. If custom applications could define the same reserved labels that Nitrobase uses, they could interfere with routing intended for another application.
For that reason, labels under reserved namespaces such as traefik.* and com.docker.* are removed. Nitrobase generates its own routing configuration from the domains defined in the panel.
labels: # removed from custom Compose - traefik.http.routers.catch-all.rule=HostRegexp(`{host:.+}`) The application still controls its own application-specific labels. It does not control the platform's routing plane.
Why checking the YAML once is not enough
Compose supports environment interpolation, multiple forms of the same setting, and a resolution step that changes the shape of the document.
For example:
volumes: - ${DATA_PATH}:/app/data The safety of that mount depends on the value of DATA_PATH, not the text visible beside volumes.
A reliable check therefore needs to inspect both intent and result:
Raw YAML │ ├─► reject unsafe fields and interpolation locations │ ▼ Resolved Compose model │ ├─► verify the final networks, mounts, namespaces and ports │ ▼ Host filesystem checks Nitrobase reruns the policy at deployment time on Docker Compose's resolved JSON model. The second pass is not redundant; it catches what the first representation may not make obvious.
The policy also makes small operational changes
Safety is not only about blocking dangerous privileges.
If a long-running service has no restart policy, Nitrobase adds one so the container returns after the VPS reboots. A one-time migration or initialization service must explicitly declare:
restart: "no" Otherwise, an init container that was meant to run once could be restarted repeatedly.
The deploy preview reports transformations like removed ports, removed reserved labels, or an added restart policy. I want these changes to be visible rather than silently rewriting the user's infrastructure.
What the policy cannot guarantee
This boundary is important, but it has limits.
An accepted Compose file can still deploy:
- an image with a software vulnerability;
- an application that leaks its own secrets;
- a database configured with a weak password;
- a service that consumes too much CPU;
- an image whose entrypoint downloads and executes untrusted code.
The policy controls how containers interact with the host and the platform. It does not prove that the software inside those containers is safe.
Compose policy protects the infrastructure boundary. Image trust protects the software supply chain. Application security protects the data and users. Those are related layers, not interchangeable promises.
My checklist before accepting custom Compose
When reviewing this workflow, these are the questions I keep returning to:
- Can the service mount the Docker socket?
- Can a bind path escape the application folder?
- Can it join the host or another application's network?
- Can it publish a port without an explicit decision?
- Can it override platform routing labels?
- Can interpolation hide the final value of a security-sensitive field?
- Is the resolved Compose model checked again before execution?
- Are automatic transformations shown to the user?
The checklist is less exciting than a one-click deploy button, but it is what makes that button possible.
The problem this feature is trying to solve
Custom Compose support is valuable because it lets people bring applications that are not yet available as templates. But flexibility without a boundary turns the VPS into a shared root shell expressed as YAML.
The Nitrobase policy tries to preserve both sides:
Bring your own application ✅ Bring arbitrary host control ❌ It remains part of Nitrobase Beta. Compose has a large surface area, Docker evolves, and unusual real-world files will continue to reveal cases that need clearer errors or additional checks.
The principle, however, is unlikely to change:
A deployment platform should understand the infrastructure it is about to execute—not merely pass it to Docker and hope for the best.
Nitrobase source code: https://github.com/AydenGroup/nitrobase