Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 6 min read

What I Learned Letting Users Paste docker-compose.yml Into a PaaS

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

Supporting custom Docker Compose looked simple when I first thought about it:

  1. let the user paste a docker-compose.yml;
  2. save it on the VPS;
  3. run docker compose up -d;
  4. 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:

  1. against the source Compose model before deployment;
  2. against the fully resolved docker compose config --format json model, 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

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.