Compose files
The sanitiser — what is rejected outright, what the platform overwrites, and what it injects. The security boundary of the whole product.
Your file is never run as written
It is parsed, judged, and rewritten, and only the rewritten copy reaches Docker. The control plane holds the Docker socket, so a compose file it ran unmodified would be a root shell on the machine.
Three kinds of rule, applied in this order.
Reject
The deployment fails. There is no sanitised form of privileged: true, so there is nothing to
negotiate.
| Rejected | Why |
|---|---|
privileged | A privileged container has every capability on the host. |
userns_mode | Opts out of user-namespace remapping. |
devices | Mapping host devices reaches hardware outside the container. |
cap_add | Added capabilities widen what the container may do to the host. |
pid, ipc, uts, cgroup set to host | Host namespaces put the container in the machine's own. |
network_mode: host | Binds directly to the server's interfaces, bypassing every routing rule. |
network_mode: container:… | Joins another container's network namespace. |
security_opt weakening seccomp or AppArmor | Switches a confinement mechanism off. |
| Bind mounts resolving outside the application's own workspace | Mounting a host path into a container reads anything on the box. |
Build contexts or env_file paths resolving outside the workspace | The same, by a slightly longer route. |
A file that declares no services, or that is not a YAML mapping, is rejected too.
Every rule above has its own case in compose-sanitiser.spec.ts. That file is a security
regression suite: a case is never deleted to make a change pass.
Overwrite
Settings the platform owns regardless of what your file declared:
| Overwritten | To |
|---|---|
restart | unless-stopped |
| Logging driver and caps | json-file, capped by DEPLOY_CONTAINER_LOG_MAX_SIZE_MB (10 MB) across DEPLOY_CONTAINER_LOG_MAX_FILES (3) |
cpus, mem_limit | The application's resource settings, when set |
ports | Removed, then republished by the platform |
Every traefik.* label | Removed |
Why every Traefik label is stripped
Routing is derived from hostnames an operator proved they control. A compose file that could declare its own router could claim any hostname on the box, including the portal's.
Inject
| Injected | |
|---|---|
| The external proxy network | On the service that faces traffic |
| Router and service labels | On exactly that service and port, and only once a hostname is verified |
| Environment variables | As ${ST_ENV_<KEY>} interpolation references |
| Build arguments | As ${ST_ARG_<KEY>}, on the exposed service's build only |
| Named volumes | Mounted on the exposed service |
| The project name | st-<appSlug> — owned by the platform, never read from your file |
Why interpolation references rather than an env file
The rendered compose file contains ${ST_ENV_DATABASE_URL}, not your database URL. The value
is supplied through the environment of the docker compose process and is never written
to disk. An env_file would put every secret in a file on the server, which is exactly what
write-only variables exist to avoid.
Publishing
Your ports: entries are removed. What replaces them depends on the application:
| State | Published as |
|---|---|
| No hostname attached | A numbered host port in the range DEPLOY_HOST_PORT_MIN–DEPLOY_HOST_PORT_MAX (30000–32000), on every interface, so you can reach it |
| A verified hostname attached | Bound to 127.0.0.1 only — external traffic arrives through the proxy, and the platform keeps a local address to health-check |
| Blue/green | Nothing published; the router is the only way in |
Multi-service applications
They work. Name the service that faces traffic with exposedServiceName; the platform infers
it when the file declares exactly one. Services that are not the exposed one still get the
restart policy, the log caps, and the environment — they do not get a router.