A conventional Linux container runs processes using the daemon host’s Linux kernel while providing selected isolated views through namespaces. Image files supply a userspace environment; they do not boot a separate kernel for each container. On Docker Desktop for macOS, the daemon and Linux containers run inside a Linux virtual machine, so the shared kernel is that VM’s kernel, not the macOS kernel.

When it breaks

Assuming that every container has an independent operating-system kernel leads to wrong diagnoses. A kernel feature missing from the daemon’s Linux environment does not appear merely because the image includes another distribution’s userspace files. Conversely, changing an image’s packages does not upgrade the host kernel. The distinction matters for system calls, kernel modules, security boundaries, and resource accounting.

Namespace isolation is configurable rather than an identical fixed bundle in every deployment. A container can join another network namespace, use host networking, share a volume, or receive different user-namespace mappings. Seeing UID zero inside a container does not by itself describe its authority outside the container. Capabilities, user mappings, security policies, and exposed resources must be considered together.

Localhost is another common source of confusion. With separate network namespaces, 127.0.0.1 reaches the calling container’s own loopback space. It does not automatically reach a sibling application or the developer’s laptop. Likewise, a file created in one ordinary container’s writable layer does not automatically become visible to another container from the same image.

Check it quickly

Run this host Python script with a Docker daemon and the pinned Python image available. The recorded Docker 29.3.1 environment used Linux 6.12.76 in Docker Desktop’s VM and arm64 Python 3.11.16. Both containers use the same image, receive separate hostnames, and have networking set to none. The script removes only its two temporary containers:

import json,subprocess,uuid

BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84'
def docker(*args,check=True):
    result=subprocess.run(['docker',*args],text=True,capture_output=True,timeout=120)
    if check:assert result.returncode==0,result.stderr
    return result
def inspect(name):return json.loads(docker('inspect',name).stdout)[0]
def execute(name,code):return docker('exec',name,'python','-c',code).stdout.strip()

name='xbcatm-isolation-'+uuid.uuid4().hex[:10];containers=[name+'-a',name+'-b']
try:
    listener='import socket,time;s=socket.socket();s.bind(("127.0.0.1",18080));s.listen();print("ready",flush=True);time.sleep(120)'
    for container,command in zip(containers,[listener,'import time;time.sleep(120)']):
        docker('run','-d','--name',container,'--hostname',container,'--network','none',BASE,'python','-c',command)
    probe='import json,os;print(json.dumps({"kernel":os.uname().release,"hostname":os.uname().nodename,"uid":os.getuid(),"namespaces":{name:os.readlink("/proc/self/ns/"+name) for name in ["pid","mnt","net","uts","ipc","user"]}}))'
    left,right=[json.loads(execute(container,probe)) for container in containers]
    assert left['kernel']==right['kernel']
    assert left['hostname']!=right['hostname']
    for namespace in ['pid','mnt','net','uts','ipc']:assert left['namespaces'][namespace]!=right['namespaces'][namespace]
    assert inspect(containers[0])['State']['Pid']!=inspect(containers[1])['State']['Pid']
    for container in containers:assert 'python' in execute(container,'from pathlib import Path;print(Path("/proc/1/cmdline").read_bytes())')
    execute(containers[0],'from pathlib import Path;Path("/private-marker").write_text("only-a")')
    assert execute(containers[1],'from pathlib import Path;print(Path("/private-marker").exists())')=='False'
    execute(containers[0],'import socket;s=socket.create_connection(("127.0.0.1",18080),timeout=1);s.close()')
    execute(containers[1],'import socket;s=socket.socket();assert s.connect_ex(("127.0.0.1",18080))!=0;s.close();s=socket.socket();s.bind(("127.0.0.1",18080));s.close()')
    print('same daemon-side Linux kernel:',left['kernel'],'with different PID, mount, network, UTS, and IPC namespace identities')
    print('each container sees its own PID 1, hostname, writable file state, and loopback port space')
    print('observed uid values:',left['uid'],right['uid'],'same user namespace:',left['namespaces']['user']==right['namespaces']['user'])
finally:docker('rm','-f',*containers,check=False)

Both containers report the same Linux kernel release. Their PID, mount, network, UTS, and IPC namespace identifiers differ, and each sees its own Python process at PID 1. Docker’s inspected daemon-side process IDs differ as well. These observations connect the container-local view with separate processes managed by the same daemon; the reported host IDs belong to the Linux environment.

The assigned hostnames also differ. UTS namespaces control that view, while PID namespaces control process-number visibility and mount namespaces control mount views. These mechanisms have distinct jobs. Linux has additional namespace types beyond those compared here, so the example should not be read as a complete inventory of all namespace features.

The first container writes /private-marker, and the second reports that the path is absent. The example does not mount a shared data directory, so the difference demonstrates ordinary per-container writable state. If both containers were given the same writable volume, visibility through that mount would follow the shared volume’s contract instead.

A listener in the first container accepts a connection to its own loopback port 18080. The second container cannot connect to that address and port, but can bind the same port number in its own network namespace. Disabling external networking has not removed loopback; it has left each container with a separate local network space.

Both recorded processes run as UID zero, and their user-namespace identities are equal in this daemon’s configuration. That is a deliberate observation of the actual defaults, not an assertion that Docker always creates a separate user namespace. Other deployments can enable user-namespace remapping or use a different runtime model, and those choices change the security analysis.

Inspect the boundary that was actually configured

Read namespace, mount, network, capability, and user settings when diagnosing access or isolation. Avoid treating a container name as proof of a particular boundary. Resource ceilings are another dimension: cgroups can limit aggregate CPU, memory, or task usage, while namespaces determine views. An isolated process namespace does not itself impose a memory budget.

Use one clear primary service role when it fits the application, but do not confuse that convention with a kernel rule requiring exactly one process. A service can start workers, and PID 1 still needs appropriate signal and child-reaping behavior. Process supervision should follow the actual application model.

For durable state, define storage whose lifecycle matches the data. For stronger tenant separation, assess the runtime and host trust model explicitly. Container isolation is real, but its guarantees come from the configured mechanisms and maintained kernel, not from imagining a small independent machine inside every image.

Sources

  • namespaces(7), namespace types, isolated views, and namespace identities, checked 2026-09-12.
  • Docker container run, process, network, user, and privilege settings, checked 2026-09-12.
  • pid_namespaces(7), PID numbering and namespace-init behavior, checked 2026-09-12.