常规 Linux 容器通过守护进程所在宿主的 Linux 内核运行进程,并利用命名空间提供选定的隔离视图。镜像文件提供用户态环境,不会给每个容器单独启动一个内核。在 macOS 的 Docker Desktop 中,守护进程和 Linux 容器位于 Linux 虚拟机里,所以容器共享的是该虚拟机内核,而不是 macOS 内核。讨论“宿主”时必须说清楚这一层。

坏掉的样子

假定每个容器都有独立操作系统内核,会导致错误诊断。如果守护进程所在 Linux 环境缺少某个内核能力,仅换成包含另一发行版用户态文件的镜像,并不会让该能力出现。反过来,升级镜像内的软件包,也不会升级宿主内核。这个区别影响系统调用、内核模块、安全边界和资源统计,不能用容器里显示的发行版名字代替判断。

命名空间隔离可以配置,不是每次部署都完全相同的固定组合。容器可以加入其他网络命名空间、使用宿主网络、共享卷,或者采用不同的用户命名空间映射。看到容器内 UID 为零,也不能直接判断容器外权限;必须结合能力集合、用户映射、安全策略和暴露资源分析。一个数字或名称不足以描述完整安全边界。

localhost 也是常见误区。在独立网络命名空间下,127.0.0.1 指向调用容器自己的回环空间,不会自动到达兄弟服务或开发者电脑。同样,在一个普通容器的可写层创建文件,不会自动出现在使用同一镜像的另一个容器里。排查访问失败时,应先分清地址和文件究竟属于哪一个视图。

快速验证

取得固定的 Python 镜像后,使用宿主 Python 和 Docker 守护进程运行脚本。实测 Docker 为 29.3.1,Docker Desktop 虚拟机内 Linux 为 6.12.76,镜像内 Python 为 arm64 的 3.11.16。两个容器使用相同镜像、不同主机名,并设置无外部网络。结束时只删除这两个临时容器:

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)

两个容器报告相同 Linux 内核版本,PID、挂载、网络、UTS 和 IPC 命名空间标识却不同,并且各自都在 PID 一看到自己的 Python 进程。Docker 检查返回的守护进程侧进程号也不同。这些观察把容器内视图与同一个守护进程管理的独立进程连接起来;报告的宿主 PID 属于 Linux 环境,不是 macOS 进程编号。

分配的主机名也不同。UTS 命名空间控制这个视图,PID 命名空间控制进程编号可见性,挂载命名空间则控制挂载视图。它们承担不同职责。Linux 还有这里没有比较的命名空间类型,因此实验不能被解释成内核全部命名空间功能的完整清单,更不能把历史上的某个固定数量当成永远不变的事实。

第一个容器写入 /private-marker,第二个容器报告该路径不存在。实验没有挂载共享数据目录,因此差异体现普通容器各自的可写状态。如果给两个容器提供同一可写卷,通过该挂载看到什么,就由共享卷契约决定。隔离结论必须附带挂载条件,不能把有共享资源和没有共享资源的场景混在一起。

第一个容器的监听者接受自身回环端口 18080 上的连接。第二个容器无法连接同样的回环地址和端口,却可以在自己的网络命名空间绑定同一端口号。关闭外部网络没有删除回环网络,而是保留了各自独立的本地网络空间。这解释了为什么“容器内 localhost 能通”不能证明另一个容器也能访问服务。

本次两个进程都以 UID 零运行,并且用户命名空间标识相同。这是对当前守护进程默认配置的实际观察,不是声称 Docker 总会为每个容器创建独立用户命名空间。其他部署可能启用用户映射,或采用不同运行时模型,相关选择都会改变安全分析。正确做法是读取实际配置,而不是把一种安装默认值推广成通用规则。

检查真实配置的边界

诊断访问和隔离时,读取命名空间、挂载、网络、能力与用户设置,不要把容器名字当成某种边界已经成立的证明。资源上限是另一个维度:cgroup 可以限制合计 CPU、内存或任务数量,而命名空间决定视图。拥有隔离的进程空间,本身不会自动附带内存预算,也不会阻止应用消耗未设限的资源。

应用适合时,可以采用一个清晰的主要服务角色,但不要把这项惯例误当成内核强制每个容器只能有一个进程。服务可以启动工作进程,PID 一仍需要正确处理信号和回收子进程。监督方式应符合实际应用模型,而不是为了满足一句简化口号,忽略已有合理的多进程结构。

持久状态需要与数据寿命一致的存储;更强的租户隔离,则需要明确评估运行时和宿主信任模型。容器隔离确实存在,但保证来自真实配置的机制和受到维护的内核,而不是来自“每个镜像里面有一台独立小机器”的想象。明确边界,才能准确解释能力、故障与风险。

来源

  • namespaces(7),命名空间类型、隔离视图与命名空间身份,已核验 2026-09-12。
  • Docker container run,进程、网络、用户与权限设置,已核验 2026-09-12。
  • pid_namespaces(7),PID 编号及命名空间初始进程行为,已核验 2026-09-12。