我怎样理解 Docker:从运行环境到一个可以交付的项目
学习 Docker 时,我最先想解决的问题是:程序在自己的电脑上能运行,怎样让它换一个环境之后也能运行?
程序不只依赖源码,还依赖解释器、系统库和安装的包。Docker 把运行条件组织成镜像,再通过容器启动程序。把这两者分清楚,后面的构建与部署就更容易连起来。
镜像是模板,容器是运行实例
镜像可以理解为准备好的环境模板,容器则是根据模板创建的一次运行。同一个镜像可以创建多个容器,它们可以有不同的名称、端口和数据挂载。
仓库用于组织镜像,Registry 提供存储和分发服务。标签帮助选择版本,例如 python:3.11-slim;latest 是默认标签,但不保证版本固定。Docker 概念介绍
容器与虚拟机
Linux 容器共享运行它的 Linux 内核,通常不需要为每个容器启动完整操作系统。在 Mac 上运行 Linux 容器时,还需要 Linux 虚拟机这一层,不能直接把 macOS 内核当成 Linux 内核。Docker Desktop Mac 文档
环境更一致,不代表换到任何机器都完全相同。CPU 架构、配置与外部服务仍然需要检查。

安装之前,先确认系统和架构
macOS 可以从 Docker Desktop 开始,Linux 服务器则按照发行版安装 Docker Engine。先确认操作系统版本和实际环境中的 CPU 架构,再选择安装包。
Linux安装教程(官方文档):https://docs.docker.com/engine/install/ubuntu/
版本选择
Docker Desktop 和 Docker Engine怎么选:
- 在有图形界面的系统中(Windows 或 macOS等),推荐使用:Docker Desktop;它集成 Docker Engine、Docker CLI、Docker Compose、Kubernetes(可选),以及一个图形化界面,适合 Windows 和 macOS 用户。
- 在没有图形界面的系统中(Linux),推荐使用:Docker Engine;
- Docker CE(Community Edition,社区版):开源免费
- Docker EE(Enterprise Edition,企业版,现已并入 Mirantis):商业付费
仓库安装与离线安装
联网环境可以按照官方文档配置软件仓库,由包管理器处理依赖。离线环境则需要提前准备匹配发行版与架构的软件包。
安装和卸载引擎之前,也要区分程序与已有数据。/var/lib/docker 中的镜像、容器和卷不能当成普通安装文件随意清理。
实操查找版本
以Ubuntu系统为例:
- 进入:https://download.docker.com/linux/ubuntu/ 这个网址,包含目录如下:
dists/:- 这是 Docker APT 存储库的核心目录,包含了不同的 Ubuntu 版本和架构的文件包。
- 进入
dists/目录后,你会看到各个 Ubuntu 发行版的名称,如bionic/(Ubuntu 18.04)、focal/(Ubuntu 20.04)、jammy/(Ubuntu 22.04)等。 - 进一步进入每个发行版的目录,可以看到
stable/、test/等子目录,这些目录中包含了对应版本的 Docker Engine 及其相关组件的安装包。 gpg:- 包含 Docker APT 仓库的 GPG 密钥,用于验证从这个仓库下载的软件包的真实性和完整性。安装 Docker 时,通常需要添加这个 GPG 密钥到系统中。
- 进入对应的系统(我的是
jammy/(Ubuntu 22.04)),进入:https://download.docker.com/linux/ubuntu/dists/jammy/ 之后,目录包含如下 stable/、test/、nightly/:- 这些是 Docker 的不同发布渠道(release channel)。每个目录中包含不同类型的 Docker 软件包。
stable/:包含稳定版本的 Docker 软件包,适合生产环境使用。test/:包含测试版的 Docker 软件包,用于开发者在发布正式版本之前测试新功能。nightly/:包含每日构建版本,提供最新的功能和更新,但可能不稳定,仅用于实验和测试。- 通常,建议使用
stable/版本,因为它是经过充分测试和验证的。 Release、InRelease、Release.gpg:Release:描述该发行版中可用软件包的元数据文件,包含每个软件包的版本信息、依赖关系等。InRelease:包含了Release文件的内容以及其 GPG 签名,提供了一种验证文件完整性和真实性的方式。Release.gpg:是对Release文件的独立 GPG 签名,用于确认文件的完整性和来源。pool/:- 该目录包含了所有 Docker 包文件的实际存放位置,按软件包的名称进行组织。
- 包括
docker-ce(Docker 社区版)、containerd.io(容器运行时)等。 - 这些
.deb文件可以直接下载,用于在没有网络连接的情况下进行离线安装。
- 进入
pool/目录,选择稳定版stable/, 然后选择系统架构类型 amd64/:用于 64 位架构的包文件(也称为 x86_64 架构)。arm64/:用于 ARM 架构的包文件。s390x/:用于 IBM 系统 z 架构的包文件。
- 我Ubuntu是安装在Mac M1电脑上的,所以选择
arm64/架构,进去之后包含以下类型文件: containerd.io(必须):容器运行时,这是 Docker 引擎所依赖的组件。docker-ce(必须):Docker 社区版的核心包,即 Docker 引擎。docker-ce-cli(必须):Docker 命令行工具。docker-compose-plugin(可选-推荐) ::用于多容器管理的插件。docker-buildx-plugin(可选-推荐):用于多平台构建的插件。docker-ce-rootless-extras(可选):用于支持 无特权(rootless)模式 的 Docker 运行。
选择合适的版本
- 版本选择:根据当前系统支持与项目依赖选择版本,不再把旧版 20.10 作为通用推荐。
- 兼容性:选择与其他软件包兼容的版本。例如,
docker-ce、docker-ce-cli和containerd.io通常需要保持版本兼容。如果选择docker-ce的最新版本,也要选择与之匹配的docker-ce-cli和containerd.io版本。
检查安装结果
docker version sudo docker run --rm hello-world
前一条检查客户端与引擎,后一条尝试拉取并运行测试镜像。若后者失败,要继续区分网络、权限与引擎问题。
遇到问题(Ubuntu)
- 运行测试镜像:
sudo docker run hello-world
- 报错:
- 原因:网络不能访问docker 的镜像源
- 方案1:配置国内镜像源
- 更改或新增配置文件:
vim /etc/docker/daemon.json - 重启服务:
service docker restart - 检查镜像源:
docker info|grep Mirrors -A 5——匹配到"Mirrors"的行后,再额外显示匹配行后面的 5 行内容(A表示 "After")
- 方案2:去阿里云官方申请了镜像地址也加速失败
- 结果:更新完之后发现还是不行,网上一搜才知道是很多镜像源已经停止服务了(放弃)
旧环境记录:CentOS 7.9
以下保留当时的安装问题记录,重新搭建时应先确认发行版支持与软件源状态。
在这里下载的最新版的 docker 安装包,如果只下载几个会提示依赖问题
以上安装遇到 GPG 验证问题
- 手动下载 gpg 文件:https://download.docker.com/linux/centos/gpg
- 导入 gpg 密钥:
sudo rpm --import gpg
- 验证 gpg 密钥:
rpm -qa gpg-pubkey*
- 重新执行安装命令即可安装成功
从一个小程序开始
先选择基础镜像,再准备源码和 Dockerfile,构建之后创建容器。下面把这几个环节分别展开。
Python 基础镜像怎样选
标准版本通常包含更多系统工具;slim 更精简;Alpine 使用不同的基础系统,部分原生依赖可能需要额外处理。
镜像体积会随版本和架构变化,不适合用一组固定数字长期比较。先确认依赖能否安装、程序能否运行,再比较构建速度与镜像大小。
原实验使用过 Python 3.9、Debian Buster 与 Bullseye 标签。这些版本信息作为历史记录保留,新的项目需要另外选择适合的基础环境。
镜像操作
先查看已经下载的镜像,再决定运行哪个版本。
docker image ls docker pull python:3.11-slim
这里的 Python 版本用于示例。
从镜像创建容器
docker run --rm python:3.11-slim python -c "print('Hello from Docker')"
程序打印之后退出,是主进程执行完成。--rm 会在退出后删除这个临时容器,适合不需要保留状态的示例。
创建容器
一个镜像可以用来创建多个容器,每个容器都是镜像的一个独立运行实例。
使用
docker run 创建并启动一个新容器时,所有指定的参数(如端口映射、卷挂载、环境变量等)都会被保存到该容器的配置中。在之后在使用 docker start 命令启动已经创建的容器时,不需要再次指定当利用
docker run 来创建容器时,Docker 在后台运行的标准操作包括:- 检查本地是否存在指定的镜像,不存在就从 registry 下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个 ip 地址给容器
- 执行用户指定的应用程序
- 容器的生命周期跟随主进程:短任务执行完会退出,服务程序则需要持续运行。
运行镜像生成容器后,通常有两种方式来操作容器内的内容:
- 容器开放一个访问端口,从浏览器访问
- 在容器内打开一个 shell,进行命令行操作
容器状态与启停
docker ps docker ps -a docker logs 容器名 docker stop 容器名 docker start 容器名
docker run 创建新容器,docker start 启动已有容器。两者的用途需要区分。
打包传输
文件形式
docker save -o python-demo.tar python-demo:local docker load -i python-demo.tar
前一条用于导出,后一条用于在接收机器上加载。镜像之外的数据卷、环境变量和凭据,需要单独准备。
镜像仓库
需要在线分发时,可以为镜像添加仓库标签,再推送到自己的仓库。运行所需的数据和配置仍然需要另行交付。
Docker存储
数据存储在容器内部:
- 修改数据需要先进入容器内部,操作繁琐
- 容器停止或进程崩溃不会自动清空可写层,但删除容器会删除这层数据。重要内容应保存到独立的挂载目录或卷。
一般用来操作存储配置文件和数据
目录挂载
Bind mount 把宿主机上的目录映射到容器中,适合编辑源码或读取已有文件。它也让容器能够修改挂载的文件,因此要明确挂载范围。
卷映射
Volume 由 Docker 管理,可以在容器重建之后继续保存数据。它并不是把容器目录持续同步到另一份目录。Docker Volume 文档
只有在首次使用卷映射时,如果卷为空且容器目录中有数据,Docker 才会将容器目录中的数据复制到卷中。之后,卷中的数据就是独立存在的,挂载到任何容器都不会被初始目录覆盖。
Docker 网络
容器之间的网络数据访问
Linux Docker Engine 默认使用 bridge 网络;实际网络也可以通过启动参数另行指定。用户自定义 bridge 网络支持通过容器名称或别名解析地址,比依赖临时容器 IP 更方便。
Docker Compose
容器批量管理工具,针对项目服务较多的工作;
多个服务逐个启动时,需要反复维护名称、端口与挂载。把它们写进文件,更方便下次复用。
Compose 启动
把服务、端口和挂载写进 compose.yaml,下一次启动就能复用这些配置。
docker compose up --build docker compose logs docker compose down
compose主要通过操作
compose.yaml文件来实现批量操作compose.yaml 配置文件
下面用一个短 Python 程序演示服务定义:
services: app: image: python:3.11-slim command: ["python", "-c", "print('Hello from Compose')"]
这个服务打印后退出。换成网页服务时,再根据应用补上端口和数据卷。
语法规范详见:官方文档;
compose批量启动

制作镜像
Dockerfile文件
docker builder通过dockerfile文件的内容来制作镜像,所以制作镜像最主要的就是编写Dockerfile文件。
通常,
Dockerfile 的第一行是 FROM 指令,它定义了镜像的基础环境。然而,如果你的机器不能联网,那么不能直接从官方 Docker Hub 或其他在线仓库拉取基础镜像。解决这个问题的办法有以下几种:方法 1: 使用已有的本地基础镜像
方法 2: 离线获取基础镜像
- 在可联网的机器上拉取镜像
- 在联网机器上将镜像保存为
.tar文件
- 将镜像传输到离线机器
- 在离线机器上加载镜像
方法 3:配置一个可用镜像源,阿里云主机可以参考这里配置
Dockerfile 中的指令是按顺序执行的,某些指令依赖于之前的指令的结果,所以编写时需要按照顺序编写。

Dockerfile文件配置参考
构建镜像
可以先创建一个最小 app.py,只写 print('Hello from Docker'),再准备 Dockerfile:
FROM python:3.11-slim WORKDIR /app COPY app.py . CMD ["python", "app.py"]
构建与运行:
docker build -t python-demo:local . docker run --rm python-demo:local
最后的点表示使用当前目录作为构建上下文。实际项目还需要安装依赖,并使用 .dockerignore 排除不该加入镜像的文件。
实操
官方前端案例:get started guide
- 克隆案例项目
- 创建项目镜像
- 创建Dockerfile文件
- 向文件内添加内容
# syntax=docker/dockerfile:1:指定 docker 语法版本FROM node:18-alpine:基于基础镜像node:18-alpineWORKDIR /app:设置了容器内的工作目录为/appCOPY . .:将当前目录下的所有文件和文件夹复制到容器的工作目录/app中RUN yarn install --production:在工作路径下运行yarn install --production安装依赖,根据package.json文件安装CMD ["node", "src/index.js"]:指定了容器启动时要执行的默认命令,将运行 Node.js,并执行src/index.js文件。- EXPOSE 3000:声明容器服务使用的端口,不会自动发布到宿主机;仍需通过 -p 配置端口映射。
- 在终端创建镜像
- 由于指定了基础镜像,可能会下载一些依赖镜像层
t表示指定镜像的名称和标签的组合,默认使用 "latest" 标签.表示告诉Docker在当前目录下查找Dockerfile文件
内容解释
指令解读
创建时指定平台
- 运行镜像创建容器
docker run:这是运行 Docker 容器的命令。dp 127.0.0.1:3000:3000:这是用于设置容器的运行选项的部分。d表示以后台(detached)模式运行容器,p127.0.0.1:3000:3000 表示将主机的 127.0.0.1 的 3000 端口映射到容器的 3000 端口。getting-started:这是指定要运行的 Docker 镜像的名称。
指令解读
查看运行结果:http://localhost:3000/
停止容器运行
- 更新项目内容,并重新制作镜像
- 修改项目文件:
src/static/js/app.js第 56 行 - 重新构建镜像并实例化容器
删除旧容器
- 在 DockerHub 上分享镜像
- 在DockerHub上创建一个和项目文件夹同名仓库
getting-started,并记住仓库名称前面的 Namespace 内容; - 为本地镜像添加一个标签,便签为
Namespace/getting-started; - 将镜像推送到DockerHub仓库,推送用新添加的标签名
官方Python 案例:Containerize a Python application
- 克隆案例项目
- 初始化 Docker
会新增一些文件
- 构建并启动容器(在项目文件夹下运行)
- Docker Compose 是一个用于定义和管理多个 Docker 容器的工具;通过执行
docker compose up,可以启动和管理定义在docker-compose.yml文件中的一组容器。 - 指令将会读取当前目录下的
docker-compose.yml文件,并根据其中定义的服务配置来启动容器。如果镜像不存在或具有更新,它将会在启动之前构建所需的镜像。 up表示启动容器,--build表示在启动之前构建镜像
指令解读
Python 案例 1 —— 强化学习(井字棋)
- 井字棋代码——from 刘建平
- 将文件放入一个空文件夹,并用pipreqs生成项目依赖文件
- 新建Dockerfile文件,文件内容如下
如果直接用python:3.11.3,则体积 900+M;Python官方镜像地址
- 构建镜像
- 运行镜像生成容器
程序会先训练模型 10 万次,然后会出现提示框,可以人机对弈;九宫格对应的按键位置为 qwe asd zxc

从能运行,到能够交付
原笔记还保留了井字棋强化学习示例的代码和截图。它们是继续整理实验材料的入口。
交付一个项目时,镜像之外还要写清楚依赖、启动配置、数据位置和外部服务。别人能够复现一次运行,Docker 才真正帮我们把环境讲清楚。