您刚停止了一个 Docker 容器以更新配置文件,但重启后发现所有数据都不见了。我也经历过这种情况。
之所以这样,是因为容器默认是短暂的(ephemeral)——当它们被 移除 时,写层中的任何数据都会消失。 真实的应用无法这样工作。您需要能持久化的数据库、能在重启后保留的配置文件,以及可实际访问的日志。
Docker 挂载通过将容器存储连接到外部位置来解决这个问题。主要有三种类型:用于生产数据的 volumes、用于开发流程的 bind mounts,以及将临时文件存于内存中的 tmpfs。
本文将带您了解如何为您的场景选择合适的挂载类型并正确实施。 要完整理解本文内容,您需要对 Docker 和容器化有一定掌握。可快速提升的课程: 我们的 Intermediate Docker 。
Docker 如何处理存储
Docker 容器使用分层文件系统,默认将一切视为临时内容。
当您构建镜像时,Dockerfile 中的每条指令都会创建一个新的只读层。这些层像叠牌一样堆叠在一起。如果您拉取一个安装了 Python 的 Ubuntu 镜像,您会得到一个基础 OS 层和一个 Python 层。
关键在于, 这些层全都是只读的。 您无法修改它们。
容器文件系统与可写层
启动容器时,Docker 会在顶部再加一层—— 可写容器层(writable layer)。
所有更改都发生在这里。您创建的每个文件、修改的每个配置、添加的每条表记录都位于此处。
听起来不错,但问题在于该层与容器生命周期绑定。
当您使用 docker rm 停止并移除容器时,可写层就没了。您所做的一切都消失。Docker 不会确认,直接全部删除。
这种设计对不需要在运行间保留状态的无状态应用而言是合理的。但真实应用并非无状态。
为什么需要挂载
对于生产环境而言,可写层有两个主要问题。
首先, 容器停止时您会丢失数据。 一个数据库容器如果在重启后忘掉所有行记录,又有何用?我想不出,除了给您的应用做集成测试。
其次, 您无法在容器之间共享数据。 比如您同时运行一个 Web 应用和一个后台任务处理器,两者都需要访问同一批文件。如果这些文件位于某个容器的可写层,另一个容器就看不到。
Docker 挂载通过将容器连接到容器生命周期之外的存储来解决这两个问题。您可以挂载主机上的目录或由 Docker 管理的 volume。即使容器被移除,数据也能保留。多个容器可以挂载到同一位置,并实时共享文件。
这就是您需要使用挂载的原因。接下来我们先概览挂载类型,然后我会演示它们如何工作。
Docker 挂载类型一览
Docker 提供三种处理持久化数据的方法,每种方法解决不同问题。以下是各类型的作用及适用时机。
Volumes
Volumes 是 Docker 对持久化存储的默认答案。
Docker 会在主机上的专用目录中为您创建并管理 volumes。您不必知道目录位置,一切由 Docker 处理。这使得 volumes 在不同系统间具有可移植性,并且可安全用于生产。
当您移除容器时,volume 保持不变。如果您启动一个新容器并附加同一个 volume,所有数据仍在原处。
Volumes 最适合生产数据库、应用状态,以及任何不能丢失的数据。
Bind mounts
Bind mounts 将主机上的特定目录直接连接到容器中。
您在主机上选择确切路径——例如 /home/user/project——Docker 将其映射到容器中。您在主机上修改文件,容器会立即看到变化;在容器中修改文件,主机也会同步显示。
这种 实时同步 让 bind mounts 非常适合开发。
但 bind mounts 也有风险。它们会将主机路径暴露给容器,并依赖具体的目录结构,这些结构可能在其他机器上并不存在。
tmpfs 挂载
tmpfs 挂载将数据存储在主机内存中,而不是磁盘上。
不会有任何内容写入文件系统。容器停止时,数据会彻底消失。因此 tmpfs 挂载适用于 不希望持久化的临时数据 ——例如认证令牌、会话数据或反正会重建的缓存文件。
需要说明的是,tmpfs 受限于可用内存,并且仅在 Linux 主机上工作。
用于持久化数据的 Docker Volumes
Volumes 是 Docker 面向生产的存储方案,除非有特定理由,否则应优先使用它们。
它们完全由 Docker 管理,在不同平台上表现一致,且按设计在容器移除后仍然存在。如果您运行数据库、存储应用状态,或处理任何需要超越单个容器生命周期的数据,volumes 就是答案。
Docker volumes 的工作原理
Docker 在主机上的专用目录中存储 volumes:
Linux:/var/lib/docker/volumes/
macOS:~/Library/Containers/com.docker.docker/Data/vms/0/data/
Windows:\\wsl$\docker-desktop-data\data\docker\volumes\(假设使用 WSL2 后端)
您无需直接管理这个目录。Docker 通过自身 API 处理创建、权限和清理。由于这种隔离,volumes 在 Linux、Mac 和 Windows 上的行为一致,从而让您的容器配置可在开发与生产环境间移植。
需要记住的是,volumes 独立于任何容器而存在。创建一个 volume、将其附加到容器、运行应用,然后停止并删除容器,volume 仍保持原样,数据完好无损。
如果您启动新容器并附加同一个 volume,数据依然存在。
创建和复用 volumes
您可以在启动任何容器之前创建一个命名 volume:
docker volume create mydata技术支持
然后在运行容器时通过 --mount 标志将其附加:
docker run -d \
--name postgres-db \
--mount source = mydata,target= /var/lib/postgresql/data \
postgres:18技术支持
这会将 mydata volume 挂载到容器内的 /var/lib/postgresql/data,Postgres 在此存储其数据库文件。
现在您可以停止并移除该容器,然后使用同一 volume 启动一个新容器:
docker rm -f postgres-db
docker run -d \
--name postgres-db-new \
--mount source = mydata,target= /var/lib/postgresql/data \
postgres:18技术支持
您的数据库会带着所有表和行回归。
这正是 volumes 的意义——跨容器生命周期的数据持久化。
管理与维护 volume 数据
您可以运行此命令查看系统中存在的 volumes:
docker volume ls 技术支持
然后,您可以运行此命令检查特定 volume,查看其存储位置以及使用它的容器:
docker volume inspect mydata技术支持
这会显示主机上的挂载点和有用的元数据。但您很少需要直接访问该目录——那是 Docker 的工作。
如果您不再需要该 volume 并希望删除它,只需运行:
docker volume rm mydata技术支持
对于附加到正在运行容器的 volume,Docker 不会允许删除。请先停止容器,然后再移除 volume。
最后,如果您想清理资源并回收磁盘空间,可以运行以下命令一次性删除所有未使用的 volumes:
docker volume prune技术支持
在生产环境中,Docker 支持将 volume 驱动程序连接到外部存储系统,如 NFS、AWS EFS 或云块存储。您在创建 volume 时指定驱动,剩余工作由 Docker 处理。这样您可将数据完全存储在主机之外,对于容器在服务器间迁移的高可用场景尤为重要。
接下来,我们讨论 bind mounts。
面向本地开发的 Bind Mounts
Bind mounts 让您能从容器内直接访问主机文件系统,这正是开发者喜爱的原因。
它们非常适合本地开发,但也有让其在生产中有风险的权衡。您获得实时文件同步和零构建步骤,但会失去可移植性,并可能带来安全漏洞。
Bind mounts 的工作原理
Bind mount 将主机上的特定目录直接映射到容器中。
您指定确切路径——如 /home/user/myapp——Docker 会在容器内提供对应路径。无需拷贝、没有 Docker 管理的存储、也没有抽象层。容器看到的就是主机上的真实文件。
您在主机上修改文件,容器会立刻看到变化。同样,您在容器内修改文件,主机也会更新。两边实时操作同一批文件。
下面是一个 bind mount 示例:
docker run -d \
--name dev-app \
--mount type = bind,source= /Users/dradecic/Desktop/app,target= /app \
python:3.14技术支持
这会将主机的 /Users/dradecic/Desktop/app 挂载到容器内的 /app。当我在 /Users/dradecic/Desktop/app 中使用文本编辑器编辑 Python 文件,容器化应用会立即看到改变。
您也可以使用更短的语法:
常见开发工作流
最常见的用例是在开发期间挂载源代码。
比如您在构建一个 FastAPI 应用。您可以将项目目录挂载到容器中,启用热重载,就得到一个完整的开发环境:
docker run -d \
--name fastapi-dev \
--mount type = bind,source= /Users/dradecic/Desktop/app,target= /app \
-w /app \
-p 8000 :8000 \
python:3.14 \
sh -c "pip install fastapi uvicorn && uvicorn main:app --reload --host 0.0.0.0" 技术支持
仅供参考,这是我的 main.py 文件:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(
title = "FastAPI Docker Demo" ,
description = "A minimal FastAPI app running inside Docker" ,
version = "1.0.0" ,
)
class Item( BaseModel) :
name: str
price: float
in_stock: bool = True
@app.get( "/" )
def read_root( ) :
return {
"message" : "FastAPI is running" ,
"docs" : "/docs" ,
"redoc" : "/redoc" ,
}
@app.get( "/health" )
def health_check( ) :
return { "status" : "ok" }
@app.post( "/items" )
def create_item( item: Item) :
return {
"message" : "Item received" ,
"item" : item,
} 技术支持
运行该 Docker 命令后,应用可在主机的 8000 端口访问:
如果您在编辑器中修改并保存 main.py,FastAPI 会自动重载。无需重建镜像、无需重启容器。您像本地一样编写代码,但应用运行在一致的容器环境中。
风险与限制
Bind mounts 会将主机文件系统暴露给容器,这可能带来安全问题。
带有 bind mount 的容器可以读取并写入主机上的文件。以 root 身份运行容器——这是默认设置——则对挂载的文件拥有 root 级别的访问权限。恶意代码或被攻陷的容器可以修改或删除挂载目录中的任何内容。
可移植性也是一个问题。
Bind mounts 依赖主机上存在的特定路径。/Users/dradecic/Desktop/app 在您的机器或生产服务器上可能不存在。这打破了容器“到处可用”的承诺。
平台差异也存在。Windows 和 Mac 通过 VM 运行 Docker,因此 bind mounts 需要经过额外的转换层。这会让文件操作变慢,并可能导致文件监听和符号链接的微妙问题。
生产环境切勿使用 bind mounts。
它们过于依赖主机特定路径,安全风险高,且无法进行版本控制。Volumes 解决了这些问题,因此是生产标准。
将 bind mounts 视为开发工具——快速、便捷、强大,但不应出现在生产环境。
用于临时数据的 tmpfs 挂载
tmpfs 挂载将数据存储在主机的 RAM 中而非磁盘。因此非常适合不需要持久化的数据。
内存中存储的行为
tmpfs 挂载完全存在于内存中。
Docker 在主机上分配 RAM,并将其作为文件系统提供给容器。写入 tmpfs 挂载的文件永远不会接触磁盘。数据会驻留在内存中,直到容器停止。
当您停止容器时,tmpfs 挂载中的一切都会被删除。无需清理,没有残留文件,也没有任何痕迹。每次重启容器,您都会得到一个干净的空 tmpfs 挂载。
简而言之,tmpfs 挂载适用于您明确不想保留的数据——临时计算、会话令牌或使用后不应持久化的敏感信息。
典型用例
最常见的用例是存储机密或敏感数据。
比如您运行的容器需要 API 密钥或数据库密码。将其存储在 tmpfs 挂载中,机密将永不落盘。容器停止时,内存中的机密随之消失。不会有文件被意外提交到版本库,或暴露在文件系统上。
缓存也是不错的选择。构建产物、编译代码或可重新下载的依赖本就无需持久化。将它们放在 tmpfs 中,可在容器生命周期内获得更快访问,结束后自动消失。
临时文件也很合适——如会话数据、锁文件或仅在容器运行期间有意义的中间处理结果。
基础配置
使用 --tmpfs 标志运行此命令以创建 tmpfs 挂载:
docker run -d \
--name temp-app \
--tmpfs /tmp:rw,size= 100m \
python:3.14技术支持
这会在容器内的 /tmp 创建一个 100MB 的 tmpfs 挂载。size 选项用于限制挂载能使用的 RAM 大小。
您也可以使用 --mount 语法:
docker run -d \
--name temp-app \
--mount type = tmpfs,destination= /tmp,tmpfs-size= 104857600 \
python:3.14技术支持
tmpfs-size 的单位是字节——104857600 字节等于 100MB。
如果您不指定大小限制,tmpfs 会使用系统 RAM 的一半。这很危险——原因显而易见。务必设置明确的大小限制。
唯一的大缺点是, tmpfs 挂载只在 Linux 上有效。
Mac 和 Windows 的 Docker Desktop 不支持它们,因为 Docker 运行在 Linux VM 中,而 tmpfs 需要内核直接支持。
Docker 挂载语法与配置
Docker 提供两种定义挂载的方式,选择合适的语法能让您的命令更易读、更易调试。
两种方法都可用,但当您需要高级选项或在单个容器中使用多个挂载时,其中一种更具可扩展性。
--mount 与 --volume 的比较
--mount 使用显式的键值对,而 -v 或 --volume 使用冒号分隔的字符串。
下面用两种语法实现同一个 volume 挂载:
docker run -d \
--mount type = volume,source= mydata,target= /app/data \
python:3.14
docker run -d \
-v mydata:/app/data \
python:3.14技术支持
两者都会创建名为 mydata 的 volume,并将其挂载到容器内的 /app/data。
除基础场景外,请优先使用 --mount 。它更啰嗦,但显式键能清楚表达各部分含义。当您添加只读访问或自定义 volume 驱动等选项时,它依然清晰,而 -v 可能看起来像一串难懂的字符。
-v 语法适用于您手动输入的简单开发工作流。
常见挂载选项
readonly 选项可阻止容器修改被挂载的数据:
docker run -d \
--mount type = volume,source= mydata,target= /app/data,readonly \
python:3.14技术支持
这对配置文件或容器只应读取的参考数据很有用。尝试写入只读挂载的容器会收到权限错误。
对于 volumes,volume-nocopy 选项会跳过将镜像中挂载点现有数据复制到 volume:
docker run -d \
--mount type = volume,source= mydata,target= /app/data,volume-nocopy \
python:3.14技术支持
默认情况下,Docker 会在新 volume 上复制镜像中挂载点处的内容。设置 volume-nocopy 后,无论镜像里有什么,您都会得到一个空的 volume。
对于 tmpfs,tmpfs-size 选项用于设置内存上限:
docker run -d \
--mount type = tmpfs,target= /tmp,tmpfs-size= 104857600 \
python:3.14技术支持
这会将 tmpfs 挂载的大小限制为 100MB。若不设置,tmpfs 可能会耗尽所有可用 RAM。
在现有数据之上进行挂载
当您将挂载目标设为镜像中已存在的目录时,挂载会完全遮蔽那里的任何内容。
假设镜像中有一个包含配置文件的 /app/data 目录。当您将 volume 挂载到 /app/data 时,这些配置文件会“消失”。容器只能看到 volume 中的内容。
此行为适用于所有挂载类型——volumes、bind mounts 和 tmpfs。挂载内容具有更高优先级,挂载激活期间原目录不可访问。
在 Docker Compose 中使用挂载
Docker Compose 让您可以轻松在应用栈的多个容器间定义和共享挂载。
无需输入冗长的带挂载标志的 docker run 命令,您只需在 docker-compose.yml 文件中声明一切。让我来演示。
在 Compose 中定义 volumes 和 bind mounts
下面是一个同时包含 volume 与 bind mount 的 Compose 文件示例:
services:
service-1:
image: ubuntu:latest
command: sleep infinity
volumes:
- ./code:/app
- shared:/data
service-2:
image: ubuntu:latest
command: sleep infinity
volumes:
- shared:/data
volumes:
shared:技术支持
volumes 键在每个服务下定义要挂载的内容。像 ./code 这样的相对路径会创建 bind mount,而像 shared 这样的名称则引用命名 volume。
顶层的 volumes 部分声明了由 Compose 创建和管理的命名 volumes。service-1 和 service-2 都挂载了同一个 shared volume,因此它们看到的是同一批文件。从一个容器写入文件,另一个容器可立即读取。
sleep infinity 命令用于让容器保持运行,以便您可以进入执行——仅用于演示。
持久化与验证数据
使用 docker compose up -d 启动您的栈,然后检查挂载是否生效:
docker compose exec service-1 sh -c "echo 'test' > /data/file.txt"
docker compose exec service-2 cat /data/file.txt技术支持
若两条命令都能正常运行,说明您的 volumes 配置正确。
现在您可以运行以下命令停止并移除所有内容:
docker compose down技术支持
您的命名 volumes 仍会保留。如果再次使用 docker compose up -d 启动栈,此前写入的数据依然存在。 这正是数据库在部署间持久化的方式 ——volume 的生命周期长于容器。
若您希望在停止栈时一并删除 volumes,请添加 -v 标志:
docker compose down -v 技术支持
这会移除 Compose 文件中定义的所有 volumes。需要清空环境时使用。
用初始数据填充 volumes
最常见的模式是使用单独的初始化容器为共享 volume 注入种子数据:
services:
init:
image: ubuntu:latest
command: sh -c "mkdir -p /source && echo 'initial data' > /source/seed.txt && cp /source/* /dest/"
volumes:
- shared:/dest
service-1:
image: ubuntu:latest
command: sleep infinity
depends_on:
- init
volumes:
- shared:/data
service-2:
image: ubuntu:latest
command: sleep infinity
depends_on:
- init
volumes:
- shared:/data
volumes:
shared:技术支持
init 容器创建种子数据并将其拷贝进 shared volume,然后退出。service-1 与 service-2 随后启动,能直接使用这些种子数据。
Compose 在单个可版本控制文件中处理协调多个容器及其共享存储的复杂性。
Docker 挂载的性能与安全
选错挂载类型会拖慢容器,或带来您未意识到的安全漏洞。如果不想遇到这些问题,请阅读本节。
不同挂载类型的性能权衡
在 Linux 上,volumes 提供最佳性能,因为它们直接存储在主机文件系统上,无需转换层。
在 Mac 和 Windows 上,Docker 运行在 Linux VM 中。volumes 依然表现良好,因为它们留在 VM 内。相反,bind mounts 需要在您的主机 OS 与 Linux VM 之间同步文件,这会增加开销。在 Mac 和 Windows 上,bind mounts 的文件操作会比原生 Linux 明显更慢。
tmpfs 在读写操作上最快,因为一切都在 RAM 中进行。没有磁盘 I/O,也没有文件系统开销。但它受限于可用内存,且在容器停止时数据会消失。
如果您在 Linux 上追求极致性能,请使用 volumes。如果您在 Mac 或 Windows 上开发并注意到文件操作缓慢,很可能是 bind mount 的开销。生产工作负载请切换到 volumes。
挂载的安全影响
每次挂载都会让容器访问其隔离文件系统之外的内容,这会产生风险。
Bind mounts 的风险最大。如果您将 /home/user 挂载进容器,被攻陷的容器可以读取您的 SSH 密钥、修改 Shell 配置,或删除整个主目录中的文件。若该容器以 root 运行(默认),它对这些文件拥有 root 级访问。
Volumes 会降低这类风险,因为它们被隔离在 Docker 的存储目录中。容器无法通过 volumes 任意挂载主机路径。但若您随意共享 volumes,也可能在容器间泄露数据。
tmpfs 挂载可最小化持久化风险——存储在内存中的机密会在容器停止时消失。但它们无法防御运行期攻击,即被攻陷的容器从内存中读取机密。
总的规则是, 挂载会打破容器隔离, 因此要谨慎使用。
安全挂载的最佳实践
只挂载容器所需的最小范围,且不要超出。
与其挂载整个项目目录,不如只挂载容器实际使用的子目录。与其以写权限挂载 /var/log,若容器只需要读取日志,请以只读方式挂载。
尽可能使用 readonly 选项:
docker run -d \
--mount type = bind,source= /app/config,target= /config,readonly \
ubuntu:latest技术支持
这可防止容器修改挂载数据,若容器被攻陷也能降低破坏范围。
以非 root 用户运行容器,以降低 bind mount 漏洞的影响。在 Dockerfile 中创建用户并在容器启动前切换:
RUN useradd -m appuser
USER appuser技术支持
定期使用 docker volume prune 清理未使用的 volumes。旧的 volumes 会随着时间堆积,既占用磁盘空间,也可能存有已删除容器的敏感数据。
切勿挂载诸如 /、/etc 或 /var 等敏感主机目录,除非您有特定理由且充分理解风险。每个挂载都应有明确目的和最小范围。
排查 Docker 挂载问题
挂载问题通常表现为权限错误、文件缺失或容器无法启动——几乎总是由同一类问题引起。
以下是诊断与修复最常见问题的方法。
权限与所有权错误
当容器内的用户无权访问被挂载的文件时,就会发生权限错误。
Docker 容器默认以 root 运行。root 在 bind mount 中创建的文件在主机上归 root 所有。若您尝试以常规用户编辑,会得到权限拒绝错误。
反过来也一样。将您拥有的目录挂载到以非 root 用户运行的容器中,容器可能无法写入。
您可以在挂载目录上使用 ls -la 检查文件所有权:
ls -la /path/to/mounted/directory技术支持
如果文件归 root 所有,而您的容器以其他用户运行,那就是不匹配。通过以拥有这些文件的同一用户运行容器来修复:
docker run -d \
--user $( id -u ) : $( id -g ) \
-v ./data:/app/data \
ubuntu:latest技术支持
这会以当前主机用户而非 root 运行容器,从而与 bind mount 中文件的所有权匹配。
对于 volumes,当容器创建文件时 Docker 会自动处理权限。但如果您遇到错误,请检查容器化应用以哪个用户运行,以及其是否对挂载点有写权限。
路径与配置错误
最常见的错误是挂载了主机上不存在的路径。
如果尝试挂载不存在的 /home/user/project,Docker 会创建一个空目录并归 root 所有。容器能启动,但挂载的是错误的东西——一个空目录而非您的实际项目。
挂载前务必验证路径是否存在:
ls /home/user/project技术支持
如果目录不存在,请先创建,或修正挂载命令中的路径。
在 Docker Compose 中,相对路径会相对于包含 docker-compose.yml 的目录解析。若您的文件在 /home/user/app/,并使用了 ./data,Docker 会查找 /home/user/app/data。
移动 Compose 文件,挂载就会出问题。
另一个常见错误是在容器内挂载到了错误的目标路径。若挂载到 /app/data,但您的应用期望 /data,应用就找不到文件。请检查应用文档或 Dockerfile,确认期望的数据路径。
平台特有的怪癖
在 Linux 上,bind mounts 直接与主机文件系统交互。
在 Mac 和 Windows 上,Docker 运行在 Linux VM 中。Bind mounts 需要在主机 OS 与该 VM 间同步文件,导致时序问题。文件监听器(在文件变更时重载应用的工具)有时会因同步延迟而错过更新。
Mac 和 Windows 处理文件权限的方式也不同。VM 会在主机 OS 与 Linux 之间转换权限,可能导致容器内显示出不正确的文件所有权。
在 Mac 和 Windows 上,bind mounts 中的符号链接并不可靠。VM 并不总能解析指向挂载目录之外的符号链接,因此容器内会出现文件缺失或损坏的情况。
在 Mac 和 Windows 上,tmpfs 挂载根本无法工作,因为 VM 不会将 tmpfs 暴露给主机。尝试使用 tmpfs 挂载时,Docker 可能会静默忽略,或根据版本抛出错误。
如果您在 Mac 或 Windows 上开发并遇到奇怪的文件同步问题,请切换到命名 volumes 以获得更好的性能和可靠性。将 bind mounts 留给对实时同步要求高、且可容忍一致性问题的开发工作流。
结语
总而言之,面向需要持久化的生产数据(如数据库、上传文件、应用状态——任何您无法承受丢失的内容)请使用 volumes。它们由 Docker 管理,跨平台可移植,是处理重要数据的最安全选择。
Bind mounts 适用于开发工作流,当您在编辑器中修改代码时,容器化应用会立刻看到变化。但请将它们排除在生产之外,因为它们过于依赖主机特定路径,并带来不必要的安全风险。
当您准备更深入学习容器化与虚拟化时,请查看我们的课程:Containerization and Virtualization with Docker and Kubernetes 。
您刚停止了一个 Docker 容器以更新配置文件,但重启后发现所有数据都不见了。我也经历过这种情况。
之所以这样,是因为容器默认是短暂的(ephemeral)——当它们被移除时,写层中的任何数据都会消失。真实的应用无法这样工作。您需要能持久化的数据库、能在重启后保留的配置文件,以及可实际访问的日志。
Docker 挂载通过将容器存储连接到外部位置来解决这个问题。主要有三种类型:用于生产数据的 volumes、用于开发流程的 bind mounts,以及将临时文件存于内存中的 tmpfs。
本文将带您了解如何为您的场景选择合适的挂载类型并正确实施。要完整理解本文内容,您需要对 Docker 和容器化有一定掌握。可快速提升的课程:我们的 Intermediate Docker。
Docker 如何处理存储
Docker 容器使用分层文件系统,默认将一切视为临时内容。
当您构建镜像时,Dockerfile 中的每条指令都会创建一个新的只读层。这些层像叠牌一样堆叠在一起。如果您拉取一个安装了 Python 的 Ubuntu 镜像,您会得到一个基础 OS 层和一个 Python 层。
关键在于,这些层全都是只读的。您无法修改它们。
容器文件系统与可写层
启动容器时,Docker 会在顶部再加一层——可写容器层(writable layer)。
所有更改都发生在这里。您创建的每个文件、修改的每个配置、添加的每条表记录都位于此处。
听起来不错,但问题在于该层与容器生命周期绑定。
当您使用
docker rm停止并移除容器时,可写层就没了。您所做的一切都消失。Docker 不会确认,直接全部删除。这种设计对不需要在运行间保留状态的无状态应用而言是合理的。但真实应用并非无状态。
为什么需要挂载
对于生产环境而言,可写层有两个主要问题。
首先,容器停止时您会丢失数据。一个数据库容器如果在重启后忘掉所有行记录,又有何用?我想不出,除了给您的应用做集成测试。
其次,您无法在容器之间共享数据。比如您同时运行一个 Web 应用和一个后台任务处理器,两者都需要访问同一批文件。如果这些文件位于某个容器的可写层,另一个容器就看不到。
Docker 挂载通过将容器连接到容器生命周期之外的存储来解决这两个问题。您可以挂载主机上的目录或由 Docker 管理的 volume。即使容器被移除,数据也能保留。多个容器可以挂载到同一位置,并实时共享文件。
这就是您需要使用挂载的原因。接下来我们先概览挂载类型,然后我会演示它们如何工作。
Docker 挂载类型一览
Docker 提供三种处理持久化数据的方法,每种方法解决不同问题。以下是各类型的作用及适用时机。
Volumes
Volumes 是 Docker 对持久化存储的默认答案。
Docker 会在主机上的专用目录中为您创建并管理 volumes。您不必知道目录位置,一切由 Docker 处理。这使得 volumes 在不同系统间具有可移植性,并且可安全用于生产。
当您移除容器时,volume 保持不变。如果您启动一个新容器并附加同一个 volume,所有数据仍在原处。
Volumes 最适合生产数据库、应用状态,以及任何不能丢失的数据。
Bind mounts
Bind mounts 将主机上的特定目录直接连接到容器中。
您在主机上选择确切路径——例如
/home/user/project——Docker 将其映射到容器中。您在主机上修改文件,容器会立即看到变化;在容器中修改文件,主机也会同步显示。这种实时同步让 bind mounts 非常适合开发。
但 bind mounts 也有风险。它们会将主机路径暴露给容器,并依赖具体的目录结构,这些结构可能在其他机器上并不存在。
tmpfs 挂载
tmpfs 挂载将数据存储在主机内存中,而不是磁盘上。
不会有任何内容写入文件系统。容器停止时,数据会彻底消失。因此 tmpfs 挂载适用于不希望持久化的临时数据——例如认证令牌、会话数据或反正会重建的缓存文件。
需要说明的是,tmpfs 受限于可用内存,并且仅在 Linux 主机上工作。
用于持久化数据的 Docker Volumes
Volumes 是 Docker 面向生产的存储方案,除非有特定理由,否则应优先使用它们。
它们完全由 Docker 管理,在不同平台上表现一致,且按设计在容器移除后仍然存在。如果您运行数据库、存储应用状态,或处理任何需要超越单个容器生命周期的数据,volumes 就是答案。
Docker volumes 的工作原理
Docker 在主机上的专用目录中存储 volumes:
/var/lib/docker/volumes/~/Library/Containers/com.docker.docker/Data/vms/0/data/\\wsl$\docker-desktop-data\data\docker\volumes\(假设使用 WSL2 后端)您无需直接管理这个目录。Docker 通过自身 API 处理创建、权限和清理。由于这种隔离,volumes 在 Linux、Mac 和 Windows 上的行为一致,从而让您的容器配置可在开发与生产环境间移植。
需要记住的是,volumes 独立于任何容器而存在。创建一个 volume、将其附加到容器、运行应用,然后停止并删除容器,volume 仍保持原样,数据完好无损。
如果您启动新容器并附加同一个 volume,数据依然存在。
创建和复用 volumes
您可以在启动任何容器之前创建一个命名 volume:
然后在运行容器时通过
--mount标志将其附加:这会将
mydatavolume 挂载到容器内的/var/lib/postgresql/data,Postgres 在此存储其数据库文件。现在您可以停止并移除该容器,然后使用同一 volume 启动一个新容器:
您的数据库会带着所有表和行回归。
这正是 volumes 的意义——跨容器生命周期的数据持久化。
管理与维护 volume 数据
您可以运行此命令查看系统中存在的 volumes:
然后,您可以运行此命令检查特定 volume,查看其存储位置以及使用它的容器:
这会显示主机上的挂载点和有用的元数据。但您很少需要直接访问该目录——那是 Docker 的工作。
如果您不再需要该 volume 并希望删除它,只需运行:
对于附加到正在运行容器的 volume,Docker 不会允许删除。请先停止容器,然后再移除 volume。
最后,如果您想清理资源并回收磁盘空间,可以运行以下命令一次性删除所有未使用的 volumes:
在生产环境中,Docker 支持将 volume 驱动程序连接到外部存储系统,如 NFS、AWS EFS 或云块存储。您在创建 volume 时指定驱动,剩余工作由 Docker 处理。这样您可将数据完全存储在主机之外,对于容器在服务器间迁移的高可用场景尤为重要。
接下来,我们讨论 bind mounts。
面向本地开发的 Bind Mounts
Bind mounts 让您能从容器内直接访问主机文件系统,这正是开发者喜爱的原因。
它们非常适合本地开发,但也有让其在生产中有风险的权衡。您获得实时文件同步和零构建步骤,但会失去可移植性,并可能带来安全漏洞。
Bind mounts 的工作原理
Bind mount 将主机上的特定目录直接映射到容器中。
您指定确切路径——如
/home/user/myapp——Docker 会在容器内提供对应路径。无需拷贝、没有 Docker 管理的存储、也没有抽象层。容器看到的就是主机上的真实文件。您在主机上修改文件,容器会立刻看到变化。同样,您在容器内修改文件,主机也会更新。两边实时操作同一批文件。
下面是一个 bind mount 示例:
这会将主机的
/Users/dradecic/Desktop/app挂载到容器内的/app。当我在/Users/dradecic/Desktop/app中使用文本编辑器编辑 Python 文件,容器化应用会立即看到改变。您也可以使用更短的语法:
常见开发工作流
最常见的用例是在开发期间挂载源代码。
比如您在构建一个 FastAPI 应用。您可以将项目目录挂载到容器中,启用热重载,就得到一个完整的开发环境:
仅供参考,这是我的
main.py文件:运行该 Docker 命令后,应用可在主机的 8000 端口访问:
如果您在编辑器中修改并保存
main.py,FastAPI 会自动重载。无需重建镜像、无需重启容器。您像本地一样编写代码,但应用运行在一致的容器环境中。风险与限制
Bind mounts 会将主机文件系统暴露给容器,这可能带来安全问题。
带有 bind mount 的容器可以读取并写入主机上的文件。以 root 身份运行容器——这是默认设置——则对挂载的文件拥有 root 级别的访问权限。恶意代码或被攻陷的容器可以修改或删除挂载目录中的任何内容。
可移植性也是一个问题。
Bind mounts 依赖主机上存在的特定路径。
/Users/dradecic/Desktop/app在您的机器或生产服务器上可能不存在。这打破了容器“到处可用”的承诺。平台差异也存在。Windows 和 Mac 通过 VM 运行 Docker,因此 bind mounts 需要经过额外的转换层。这会让文件操作变慢,并可能导致文件监听和符号链接的微妙问题。
生产环境切勿使用 bind mounts。
它们过于依赖主机特定路径,安全风险高,且无法进行版本控制。Volumes 解决了这些问题,因此是生产标准。
将 bind mounts 视为开发工具——快速、便捷、强大,但不应出现在生产环境。
用于临时数据的 tmpfs 挂载
tmpfs 挂载将数据存储在主机的 RAM 中而非磁盘。因此非常适合不需要持久化的数据。
内存中存储的行为
tmpfs 挂载完全存在于内存中。
Docker 在主机上分配 RAM,并将其作为文件系统提供给容器。写入 tmpfs 挂载的文件永远不会接触磁盘。数据会驻留在内存中,直到容器停止。
当您停止容器时,tmpfs 挂载中的一切都会被删除。无需清理,没有残留文件,也没有任何痕迹。每次重启容器,您都会得到一个干净的空 tmpfs 挂载。
简而言之,tmpfs 挂载适用于您明确不想保留的数据——临时计算、会话令牌或使用后不应持久化的敏感信息。
典型用例
最常见的用例是存储机密或敏感数据。
比如您运行的容器需要 API 密钥或数据库密码。将其存储在 tmpfs 挂载中,机密将永不落盘。容器停止时,内存中的机密随之消失。不会有文件被意外提交到版本库,或暴露在文件系统上。
缓存也是不错的选择。构建产物、编译代码或可重新下载的依赖本就无需持久化。将它们放在 tmpfs 中,可在容器生命周期内获得更快访问,结束后自动消失。
临时文件也很合适——如会话数据、锁文件或仅在容器运行期间有意义的中间处理结果。
基础配置
使用
--tmpfs标志运行此命令以创建 tmpfs 挂载:这会在容器内的
/tmp创建一个 100MB 的 tmpfs 挂载。size选项用于限制挂载能使用的 RAM 大小。您也可以使用
--mount语法:tmpfs-size的单位是字节——104857600 字节等于 100MB。如果您不指定大小限制,tmpfs 会使用系统 RAM 的一半。这很危险——原因显而易见。务必设置明确的大小限制。
唯一的大缺点是,tmpfs 挂载只在 Linux 上有效。
Mac 和 Windows 的 Docker Desktop 不支持它们,因为 Docker 运行在 Linux VM 中,而 tmpfs 需要内核直接支持。
Docker 挂载语法与配置
Docker 提供两种定义挂载的方式,选择合适的语法能让您的命令更易读、更易调试。
两种方法都可用,但当您需要高级选项或在单个容器中使用多个挂载时,其中一种更具可扩展性。
--mount 与 --volume 的比较
--mount使用显式的键值对,而-v或--volume使用冒号分隔的字符串。下面用两种语法实现同一个 volume 挂载:
两者都会创建名为
mydata的 volume,并将其挂载到容器内的/app/data。除基础场景外,请优先使用
--mount。它更啰嗦,但显式键能清楚表达各部分含义。当您添加只读访问或自定义 volume 驱动等选项时,它依然清晰,而 -v 可能看起来像一串难懂的字符。-v语法适用于您手动输入的简单开发工作流。常见挂载选项
readonly选项可阻止容器修改被挂载的数据:这对配置文件或容器只应读取的参考数据很有用。尝试写入只读挂载的容器会收到权限错误。
对于 volumes,
volume-nocopy选项会跳过将镜像中挂载点现有数据复制到 volume:默认情况下,Docker 会在新 volume 上复制镜像中挂载点处的内容。设置
volume-nocopy后,无论镜像里有什么,您都会得到一个空的 volume。对于 tmpfs,
tmpfs-size选项用于设置内存上限:这会将 tmpfs 挂载的大小限制为 100MB。若不设置,tmpfs 可能会耗尽所有可用 RAM。
在现有数据之上进行挂载
当您将挂载目标设为镜像中已存在的目录时,挂载会完全遮蔽那里的任何内容。
假设镜像中有一个包含配置文件的
/app/data目录。当您将 volume 挂载到/app/data时,这些配置文件会“消失”。容器只能看到 volume 中的内容。此行为适用于所有挂载类型——volumes、bind mounts 和 tmpfs。挂载内容具有更高优先级,挂载激活期间原目录不可访问。
在 Docker Compose 中使用挂载
Docker Compose 让您可以轻松在应用栈的多个容器间定义和共享挂载。
无需输入冗长的带挂载标志的
docker run命令,您只需在docker-compose.yml文件中声明一切。让我来演示。在 Compose 中定义 volumes 和 bind mounts
下面是一个同时包含 volume 与 bind mount 的 Compose 文件示例:
volumes键在每个服务下定义要挂载的内容。像./code这样的相对路径会创建 bind mount,而像shared这样的名称则引用命名 volume。顶层的
volumes部分声明了由 Compose 创建和管理的命名 volumes。service-1和service-2都挂载了同一个sharedvolume,因此它们看到的是同一批文件。从一个容器写入文件,另一个容器可立即读取。sleep infinity命令用于让容器保持运行,以便您可以进入执行——仅用于演示。持久化与验证数据
使用
docker compose up -d启动您的栈,然后检查挂载是否生效:若两条命令都能正常运行,说明您的 volumes 配置正确。
现在您可以运行以下命令停止并移除所有内容:
您的命名 volumes 仍会保留。如果再次使用
docker compose up -d启动栈,此前写入的数据依然存在。这正是数据库在部署间持久化的方式——volume 的生命周期长于容器。若您希望在停止栈时一并删除 volumes,请添加 -v 标志:
这会移除 Compose 文件中定义的所有 volumes。需要清空环境时使用。
用初始数据填充 volumes
最常见的模式是使用单独的初始化容器为共享 volume 注入种子数据:
init容器创建种子数据并将其拷贝进 shared volume,然后退出。service-1与service-2随后启动,能直接使用这些种子数据。Compose 在单个可版本控制文件中处理协调多个容器及其共享存储的复杂性。
Docker 挂载的性能与安全
选错挂载类型会拖慢容器,或带来您未意识到的安全漏洞。如果不想遇到这些问题,请阅读本节。
不同挂载类型的性能权衡
在 Linux 上,volumes 提供最佳性能,因为它们直接存储在主机文件系统上,无需转换层。
在 Mac 和 Windows 上,Docker 运行在 Linux VM 中。volumes 依然表现良好,因为它们留在 VM 内。相反,bind mounts 需要在您的主机 OS 与 Linux VM 之间同步文件,这会增加开销。在 Mac 和 Windows 上,bind mounts 的文件操作会比原生 Linux 明显更慢。
tmpfs 在读写操作上最快,因为一切都在 RAM 中进行。没有磁盘 I/O,也没有文件系统开销。但它受限于可用内存,且在容器停止时数据会消失。
如果您在 Linux 上追求极致性能,请使用 volumes。如果您在 Mac 或 Windows 上开发并注意到文件操作缓慢,很可能是 bind mount 的开销。生产工作负载请切换到 volumes。
挂载的安全影响
每次挂载都会让容器访问其隔离文件系统之外的内容,这会产生风险。
Bind mounts 的风险最大。如果您将
/home/user挂载进容器,被攻陷的容器可以读取您的 SSH 密钥、修改 Shell 配置,或删除整个主目录中的文件。若该容器以 root 运行(默认),它对这些文件拥有 root 级访问。Volumes 会降低这类风险,因为它们被隔离在 Docker 的存储目录中。容器无法通过 volumes 任意挂载主机路径。但若您随意共享 volumes,也可能在容器间泄露数据。
tmpfs 挂载可最小化持久化风险——存储在内存中的机密会在容器停止时消失。但它们无法防御运行期攻击,即被攻陷的容器从内存中读取机密。
总的规则是,挂载会打破容器隔离,因此要谨慎使用。
安全挂载的最佳实践
只挂载容器所需的最小范围,且不要超出。
与其挂载整个项目目录,不如只挂载容器实际使用的子目录。与其以写权限挂载
/var/log,若容器只需要读取日志,请以只读方式挂载。尽可能使用
readonly选项:这可防止容器修改挂载数据,若容器被攻陷也能降低破坏范围。
以非 root 用户运行容器,以降低 bind mount 漏洞的影响。在 Dockerfile 中创建用户并在容器启动前切换:
定期使用
docker volume prune清理未使用的 volumes。旧的 volumes 会随着时间堆积,既占用磁盘空间,也可能存有已删除容器的敏感数据。切勿挂载诸如
/、/etc或/var等敏感主机目录,除非您有特定理由且充分理解风险。每个挂载都应有明确目的和最小范围。排查 Docker 挂载问题
挂载问题通常表现为权限错误、文件缺失或容器无法启动——几乎总是由同一类问题引起。
以下是诊断与修复最常见问题的方法。
权限与所有权错误
当容器内的用户无权访问被挂载的文件时,就会发生权限错误。
Docker 容器默认以 root 运行。root 在 bind mount 中创建的文件在主机上归 root 所有。若您尝试以常规用户编辑,会得到权限拒绝错误。
反过来也一样。将您拥有的目录挂载到以非 root 用户运行的容器中,容器可能无法写入。
您可以在挂载目录上使用
ls -la检查文件所有权:如果文件归 root 所有,而您的容器以其他用户运行,那就是不匹配。通过以拥有这些文件的同一用户运行容器来修复:
这会以当前主机用户而非 root 运行容器,从而与 bind mount 中文件的所有权匹配。
对于 volumes,当容器创建文件时 Docker 会自动处理权限。但如果您遇到错误,请检查容器化应用以哪个用户运行,以及其是否对挂载点有写权限。
路径与配置错误
最常见的错误是挂载了主机上不存在的路径。
如果尝试挂载不存在的
/home/user/project,Docker 会创建一个空目录并归 root 所有。容器能启动,但挂载的是错误的东西——一个空目录而非您的实际项目。挂载前务必验证路径是否存在:
如果目录不存在,请先创建,或修正挂载命令中的路径。
在 Docker Compose 中,相对路径会相对于包含
docker-compose.yml的目录解析。若您的文件在/home/user/app/,并使用了./data,Docker 会查找/home/user/app/data。移动 Compose 文件,挂载就会出问题。
另一个常见错误是在容器内挂载到了错误的目标路径。若挂载到
/app/data,但您的应用期望/data,应用就找不到文件。请检查应用文档或 Dockerfile,确认期望的数据路径。平台特有的怪癖
在 Linux 上,bind mounts 直接与主机文件系统交互。
在 Mac 和 Windows 上,Docker 运行在 Linux VM 中。Bind mounts 需要在主机 OS 与该 VM 间同步文件,导致时序问题。文件监听器(在文件变更时重载应用的工具)有时会因同步延迟而错过更新。
Mac 和 Windows 处理文件权限的方式也不同。VM 会在主机 OS 与 Linux 之间转换权限,可能导致容器内显示出不正确的文件所有权。
在 Mac 和 Windows 上,bind mounts 中的符号链接并不可靠。VM 并不总能解析指向挂载目录之外的符号链接,因此容器内会出现文件缺失或损坏的情况。
在 Mac 和 Windows 上,tmpfs 挂载根本无法工作,因为 VM 不会将 tmpfs 暴露给主机。尝试使用 tmpfs 挂载时,Docker 可能会静默忽略,或根据版本抛出错误。
如果您在 Mac 或 Windows 上开发并遇到奇怪的文件同步问题,请切换到命名 volumes 以获得更好的性能和可靠性。将 bind mounts 留给对实时同步要求高、且可容忍一致性问题的开发工作流。
结语
总而言之,面向需要持久化的生产数据(如数据库、上传文件、应用状态——任何您无法承受丢失的内容)请使用 volumes。它们由 Docker 管理,跨平台可移植,是处理重要数据的最安全选择。
Bind mounts 适用于开发工作流,当您在编辑器中修改代码时,容器化应用会立刻看到变化。但请将它们排除在生产之外,因为它们过于依赖主机特定路径,并带来不必要的安全风险。
当您准备更深入学习容器化与虚拟化时,请查看我们的课程:Containerization and Virtualization with Docker and Kubernetes。