Docker基本介绍
Docker 是一个开源的平台,用于开发、发布和运行应用程序。它最核心的理念是 “容器化”(Containerization),可以把它想象成一个非常高效、轻量级的“虚拟机”,但它的工作原理和虚拟机有本质的不同。
举一个简单的例子,对于传统运输业(好比传统软件部署),货物(软件、库、依赖)形状各异,装船(服务器)非常麻烦,容易互相影响,效率低下。而现代集装箱运输(好比 Docker): 把所有货物(代码、运行时环境、系统工具、系统库)都打包进一个标准化的集装箱(Container) 里。这个集装箱可以在任何一艘货船(任何安装了 Docker 的服务器)上被轻松、快速、稳定地搬运和运行,完全不用担心船上的环境。Docker 就是这个集装箱系统,它保证了应用程序在任何环境中都能一致地运行。
核心组成
Docker引擎
这是 Docker 的核心,是一个客户端-服务器端的应用程序。主要由以下三部分组成:
1.守护进程 - “大脑与执行者”
- 角色:这是一个长期运行在后台的系统服务(在 Linux 上通常是
dockerd
进程)。它是真正的“引擎”部分,负责所有繁重的工作。 - 核心职责:
- 监听请求:持续监听 Docker API 发来的请求。
- 管理核心对象:创建和管理 Docker 的核心对象,如镜像(Images)、容器(Containers)、网络(Networks) 和数据卷(Volumes)。
- 协调工作:与操作系统内核交互,调用
containerd
和runc
等底层工具来实际创建和运行容器。
2.REST API - “沟通的桥梁”
- 角色:Docker Daemon 提供了一个 RESTful API 接口。这个接口规定了外部程序如何与 Daemon 进行指令和信息的交互。
- 核心职责:
- 提供标准接口:无论是 Docker 自己的命令行工具(CLI),还是其他第三方工具(如 Kubernetes),都必须通过这个 API 与 Docker Daemon 通信。
- 实现远程控制:正因为有这个 API,你才能在一台机器上使用
docker
命令去控制另一台远程服务器上的 Docker Daemon。
3.命令行界面 (CLI) - “你手中的遥控器”
- 角色:我们最常直接打交道的部分,就是
docker
这个命令。它是一个客户端工具。 - 核心职责:
- 接收用户命令:你输入
docker run
,docker ps
等命令。 - 与 API 交互:CLI 会将你的命令翻译成 API 请求,发送给 Docker Daemon。
- 显示结果:接收 Daemon 通过 API 返回的结果,并将其格式化后显示在终端上。
- 接收用户命令:你输入
Docker引擎协调工作:首先,用户在终端中输出docker run -d nginx
并按下回车。这时,CLI接受到这个命令,将其封装成一个格式正确的 HTTP 请求(基于 REST API 的规范)。然后这个请求通过REST API 发送到 Docker Daemon 的 API 端点。Docker Daemon接收到API请求开始解析和执行(检查本地是否有 nginx
镜像,如果没有,就从 Docker Hub 下载(pull
)----->调用 containerd
和 runc
等底层运行时,根据 nginx
镜像的规范创建一个新的、隔离的进程(容器)----->配置容器的网络、文件系统等。------->启动容器内的主进程(nginx
)) 。最后Docker Daemon 将容器已成功启动的消息和其 ID 通过 REST API 返回给 CLI,CLI 接收到成功消息,将其简洁地打印在你的终端上(例如,输出一长串容器 ID)
镜像
镜像是只读的、分层的文件系统模板。它包含了运行某个软件所需的一切:代码、运行时环境、库、环境变量和配置文件。
- 只读性(Read-only):镜像一旦创建就无法更改。这种设计保证了镜像的一致性——无论在哪里,同一个镜像的内容都是完全相同的。
- 分层结构(Layered):镜像由一系列层(Layer)组成。每一层代表 Dockerfile 中的一条指令(例如,
RUN apt-get install
)。这种设计极大地提升了存储和传输的效率。多个镜像可以共享相同的底层(如基础操作系统层),只需存储差异部分。
Docker镜像与传统拷贝的系统镜像有本质的区别,它不包含一个完整的操作系统内核而是假设自己将运行在一个已有的、兼容的Linux内核之上。它里面包含的只是一个最小化的文件系统(例如,一个精简版的Ubuntu用户空间,只有/bin
, /lib
, /usr
等必要的目录和文件),以及你的应用程序和其依赖。当你运行 docker run -it ubuntu:latest /bin/bash
时,你进入的“Ubuntu系统”,其实只是一个拥有Ubuntu文件系统风格的环境,它直接使用了宿主机的Linux内核。这就是它如此轻量的原因。而传统拷贝的Ubuntu镜像,是一个完整的磁盘快照,包含了GRUB引导程序、Linux内核、驱动、所有系统文件和用户文件。它被设计用于在裸机上启动一个独立、完整的操作系统。
Docker的分层结构实现了极致的效率和可复用性。一个Docker镜像并不是一个大的、单一的文件块。它是由一系列只读的层(Layer) 叠加而成的,每一层都是在上一层的基础上进行的一组文件变更(增、删、改)。镜像通常通过一个名为 **Dockerfile
** 的文本文件来定义和构建。Dockerfile
中的每一条指令都会在镜像中创建一个新的层。以以下Dockerfile为例:
# 第1层:基础层(Base Layer)
FROM ubuntu:20.04# 第2层:执行命令,安装nginx
RUN apt-get update && apt-get install -y nginx# 第3层:拷贝本地文件
COPY index.html /var/www/html/# 第4层:指定启动命令
CMD ["nginx", "-g", "daemon off;"]
Docker 会按顺序执行 Dockerfile 中的每一行指令,每条指令(如 FROM
、RUN
、COPY
)都会生成一个新的层(Layer)。这些层是只读的,最终叠加成一个完整的镜像。Docker 的层是内容可寻址的(通过哈希值唯一标识)。如果两个不同的 Dockerfile 在某一步之前的指令完全一致(例如都基于 ubuntu:20.04
并执行了相同的 RUN apt-get update
),则这些层会被直接复用,而不会重复构建或下载(例如,两个镜像都基于 ubuntu:20.04
,则它们共享完全相同的底层文件系统层,而不会重复存储)。构建完成后,所有层会被逻辑上组合成一个完整的文件系统(通过联合文件系统如 overlay2
实现),但物理上每一层仍然是独立的。当你运行容器时,Docker 会在所有只读层之上添加一个可写层(容器层)。
注意:如果你修改了Dockerfile并重新构建时,Docker会复用前面没有变化的层的缓存。每一层只记录文件变化,而不是完整拷贝。如果某一层删除了一个文件,它并不是真的在磁盘上删除,而是在新层中记录一个“此文件已被删除”的标记。最终呈现给用户的联合文件系统里,这个文件就看不见了。
容器
容器是镜像的一个运行时的实例。当你使用 docker run
命令时,Docker 引擎会从镜像创建一个容器。
- 可写层(Writable Layer):当容器启动时,Docker 会在镜像的只读层之上添加一个可写的顶层(容器层)。所有对运行中容器的修改(如创建文件、安装新包、写入日志)都发生在这个可写层中。
- 隔离性:容器使用内核的命名空间(Namespaces)和控制组(Cgroups)技术,与主机和其他容器在进程、网络、文件系统等方面实现隔离,就像一个轻量级的沙箱。
容器将镜像中打包的静态模板变为动态的、运行中的应用进程。每个容器都拥有自己独立的运行环境,互不干扰。你可以在同一台机器上同时运行一个 Python 2.7 应用和一个 Python 3.10 应用而毫无冲突。
Docker的容器之间虽然通过Linux 内核的三大核心技术(命名空间Namespaces、控制组cgroups、联合文件系统)来实现隔离,使两个容器之间无法看到对方的进程,也无法直接读写对方的文件。但是Docker还是提供了容器之间通信的方式。容器之间可以通过网络进行通信(最常见的是HTTP/REST API)。Docker 提供了自定义网络来方便地管理容器间的连接,如果需要容器之间进行通信,最佳方式是为一组需要通信的容器创建一个独立的自定义网络。
1.创建自定义网络
docker network create my-app-network
2.将容器连接到同一网络
# 启动一个 Redis 容器,并连接到自定义网络
docker run -d --name redis-server --network my-app-network redis:alpine# 启动一个 Python 应用容器,连接到同一个网络
docker run -d --name python-app --network my-app-network -p 5000:5000 my-python-app
3.使用容器名进行通信
在自定义网络中,Docker 内置了 DNS 服务器,容器之间可以直接通过容器名来访问对方,而不是IP地址(IP地址可能会变)。在上面的例子中,python-app
容器里的代码要连接 Redis,只需要使用 redis-server
这个主机名即可:
# 在 Python 应用代码中
redis_client = redis.Redis(host='redis-server', port=6379, db=0)
# 注意:这里写的不是IP,而是另一个容器的名称!
容器的生命周期由 Docker 引擎管理,常用命令包括:
docker create
:从镜像创建一个容器(但并不启动)。docker start
:启动一个已存在的容器。docker run
:create
和start
的结合(最常用)。docker stop
:停止运行中的容器。docker rm
:删除已停止的容器。
注意:删除容器会同时删除其可写层!任何在容器运行时产生的、未持久化的数据都会丢失。
使用 Docker 镜像的完整流程
1.查找
主要的公共镜像仓库是 Docker Hub(类似于 GitHub 对于代码)。你可以在上面搜索你需要的任何软件镜像,例如 nginx
, mysql
, python
, redis
, node
等。常见镜像主要有以下三部分:
- 官方镜像:由软件官方维护,质量最高,最值得信赖。镜像名前没有用户名,例如
nginx
,python
。 - 认证镜像:来自可信的公司或组织,例如
microsoft/
,google/
。 - 社区镜像:由个人开发者维护,质量参差不齐,使用时需注意阅读文档和评价。
也可使用命令docker search ......搜索
2.下载镜像(Pull)
找到想要的镜像后,使用 docker pull
命令将其下载到本地。例如:
# 下载最新的官方 Nginx 镜像
docker pull nginx# 下载指定版本的镜像(强烈推荐,避免版本更新导致意外)
docker pull nginx:1.23-alpine# 下载带用户名的镜像(例如来自微软的.NET运行时)
docker pull mcr.microsoft.com/dotnet/aspnet:6.0
3.运行镜像(Run)
下载后,使用 docker run
命令来启动一个容器实例。例如
# 运行Nginx并映射端口
docker run -d --name my-nginx -p 8080:80 nginx:alpine
-d
(--detach):在后台运行容器。--name
:给容器起一个有意义的名字,方便管理。-p 8080:80
:将主机(你的电脑)的 8080 端口映射到容器的 80 端口。- 现在,你可以在浏览器访问
http://localhost:8080
来看到 Nginx 的欢迎页面
- 现在,你可以在浏览器访问
Docker与ROS
模块化与解耦 (Modularity & Decoupling)
- Docker:将每个应用/服务及其依赖打包成一个独立的容器。每个容器是一个功能单元(如数据库、Web服务器、日志服务)。
- ROS:将机器人软件系统分解成多个独立的节点(Node)。每个节点是一个进程,负责一个特定的功能(如传感器数据采集、运动控制、SLAM)。
共同点:两者都推崇将复杂系统拆分为小而专的模块,模块之间通过定义良好的接口进行交互,从而降低系统复杂度,提高可维护性和可复用性。
通信机制 (Communication)
- Docker:容器之间可以通过网络进行通信(最常见的是HTTP/REST API)。Docker 提供了自定义网络来方便地管理容器间的连接。
- ROS:节点之间通过主题(Topics)、服务(Services) 和动作(Actions) 这三种核心机制进行通信,基于发布/订阅和请求/响应模式。
共同点:两者都提供了强大的、标准化的通信基础设施,让模块之间能够轻松地交换数据和服务。
环境隔离与依赖管理
- Docker:通过容器技术实现极致的环境隔离。每个容器有自己的文件系统、网络和进程空间,彻底解决了依赖问题。Python 2 和 Python 3 的应用可以毫无冲突地运行在同一台主机上。
- ROS:主要通过功能包(Package) 和工作空间(Workspace) 来管理依赖。虽然隔离性不如 Docker 强(ROS节点通常共享主机ROS环境),但理念是相似的:将代码、配置和依赖组织在一起,形成一个可复用的单元。
共同点:都致力于让软件模块能够“自带环境”,减少与系统其他部分的冲突,方便分发和部署。
4编排与启动 (Orchestration & Launch)
- Docker:使用
docker-compose.yml
文件或 Kubernetes 等工具来定义和启动一组需要协同工作的容器。 - ROS:使用
.launch
文件来定义和启动一组需要协同工作的节点。
共同点:都提供了声明式的配置文件,用来描述一个由多个模块组成的复杂系统应该如何被启动和连接。docker-compose up
和 roslaunch
命令的角色非常相似。
差异之处:
Docker+ROS:
- 环境标准化:用 Docker 容器来封装一个标准的、可复现的 ROS 开发环境。新成员无需在本地复杂地安装配置ROS、CUDA、OpenCV等依赖,只需一条
docker run
命令就能获得一个完全一致的开发环境。 - 依赖隔离:如果一台物理机器人上需要运行多个不同版本ROS的算法(例如,一个模块需要ROS Noetic,另一个需要ROS 2 Humble),可以为每个算法创建一个独立的 Docker 容器,从而避免版本冲突。
- 简化部署:将整个ROS应用程序及其所有依赖打包成一个Docker镜像,可以轻松地部署到不同的机器人(如无人机、无人车)或不同的计算机上,实现“一次构建,处处运行”。