Docker vs Linux Users for Multi-Tenant Workspace Isolation
Separate Linux users look like a cheap way to isolate tenants until you try to enforce a CPU limit. A walkthrough of why containers win for multi-tenant development workspaces — and how to verify the limits are real.
If you're building a platform that runs other people's code, the first design question is how to keep tenants apart. The cheapest-looking answer is the classic Unix one: give each tenant a Linux user account, set permissions carefully, and let the kernel do the rest.
It works, briefly. Then someone runs a build.
The case for Linux users
To be fair to the approach, it has real advantages:
- Nearly zero overhead. No container runtime, no image layers, no network namespaces. A user account costs a row in
/etc/passwd. - Simple mental model. File permissions are well understood and have been debugged for decades.
- Fast provisioning.
useraddreturns instantly. Pulling and unpacking an image does not. - Shared package cache by default. One copy of the base toolchain for everyone.
For a small number of trusted tenants — say, a handful of internal teams — this is a defensible choice. The problems appear at the boundaries.
Where it breaks
Resource limits are the hard part
This is the one that ends the debate. Unix users control access, not consumption. Nothing about a user account stops a tenant from spawning 200 processes or eating every core on the machine.
You can reach for ulimit, but it's per-process, not per-tenant — a fork bomb sails straight through a per-process CPU cap. The real answer is cgroups, and once you're writing systemd slices and cgroup hierarchies per tenant, you are building a container runtime by hand, badly, without the parts anyone has tested.
Containers give you this as a first-class flag. A limit of two CPUs is one argument, and you can confirm the kernel is honouring it from inside the workspace:
$ cat /sys/fs/cgroup/cpu.max
200000 100000
That reads as 200,000μs of CPU time per 100,000μs period — two full cores, enforced by the kernel. Memory and PID limits work the same way. Being able to verify the limit from inside the tenant environment matters: it means the constraint is real rather than a value in your control plane's database.
The shared filesystem is a slow leak
Every tenant on a shared host can see the same /, and quite a lot of it is world-readable by default. /etc/passwd enumerates your other customers. /proc exposes their process list and command lines — which frequently contain credentials passed as arguments. Misconfigured /tmp files show up regularly in real incidents.
You can harden all of this: hidepid, restrictive umasks, private /tmp per user, careful auditing of anything world-readable. What you cannot do is stop the next package update from adding a world-readable file with something interesting in it. You are maintaining a denylist against an operating system that assumes cooperating users.
Containers invert the default. Each workspace gets its own mount namespace and sees only what you put in the image plus its own volume. There is no enumeration problem because there is nothing to enumerate.
Dependency conflicts come back
Multi-tenancy on one host means one set of system libraries. Tenant A needs Python 3.11; tenant B needs 3.13. Tenant C needs a system OpenSSL that tenant D's runtime is incompatible with.
You end up managing per-user toolchain installs, which is most of the complexity of images with none of the reproducibility. Containers make the base environment a versioned artifact — you can offer a Node image, a Python image, a PHP image, and rebuild any of them without touching the others.
Recreation is destructive
When a user-account environment gets into a bad state, "start fresh" means deleting a home directory and hoping nothing outside it mattered. There is no clean line between the environment and the project data, because they live in the same tree.
With containers, that line is the whole design. Destroy the container, keep the volume:
/opt/easyspawn/workspaces/<user-id>/<project-id> → mounted at /workspace
The container is rebuildable from an image; the volume holds the source, git history, dependencies, and environment. Recreating a broken workspace becomes routine instead of frightening.
Escalation surface
A local user account on a shared host is one kernel bug or one SUID misconfiguration away from being every account on that host. That risk never disappears — container escapes exist too — but containers add layers: namespaces, capability dropping, seccomp profiles, read-only root filesystems, no privileged operations.
More importantly, containers make the default restrictive and the exceptions explicit. With user accounts, the default is permissive and you spend your time discovering what you forgot to lock down.
The comparison
| Concern | Linux users | Containers |
|---|---|---|
| CPU / memory limits | Manual cgroups | Built in, verifiable |
| Filesystem isolation | Permissions on a shared tree | Separate mount namespace |
| Process visibility | Visible via /proc by default |
Separate PID namespace |
| Dependency conflicts | One system toolchain | Per-image toolchains |
| Clean recreation | Delete home dir, hope | Destroy container, keep volume |
| Provisioning speed | Instant | Seconds (image pull, then cached) |
| Overhead | Minimal | Small but real |
| Escalation blast radius | Whole host | Container, then host |
| Path to orchestration | None | Kubernetes when you need it |
What you give up
Containers are not free, and pretending otherwise is how you end up surprised:
- Provisioning is slower on a cold image pull. Warm caches and a small set of base images matter.
- Storage multiplies. Layers plus per-tenant volumes use more disk than one shared toolchain.
- The runtime is a dependency. You now operate a container runtime, and its bugs are your bugs.
- Isolation is not a VM. Containers share a kernel. For genuinely hostile multi-tenancy, you eventually want microVMs — Firecracker, gVisor, Kata — underneath the containers. Containers are the right default; they are not the last word.
How to verify isolation is real
Whichever route you take, test it rather than trusting the config:
- Burn CPU. Run a busy loop with more threads than the limit allows and confirm from the host that the workspace is capped.
- Allocate past the memory limit. The process should be OOM-killed, and only that workspace should be affected.
- Fork bomb it. The PID limit should hold and the host should stay responsive.
- Try to enumerate. From inside a workspace, look for other tenants —
/proc,/etc/passwd, mount points, the Docker socket. You should find nothing. - Fill the disk. One tenant exhausting storage must not take down the others.
- Destroy and recreate. Confirm the project data survives and nothing else does.
If any of these surprise you, the isolation is aspirational.
The conclusion
Linux users answer "who can read this file." Multi-tenant platforms need to answer "how much of this machine can you take," and that is a different question with a different tool.
Containers give you enforceable limits, real filesystem isolation, per-tenant toolchains, and a clean separation between disposable runtime and durable data — plus a path to orchestration when one host stops being enough. The overhead is real and worth it.
EasySpawn runs each workspace in an isolated container with enforced CPU, memory, and process limits, as a non-root user, with project data on persistent storage. See how it works or join the waitlist.
Keep reading
The Sandbox Is the Wrong Abstraction for AI Coding Agents
The industry settled on ephemeral sandboxes for AI agents — isolated, disposable, destroyed after each task. That's exactly right for running untrusted code and exactly wrong for building software. Here's the distinction that matters.
Leaving Gitpod: Where Cloud Dev Environments Went in 2026
Gitpod Classic shut down in October 2025 and the product became Ona, an AI-agent platform on a new runtime. If that broke your workflow, here's an honest map of the alternatives and what to check before you migrate.