Skip to content

Container runtime

Your service runs in a locked-down container: a read-only root filesystem, most Linux capabilities dropped, and fixed process limits. This page lists exactly what that means, and gives you one docker run that reproduces it on your own machine.

The short version

  • The container runs as the user your image declares — root, if the image doesn't set a USER, which is most images.
  • The root filesystem is read-only. You can write to /tmp, /var/tmp and any volume you attach.
  • All Linux capabilities are dropped and four are added back. Root is still uid 0, but root is not root for file permissions here: it can't write to a file it doesn't own just because it is root.

This applies to web services, private services, workers and cron jobs. Static sites are served by HostStack's own web server, and dev boxes have their own, looser sandbox.

Reproduce it locally

If an image works on your machine and fails on HostStack, run it with these flags first. They are the sandbox, in one command:

bash
docker run --rm -it \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --tmpfs /var/tmp:rw,noexec,nosuid,size=64m \
  --cap-drop ALL \
  --cap-add CHOWN --cap-add SETGID --cap-add SETUID --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --pids-limit 256 --ulimit nofile=1024:4096 \
  --memory 512m --memory-swap 512m \
  -e PORT=3000 -p 3000:3000 \
  your-image

Set --memory and --memory-swap to your service size's memory. Anything that fails under this command fails on HostStack too, and it is much faster to debug here than one deploy at a time.

The capability set

Every capability is dropped, then four are added back — enough for the usual entrypoint patterns: a chown of the app directory, a gosu or su-exec drop to an unprivileged user, and binding ports below 1024.

CapabilityStatusWhat it allows
CHOWNGrantedChanging a file’s owner.
SETUID, SETGIDGrantedSwitching to another user or group.
NET_BIND_SERVICEGrantedListening on ports below 1024.
DAC_OVERRIDEDroppedIgnoring file permission bits — what normally lets root write anywhere.
FOWNERDroppedChanging mode or timestamps of a file you don't own (chmod after a chown to someone else).
Everything elseDroppedRaw sockets, mounts, ptrace, module loading …

Without DAC_OVERRIDE and FOWNER, root can only write where the permission bits let it, and can only chmod files it owns. id -u still prints 0, which is why this is so easy to misread.

Where you can write

PathBehaviour
/tmp, /var/tmpIn-memory, 64 MB each, noexec, and emptied on every restart and deploy. The path is also in HOSTSTACK_TMP_DIR.
a volume’s mount pathWritable and kept across restarts and deploys. See Persistent volumes.
everything elseRead-only — including your application directory.

Because /tmp is noexec, a tool that downloads a binary there and runs it will fail. Put such binaries in the image at build time.

Errors this produces

None of these messages mentions capabilities or the read-only filesystem, and all of them come from it:

  • A chown succeeds and the chmod on the next line fails with Operation not permitted — under set -e, your entrypoint dies one line after doing the work correctly.
  • Permission denied writing a file the same script just created as a different user, for example file_put_contents(): Failed to open stream: Permission denied.
  • cp -a or tail denied on files the process's own start-up script created.
  • Read-only file system for any write outside /tmp, /var/tmp and your volumes — a cache directory, a PID file, a sessions/ folder next to the code.

Limits

LimitValue
MemoryYour service size’s memory. There is no swap: going over it kills the process.
CPUYour service size’s CPU, as a hard ceiling.
Processes and threads256 per container.
Open files1,024 soft, 4,096 hard (ulimit -n).
Privilege escalationno-new-privileges: setuid binaries can’t gain privileges.
System callsFiltered by a seccomp profile — at least Docker’s default one.
LogsWhatever the process writes to stdout and stderr, which you read under the service’s Logs.
HTTP requestsNo limit on how long a response takes. Request bodies and upload time are bounded at the edge — see Request size and time limits.

Networking and the port

Listen on 0.0.0.0 — not 127.0.0.1 — on the port in the PORT environment variable (3000 unless you change the service's port). A server bound to loopback can't be reached, and the deploy fails with the container never answered on port 3000. See Health checks.

The container can reach the internet and the other services and databases in its project over the project's private network. Nothing reaches it from outside except through HostStack's edge, on the service's URL and domains. See Networking.

What the container is given

Besides your own environment variables, HostStack sets these on every deploy:

PORT

The port to listen on. Always set, and it overrides a PORT you set yourself.

HOSTSTACK_SERVICE_NAME

The service's name.

HOSTSTACK_SERVICE_ID

The service's ID, svc_....

HOSTSTACK_PROJECT_ID

The project's numeric ID.

HOSTSTACK_TEAM_ID

The team's numeric ID.

HOSTSTACK_INTERNAL_URL

The address other services in the project reach this one on.

HOSTSTACK_DEPLOY_ID

The deploy that started this container, dpl_....

HOSTSTACK_BRANCH

The branch that was deployed.

HOSTSTACK_COMMIT_SHA

The deployed commit, with HOSTSTACK_COMMIT_SHORT_SHA and HOSTSTACK_RELEASE alongside it. Set when the commit is known.

HOSTSTACK_TMP_DIR

/tmp — the writable scratch directory.

Next steps