A beginner-friendly introduction to Docker, including containers, images, namespaces, cgroups, common commands, permissions, networking, bind mounts, ports, Dockerfiles, registries, layered builds, and Docker Compose.
容器 (Container) 是一种轻量级的虚拟化技术, 允许你在同一台物理机上运行多个隔离的环境. 它和 VM 的区别在于, VM 需要模拟整个操作系统, 包括内核, 驱动, 文件系统等; 而容器直接利用宿主机的内核, 只隔离用户空间. 因此, 容器启动更快, 占用资源更少.
我们经常会遇到某个奇奇怪怪的项目发来的各种依赖要求千奇百怪的软件, 很容易弄得本地环境一团糟. 这时候, Docker 就能派上用场了. 你可以在 Docker 容器里安装和配置好项目需要的环境, 包括操作系统版本, 库依赖, 环境变量等. 然后把这个容器打包成一个镜像 (Image), 发给别人或者部署到服务器上. 别人只需要运行这个镜像, 就能得到和你一样的环境, 不用担心依赖冲突或者环境不一致的问题.
这篇文档的目标不是让你成为容器平台管理员, 而是让你能把 Docker 作为开发和实验环境管理工具稳定地用起来. 对大多数 CS/ECE 学生来说, Docker 最重要的价值是隔离依赖, 复现环境, 跑别人的项目, 以及把自己的项目交给别人运行.
docker run, docker ps, docker exec, docker logs, docker stop, docker rm, docker images 等基础命令.docker compose.yml, 并用 Docker Compose 启动多容器开发环境.Docker 的学习边界也要明确: Docker 不是万能沙箱. 它默认隔离程度比普通进程强, 但弱于真正的虚拟机. 如果你运行不可信代码, 尤其是加了 --privileged, 挂载了宿主机敏感目录, 或者把用户加入了 docker 组, 就要把它视为有较高风险的操作.
Docker 容器依赖 Linux 内核里的两个核心机制: namespaces 和 cgroups.
Namespace 负责"看见什么". 它让不同进程看到不同的系统视图. 常见 namespace 包括:
例如, 你在容器里运行:
ps aux
hostname
ip addr看到的进程, 主机名和网卡都可能和宿主机不同. 这不是因为 Docker 启动了另一个完整内核, 而是同一个 Linux 内核给这个进程提供了不同的视图.
Cgroups 负责"能用多少". 它限制和统计进程组的资源使用, 例如:
例如限制容器最多使用 512 MB 内存:
docker run --rm -m 512m ubuntu:24.04 bash限制容器最多使用 1 个 CPU:
docker run --rm --cpus=1 ubuntu:24.04 bash所以, Docker 的本质不是"模拟一台机器", 而是在 Linux 上启动一个普通进程, 然后用 namespace 改变它看到的世界, 用 cgroups 限制它能使用的资源, 再给它准备一个隔离的文件系统.
这也解释了为什么 Linux 上 Docker 比虚拟机轻: 容器不需要启动另一个内核, 不需要模拟硬件, 不需要跑完整操作系统. 容器里的 Ubuntu, Debian, Alpine 等只是用户空间里的文件和程序, 它们最终仍然调用宿主机 Linux 内核.
严格来说, Docker 容器需要 Linux 内核提供 namespaces 和 cgroups. macOS 的内核是 XNU, Windows 的内核也不是 Linux 内核, 因此它们不能原生运行 Linux Docker 容器.
这就是为什么说 macOS/Windows 没有真正 native 的 Linux Docker. 它们通常通过一个隐藏的 Linux 虚拟机来运行 Docker:
docker 命令, 实际上是通过 Docker CLI 和 VM 里的 Docker daemon 通信.这会带来几个实际影响:
localhost, bridge network, host network 的行为可能和 Linux 原生 Docker 不完全一致.如果你的目标是部署到 Linux 服务器, 最终最好在 Linux 环境里测试一遍 Docker 行为. macOS/Windows 上的 Docker Desktop 非常适合开发, 但它不是 Linux 原生 Docker 的完全等价替身.
Docker 的核心对象有三个:
ubuntu:24.04, python:3.12.Docker 有两种很常见的使用方式.
第一种是开发用法: 把 Docker 当成可丢弃的开发环境. 你把源码目录挂载进容器, 在容器里安装编译器, Python 包, EDA 工具依赖, 然后编译或运行项目. 这种方式强调迭代方便, 常见参数是 -it, --rm, -v, -w, -u.
docker run --rm -it \
-u "$(id -u):$(id -g)" \
-v "$PWD:/work" \
-w /work \
my-dev:latest \
bash第二种是分发用法: 把应用和运行依赖打进镜像, 推送到镜像仓库, 别人在服务器上拉取并运行. 这种方式强调镜像小, 启动命令明确, 配置通过环境变量或挂载传入, 常见参数是 -d, --name, -p, -e, --restart.
docker run -d \
--name my-app \
-p 127.0.0.1:8080:8080 \
-e APP_ENV=production \
ghcr.io/user/my-app:v1.0.0开发镜像可以大一些, 里面带调试工具和编译工具链; 分发镜像应该尽量小, 只包含运行时需要的文件. 这就是后面分层构建和多阶段构建要解决的问题.
拉取镜像:
docker pull ubuntu:24.04
docker pull python:3.12-slim查看本地镜像:
docker images运行一个容器:
docker run ubuntu:24.04 echo hello这条命令会基于 ubuntu:24.04 镜像启动一个容器, 在容器里执行 echo hello, 命令结束后容器退出.
查看正在运行的容器:
docker ps查看所有容器, 包括已经退出的:
docker ps -a给容器起名字:
docker run --name my-ubuntu ubuntu:24.04 echo hello删除已经退出的容器:
docker rm my-ubuntu使用 --rm 表示容器退出后自动删除:
docker run --rm ubuntu:24.04 echo hello停止正在运行的容器:
docker stop <container>强制删除容器:
docker rm -f <container>查看容器日志:
docker logs <container>
docker logs -f <container>-f 类似 tail -f, 会持续跟随新日志.
进入一个正在运行的容器:
docker exec -it <container> bash如果镜像里没有 bash, 可以尝试:
docker exec -it <container> sh查看 Docker 占用空间:
docker system df清理未使用的容器, 网络, dangling images, build cache:
docker system prune这个命令会删除不再使用的对象. 如果你不确定镜像是否还需要, 不要随手加 -a, 因为 docker system prune -a 会删除所有当前没有被容器使用的镜像.
docker rundocker run 是最常用的 Docker 命令. 它做了三件事: 基于镜像创建容器, 启动容器, 在容器里执行指定命令.
基本形式是:
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]例如:
docker run --rm ubuntu:24.04 echo hello这里 ubuntu:24.04 是镜像, echo hello 是容器启动后执行的命令. 如果不写命令, Docker 会使用镜像里定义的 CMD 或 ENTRYPOINT.
最常见参数:
--rm: 容器退出后自动删除, 适合临时任务.-it: 交互式终端, 常用于进入 shell.-d: 后台运行, 常用于服务.--name: 给容器命名.-v 或 --mount: 挂载目录或 volume.-w: 设置工作目录.-p: 端口映射.-e: 设置环境变量.--env-file: 从文件读取环境变量.-u: 指定容器内进程使用的 UID/GID.--network: 指定网络模式或网络名.--restart: 设置重启策略.--entrypoint: 覆盖镜像入口程序.启动一个临时 Ubuntu shell:
docker run --rm -it ubuntu:24.04 bash挂载当前目录并设置工作目录:
docker run --rm -it \
-v "$PWD:/work" \
-w /work \
ubuntu:24.04 \
bash用当前用户身份运行, 避免输出文件变成 root 拥有:
docker run --rm -it \
-u "$(id -u):$(id -g)" \
-v "$PWD:/work" \
-w /work \
ubuntu:24.04 \
bash运行后台服务并映射端口:
docker run -d \
--name web \
-p 127.0.0.1:8080:80 \
nginx传入环境变量:
docker run --rm \
-e APP_ENV=dev \
-e LOG_LEVEL=debug \
my-app:latest从文件传入环境变量:
docker run --rm --env-file .env my-app:latest.env 文件通常长这样:
APP_ENV=dev
LOG_LEVEL=debug后台服务如果希望 Docker daemon 重启后自动恢复, 可以设置重启策略:
docker run -d \
--name web \
--restart unless-stopped \
-p 8080:80 \
nginx常见重启策略:
no: 不自动重启, 默认值.on-failure: 只有异常退出时重启.unless-stopped: 除非手动停止, 否则 daemon 重启后也会恢复.always: 总是尝试重启.覆盖入口程序适合调试镜像:
docker run --rm -it --entrypoint bash my-app:latest如果镜像里没有 bash, 可以换成 sh.
复杂命令建议写成多行, 每一类参数放在一起. 这样比一长行更容易检查挂载, 端口和权限:
docker run --rm -it \
--name dev \
-u "$(id -u):$(id -g)" \
-v "$PWD:/work" \
-w /work \
-e CC=clang \
-p 127.0.0.1:8000:8000 \
my-dev:latest \
bash如果命令变得更长, 通常说明应该考虑写 docker compose.yml 或 Makefile, 避免每次手动输入出错.
Docker 的权限问题有两层: 谁能控制 Docker daemon, 以及容器里的用户如何映射到宿主机文件.
首先, Docker daemon 通常以 root 权限运行. 能访问 Docker socket 的用户, 基本上就能获得宿主机上的 root 级别能力. 因此把用户加入 docker 组并不是一个普通授权:
sudo usermod -aG docker $USER加入后, 这个用户可以运行:
docker run --rm -it -v /:/host ubuntu:24.04 bash这会把宿主机根目录挂载进容器. 如果容器里又以 root 身份运行, 就可能修改宿主机文件. 所以, docker 组应当被视为近似 root 权限.
第二层是容器内用户和文件权限. 默认情况下, 很多镜像里的进程以 root 用户运行. 如果你把当前目录挂载进容器, 容器里创建的文件可能会变成 root 拥有:
docker run --rm -v "$PWD:/work" -w /work ubuntu:24.04 touch output.txt
ls -l output.txt在 Linux 原生 Docker 上, 文件权限按数字 UID/GID 判断. 容器里的 root 是 UID 0, 宿主机也把这个文件看成 UID 0 创建的文件.
常见解决方式是用当前用户的 UID/GID 运行容器:
docker run --rm \
-u "$(id -u):$(id -g)" \
-v "$PWD:/work" \
-w /work \
ubuntu:24.04 \
touch output.txt这样容器里创建的文件在宿主机上会属于当前用户.
有些镜像内部需要用户存在于 /etc/passwd, 只传 UID/GID 可能导致用户名显示异常, 但文件权限通常仍然是正确的. 更完整的做法是在 Dockerfile 里创建非 root 用户, 后面会介绍.
Docker 也支持 rootless mode 和 user namespace remapping, 用来降低 daemon 或容器 root 带来的风险. 但这些配置会改变网络, 存储和权限行为, 对初学者来说可以先知道它们存在, 不必一开始就使用.
Docker 默认使用 bridge 网络. 可以把它理解成 Docker 在宿主机上创建了一个虚拟交换机, 每个容器接入这个交换机, 获得一个内部 IP. 容器可以通过 NAT 访问外网, 但外部不能直接访问容器端口, 除非你做端口映射.
查看网络:
docker network ls
docker network inspect bridge常见网络模式:
bridge: 默认模式, 容器接入 Docker 创建的虚拟网桥.host: 容器直接使用宿主机网络命名空间, Linux 上可用, 隔离更弱.none: 容器没有网络.创建自定义网络:
docker network create mynet启动两个容器加入同一个网络:
docker run -d --name redis --network mynet redis:7
docker run --rm -it --network mynet redis:7 redis-cli -h redis ping第二个容器可以用 redis 这个容器名访问第一个容器. 这是 Docker 内部 DNS 提供的能力.
使用 host 网络:
docker run --rm --network host nginxhost 网络下容器和宿主机共享网络栈, 不需要 -p 做端口映射. 但它也意味着网络隔离变弱, 端口冲突更直接. 在 macOS/Windows 上, 因为 Docker 实际运行在 Linux VM 中, host 网络的行为和 Linux 原生 Docker 不完全一样.
容器自己的文件系统是临时的. 如果你在容器里写文件, 容器删除后这些改动通常也会消失. 想让数据持久化, 或者让容器访问宿主机工程目录, 就需要挂载.
从结构上看, 容器文件系统通常由两部分组成:
当你删除容器时, 容器可写层也会被删除, 但镜像层仍然保留. 这就是为什么同一个镜像可以反复启动新容器, 但一个容器里临时安装的软件不会自动影响下一个容器.
挂载会把宿主机目录或 volume 接到容器文件系统的某个路径上. 如果挂载目标路径原本在镜像里已经有文件, 挂载后这个路径会被挂载内容覆盖. 例如:
docker run --rm -v "$PWD:/usr/share/nginx/html" nginx此时 nginx 镜像里原本的 /usr/share/nginx/html 内容会被你当前目录覆盖. 这不是删除了镜像里的文件, 而是 mount namespace 里这个路径被新的挂载点遮住了.
常见挂载方式有三种:
Bind mount 最常用于开发:
docker run --rm -it \
-v "$PWD:/work" \
-w /work \
ubuntu:24.04 \
bash这里:
-v "$PWD:/work": 把当前目录挂载到容器的 /work.-w /work: 设置容器启动后的工作目录.bash: 在容器里启动 shell.也可以使用更清晰的 --mount 写法:
docker run --rm -it \
--mount type=bind,source="$PWD",target=/work \
-w /work \
ubuntu:24.04 \
bash只读挂载:
docker run --rm \
-v "$PWD:/work:ro" \
-w /work \
ubuntu:24.04 \
ls:ro 表示 read-only, 容器不能修改这个目录. 如果你只是让容器读取数据或源码, 只读挂载更安全.
Volume 适合保存数据库数据或构建缓存:
docker volume create mydata
docker run -d \
--name db \
-v mydata:/var/lib/postgresql/data \
postgres:16查看 volume:
docker volume ls
docker volume inspect mydata文件权限是挂载里最常见的坑. Linux 上 bind mount 后, 容器和宿主机看到的是同一批文件, 权限按 UID/GID 判断. 如果容器用 root 写文件, 宿主机上可能变成 root 文件. 如果容器用 UID 1000 写文件, 宿主机上 UID 1000 的用户就会拥有它, 即使容器里这个用户没有名字.
macOS/Windows 上还有一层 Docker Desktop 的文件共享映射, 所以权限和性能表现可能和 Linux 服务器不同. 如果你的项目最终跑在 Linux 服务器上, 涉及挂载和权限时最好在 Linux 上再验证一次.
容器默认有自己的网络命名空间. 容器里服务监听的端口, 宿主机不一定能直接访问. 需要使用 -p 做端口映射.
运行 nginx:
docker run --rm -d --name web -p 8080:80 nginx这里 -p 8080:80 的含义是:
宿主机 8080 端口 -> 容器 80 端口然后访问:
curl http://localhost:8080指定只监听本机地址:
docker run --rm -d --name web -p 127.0.0.1:8080:80 nginx这样只有宿主机本机能访问 127.0.0.1:8080, 外部机器不能直接访问. 这比默认绑定到所有网卡更安全.
查看端口映射:
docker port web
docker ps常见错误是服务在容器里只监听 127.0.0.1. 如果容器内应用只绑定 localhost, Docker 端口映射可能访问不到它. 对 Web 服务来说, 容器内通常应监听 0.0.0.0, 然后由 Docker 决定宿主机如何映射.
例如 Python HTTP server 默认通常监听所有地址:
docker run --rm -p 8000:8000 python:3.12 \
python -m http.server 8000交互式容器常用于临时调试环境:
docker run --rm -it ubuntu:24.04 bash选项含义:
-i: 保持 stdin 打开.-t: 分配伪终端.--rm: 容器退出后自动删除.进入后你可以像在一台 Linux 机器里一样运行命令:
apt update
apt install -y build-essential
gcc --version但要记住, 如果没有挂载 volume 或 bind mount, 容器删除后这些安装和改动都会消失.
给容器挂载当前目录:
docker run --rm -it \
-v "$PWD:/work" \
-w /work \
ubuntu:24.04 \
bash如果容器已经在后台运行, 可以用 exec 进入:
docker exec -it <container> bashdocker run 是创建并启动新容器, docker exec 是在已有容器中再启动一个进程. 这两个命令不要混淆.
--privileged 会显著放宽容器隔离, 给容器更多 Linux capabilities, 设备访问权限, 并允许一些原本被限制的内核操作.
docker run --rm -it --privileged ubuntu:24.04 bash它常用于需要访问硬件设备或底层系统能力的场景, 例如:
/dev 设备.但 --privileged 基本上意味着容器不再是普通意义上的隔离环境. 如果再配合挂载宿主机目录, 容器内进程可能对宿主机造成严重影响.
更推荐的做法是只授予需要的能力或设备:
docker run --rm -it \
--device /dev/ttyUSB0:/dev/ttyUSB0 \
ubuntu:24.04 \
bash或者添加特定 capability:
docker run --rm -it \
--cap-add NET_ADMIN \
ubuntu:24.04 \
bash基本原则是: 能用 --device 就不要用 --privileged; 能加一个 capability 就不要全量放开.
Dockerfile 是描述如何构建镜像的文件. 一个最小例子:
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /work
CMD ["bash"]构建镜像:
docker build -t my-dev:latest .这条命令里的 . 很重要, 它表示构建上下文 (Build Context) 是当前目录. Docker build 大致会按下面的流程执行:
Dockerfile..dockerignore 排除不需要发送给 Docker daemon 的文件.FROM 指定的基础镜像开始.-t 指定的 tag.可以把 Dockerfile 想成一份"制作镜像的菜谱". docker build 不会直接在你的宿主机系统里执行这些命令, 而是在构建环境中基于上一层镜像状态执行. 例如 RUN apt-get install ... 安装的是镜像里的软件, 不是安装到你的宿主机.
构建上下文决定了 COPY 能看到哪些文件. 例如:
COPY requirements.txt .
COPY src/ ./src/这里的 requirements.txt 和 src/ 必须在 build context 里面. 如果你运行:
docker build -t my-app .那么当前目录就是 build context. 如果你运行:
docker build -t my-app ..那么上一级目录才是 build context, COPY 的源路径也会相对于上一级目录解释.
.dockerignore 的作用类似 .gitignore, 但它影响的是 build context. 例如:
.git
build
node_modules
*.log这些文件不会被发送给 Docker daemon, 也不能被 COPY 进镜像. 这可以减少构建上下文大小, 避免把无关文件, 缓存, 密钥误打进镜像.
Docker 的构建缓存也是按指令顺序工作的. 对于每条指令, Docker 会检查这条指令和它依赖的输入是否和上次相同. 如果相同, 就复用旧 layer; 如果不同, 这一层以及后面的层通常都要重新构建.
例如:
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .如果只修改源码, 前两条通常能复用缓存, 只需要重新执行 COPY . . 之后的部分. 如果修改了 requirements.txt, pip install 这一层就会重新执行.
有时你想确认构建不使用缓存:
docker build --no-cache -t my-app .如果 Dockerfile 不在默认位置, 可以用 -f 指定:
docker build -f docker/Dockerfile -t my-app .注意这里 -f 指定的是 Dockerfile 路径, 最后的 . 仍然是 build context. 这两个概念不要混淆.
运行镜像:
docker run --rm -it my-dev:latest常见指令:
FROM: 指定基础镜像.RUN: 构建镜像时执行命令.COPY: 把构建上下文中的文件复制进镜像.WORKDIR: 设置工作目录.ENV: 设置环境变量.ARG: 构建参数.CMD: 容器默认启动命令.ENTRYPOINT: 容器入口程序.USER: 指定运行用户.RUN, CMD, ENTRYPOINT 容易混淆:
RUN 在 docker build 时执行, 结果写入镜像.CMD 在 docker run 时作为默认命令, 可以被命令行参数覆盖.ENTRYPOINT 更像固定入口, 命令行参数通常会追加给它.例如 Python 项目:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]构建并运行:
docker build -t my-python-app .
docker run --rm my-python-app构建上下文是 docker build 最后那个路径, 例如上面的 .. Docker 只能 COPY 构建上下文里的文件. 如果项目目录很大, 应该写 .dockerignore 排除不需要的文件:
.git
build
__pycache__
*.log为了避免容器里默认 root 运行, 可以创建普通用户:
FROM ubuntu:24.04
RUN useradd -m -u 1000 appuser
USER appuser
WORKDIR /home/appuser
CMD ["bash"]这能减少容器进程对文件系统的破坏面, 也能缓解 bind mount 时的权限问题.
Docker 镜像由多层只读 layer 组成. Dockerfile 中很多指令都会创建新层, 例如 RUN, COPY, ADD. 构建时 Docker 会缓存已有层, 如果某一层没有变化, 下次构建可以直接复用.
Layer 不只是构建缓存, 也影响分发体积. 当你 push 或 pull 镜像时, Registry 传输的是一层一层的内容. 如果两张镜像共享相同基础层, 客户端已经有这些层时就不需要重复下载. 例如多个镜像都基于 ubuntu:24.04, 那么基础系统层可以复用.
但 layer 也有一个重要特性: 后面的层删除前面层里的文件, 不会让前面的层变小. 它只是记录"这个文件在最终视图里被删除了". 因此, 想减小分发镜像体积, 应该避免把不需要分发的文件写进任何会保留到最终镜像的 layer.
例如:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]这里先复制 requirements.txt, 安装依赖, 最后再复制源码. 这样如果你只改了源码, pip install 那一层仍然可以复用, 构建会快很多.
如果写成:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "main.py"]那么任何源码改动都会导致 COPY . . 这一层变化, 后面的 pip install 也会重新执行.
APT 安装依赖时, 常见写法是把 update, install, cleanup 放在同一个 RUN 里:
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
&& rm -rf /var/lib/apt/lists/*如果分成多个 RUN, 清理命令可能不能减少前面 layer 已经写入的体积.
例如下面这种写法并不能有效缩小镜像:
RUN apt-get update
RUN apt-get install -y build-essential cmake
RUN rm -rf /var/lib/apt/lists/*因为 apt 索引已经写进了前面的 layer, 后面删除只是在新 layer 里标记删除. 正确做法是把安装和清理放在同一个 RUN 中, 让不需要的中间文件从未进入最终 layer.
多阶段构建可以把编译环境和运行环境分开. 例如 C++ 程序:
FROM ubuntu:24.04 AS builder
RUN apt-get update && apt-get install -y g++ cmake && rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY . .
RUN cmake -S . -B build && cmake --build build
FROM ubuntu:24.04
COPY --from=builder /src/build/my_app /usr/local/bin/my_app
CMD ["my_app"]最终镜像只包含运行程序, 不包含完整编译工具链. 这能减小镜像体积, 也能减少运行时攻击面.
多阶段构建是减小分发镜像体积最重要的方法之一. 编译时需要的内容通常很多, 例如编译器, CMake, header, 静态库, 源码, 测试数据. 运行时可能只需要一个二进制文件和少量动态库. 多阶段构建把这些内容分开: builder stage 可以很大, final stage 只复制真正要分发的产物.
这对分发特别重要. 开发镜像可以保留工具链方便调试; 分发镜像应尽量只包含运行时依赖, 这样 pull 更快, 存储更少, 暴露的工具也更少.
查看镜像历史:
docker history my-app:latest这可以帮助你理解每一层大概来自哪条 Dockerfile 指令.
镜像仓库 (Registry) 用来保存和分发镜像. 常见公共仓库包括 Docker Hub, GitHub Container Registry, GitLab Container Registry 等.
镜像名通常由几部分组成:
registry/namespace/name:tag例如:
docker.io/library/ubuntu:24.04
ghcr.io/user/project:latest
registry.example.com/team/app:v1.0.0如果省略 registry, Docker 默认使用 Docker Hub. 如果省略 tag, 默认使用 latest, 但生产环境不建议依赖 latest, 因为它不是稳定版本号.
登录仓库:
docker login
docker login ghcr.io给镜像打 tag:
docker tag my-app:latest ghcr.io/user/my-app:v1.0.0推送镜像:
docker push ghcr.io/user/my-app:v1.0.0拉取镜像:
docker pull ghcr.io/user/my-app:v1.0.0查看镜像详细信息:
docker image inspect my-app:latest镜像 tag 只是名字, 不是内容本身. 真正唯一标识镜像内容的是 digest, 例如 sha256:.... 如果你需要完全可复现的环境, 可以使用 digest 固定镜像:
docker pull ubuntu@sha256:<digest>对课程项目或论文 artifact 来说, 尽量不要只写 ubuntu:latest, 而应使用明确版本, 例如 ubuntu:24.04 或自己的固定 tag.
Docker Compose 用来管理多容器应用. 如果一个项目需要 Web 服务, 数据库, Redis, 消息队列等多个容器, 手写一长串 docker run 会很难维护, Compose 可以把这些配置写进一个 YAML 文件.
一个简单例子:
services:
web:
build: .
ports:
- "8080:80"
volumes:
- .:/app
depends_on:
- redis
redis:
image: redis:7启动:
docker compose up后台启动:
docker compose up -d查看日志:
docker compose logs
docker compose logs -f web进入某个服务容器:
docker compose exec web bash停止并删除容器和默认网络:
docker compose down如果还想删除 volume:
docker compose down -vdown -v 会删除数据卷, 数据库数据可能丢失, 使用前要确认.
Compose 会自动为同一个项目里的服务创建网络, 服务之间可以用服务名访问. 例如上面的 web 可以通过 hostname redis 访问 Redis.
一个带环境变量和 volume 的例子:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
volumes:
db-data:这里 db-data 是 Docker 管理的 volume, 容器删除后数据仍然存在. 端口映射使用 127.0.0.1:5432:5432, 表示数据库只暴露给本机访问.
对于初学者来说, 读 Compose 文件时可以先看四件事:
image 或 build: 服务从哪里来.ports: 哪些端口暴露到宿主机.volumes: 哪些文件或数据会持久化.environment: 服务依赖哪些环境变量.