Sthora
Deploying applications

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.

RejectedWhy
privilegedA privileged container has every capability on the host.
userns_modeOpts out of user-namespace remapping.
devicesMapping host devices reaches hardware outside the container.
cap_addAdded capabilities widen what the container may do to the host.
pid, ipc, uts, cgroup set to hostHost namespaces put the container in the machine's own.
network_mode: hostBinds directly to the server's interfaces, bypassing every routing rule.
network_mode: container:…Joins another container's network namespace.
security_opt weakening seccomp or AppArmorSwitches a confinement mechanism off.
Bind mounts resolving outside the application's own workspaceMounting a host path into a container reads anything on the box.
Build contexts or env_file paths resolving outside the workspaceThe 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:

OverwrittenTo
restartunless-stopped
Logging driver and capsjson-file, capped by DEPLOY_CONTAINER_LOG_MAX_SIZE_MB (10 MB) across DEPLOY_CONTAINER_LOG_MAX_FILES (3)
cpus, mem_limitThe application's resource settings, when set
portsRemoved, then republished by the platform
Every traefik.* labelRemoved

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 networkOn the service that faces traffic
Router and service labelsOn exactly that service and port, and only once a hostname is verified
Environment variablesAs ${ST_ENV_<KEY>} interpolation references
Build argumentsAs ${ST_ARG_<KEY>}, on the exposed service's build only
Named volumesMounted on the exposed service
The project namest-<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:

StatePublished as
No hostname attachedA numbered host port in the range DEPLOY_HOST_PORT_MINDEPLOY_HOST_PORT_MAX (30000–32000), on every interface, so you can reach it
A verified hostname attachedBound to 127.0.0.1 only — external traffic arrives through the proxy, and the platform keeps a local address to health-check
Blue/greenNothing 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.

On this page