一、引言:为什么需要Docker?
还记得第2篇我们安装Linux环境时用了虚拟机吗?虚拟机确实解决了"一台电脑跑多个系统"的问题,但它有三大痛点:
-
重:每个虚拟机都要跑一个完整的操作系统内核,启动要几十秒,占用几个GB的磁盘
-
慢:虚拟机的资源是预先分配的,不管实际用不用,分配的CPU和内存都得占着,资源利用率不高
-
** "我这里能跑啊" **:开发环境里正常运行的应用,到了测试环境就各种报错------"依赖版本不对""系统库缺失""配置文件路径不一样"
Docker的解决方案很巧妙:不再笨拙地模拟整个硬件和操作系统,而是让所有容器共享宿主机的操作系统内核,只是在用户空间上做隔离。
Docker vs 虚拟机:
| 对比维度 | 虚拟机 | Docker容器 |
|---|---|---|
| 启动速度 | 几十秒到几分钟 | 毫秒到秒级 |
| 磁盘占用 | 每个VM几个GB | 镜像通常几十到几百MB |
| 资源利用率 | 预先分配,浪费多 | 按需使用,共享宿主 |
| 隔离级别 | 完全操作系统隔离 | 进程级隔离(共享内核) |
| 可移植性 | 受虚拟化平台限制 | 任何装了Docker的系统都能跑 |
Docker的三大核心价值:
-
环境一致性:开发、测试、生产用同一个镜像,"这里能跑"不再是问题
-
轻量化:共享宿主内核,秒级启动,一台服务器能运行几十上百个容器
-
可复现构建:Dockerfile描述构建步骤,任何人都能复现一模一样的镜像
二、核心概念:镜像、容器、仓库
Docker有三个核心概念,理解它们的关系是入门的关键。
2.1 镜像
镜像可以类比为:一个软件的"安装文件"或"快照"。它包含了运行某个应用所需的一切------代码、运行时、系统库、环境变量、配置文件。
关键特性:镜像是只读的、分层的。当你修改镜像时,Docker不会改动原有层,而是在上面叠加新的一层。这种设计让不同镜像可以共享相同的基础层,极大节省磁盘空间。
2.2 容器
容器可以类比为:运行起来的"进程实例"。容器就是镜像的运行态。
关键特性:容器是可读写的------Docker在镜像的只读层上加了一个读写层,你在容器中的所有操作(写文件、装软件)都发生在这个读写层。容器删除,读写层也随之消失。
镜像和容器的关系,就像程序和进程的关系(第11篇的概念!):
-
镜像是静态的定义(程序)
-
容器是动态的运行实例(进程)
-
同一个镜像可以启动多个互相隔离的容器
2.3 仓库
仓库可以类比为:镜像的"GitHub"。Docker Hub是默认的公共仓库,里面有官方维护的Nginx、MySQL、Python等镜像,也有社区贡献的各种镜像。
关键操作:docker pull nginx就是把Nginx镜像从仓库拉取到本地;docker push myname/myimage就是把你自己的镜像推送上去。
2.4 一张图理清三者关系
text
docker pull → 从仓库下载镜像到本地
docker run → 从本地镜像启动容器
docker build → 用Dockerfile把应用打包成镜像
docker push → 把本地镜像上传到仓库
三者关系也可以简单总结为:从仓库下载镜像,用镜像启动容器,把容器打包成新镜像,再将镜像上传到仓库。 整个Docker生态就是围绕这个闭环运转的。
三、Docker快速上手
3.1 安装Docker
Docker官方提供了一键安装脚本(适合快速体验,生产环境建议用发行版官方源):
bash
# Ubuntu/Debian
curl -fsSL https://get.docker.com | sudo bash
# 将当前用户加入docker组(省去每次sudo)
sudo usermod -aG docker $USER
# 退出重新登录后生效
验证安装:
bash
docker --version
docker run hello-world
如果看到Hello from Docker!的欢迎信息,说明安装成功。
3.2 docker run:启动你的第一个容器
bash
# 启动一个Nginx容器
docker run -d --name my-nginx -p 8080:80 nginx
参数拆解:
| 参数 | 含义 |
|---|---|
-d |
后台运行(detached mode) |
--name my-nginx |
给容器命名,方便后续操作 |
-p 8080:80 |
端口映射:将宿主8080端口映射到容器80端口 |
nginx |
使用的镜像名(本地没有会自动拉取) |
验证:
bash
curl http://localhost:8080
# 看到Nginx欢迎页面的HTML内容
3.3 docker ps:查看运行中的容器
bash
docker ps # 只看运行中的
docker ps -a # 包括已停止的
输出解读:
text
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 nginx Up 10 minutes 0.0.0.0:8080->80/tcp my-nginx
-
CONTAINER ID:容器的唯一标识(可以只用前几位来操作) -
STATUS:容器当前状态 -
PORTS:端口映射关系
3.4 docker exec:进入容器的"门"
bash
# 进入容器内部,启动bash交互式环境
docker exec -it my-nginx bash
参数拆解:
-
-i:交互模式,保持标准输入打开 -
-t:分配一个伪终端 -
my-nginx:容器名(也可以用容器ID) -
bash:要执行的命令
进入容器后,你就等于在这台"微型Linux"里操作了,可以查看配置、调试问题,就像SSH进了另一台机器一样:
bash
# 在容器内部
ls /etc/nginx/
cat /usr/share/nginx/html/index.html # 这就是Nginx欢迎页的源文件
exit # 退出容器
3.5 容器的生命周期管理
| 命令 | 作用 | 类比 |
|---|---|---|
docker start 容器名 |
启动已停止的容器 | 开机 |
docker stop 容器名 |
停止运行中的容器 | 关机 |
docker restart 容器名 |
重启容器 | 重启 |
docker rm 容器名 |
删除已停止的容器 | 卸载(需先stop) |
docker rm -f 容器名 |
强制删除(包括运行中的) | 强制卸载 |
重要 :docker rm只能删除已停止的容器。要删除运行中的容器,先docker stop再docker rm,或用docker rm -f直接强制删除。
3.6 查看日志
bash
# 查看容器日志
docker logs my-nginx
# 实时跟踪
docker logs -f my-nginx
四、实战:将应用打包成Docker镜像
下面我们将一个简单的Python Web应用打包成Docker镜像。
4.1 准备应用代码
bash
mkdir myapp && cd myapp
创建app.py:
python
from flask import Flask
import os
app = Flask(__name__)
@app.route('/')
def hello():
host = os.uname().nodename
return f'Hello Docker! (hostname: {host})'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
创建requirements.txt:
text
flask
4.2 编写Dockerfile
Dockerfile是一个描述了"如何构建这个镜像"的指令文件:
dockerfile
# 1. 基础镜像:从官方Python镜像开始
FROM python:3.10-slim
# 2. 设置工作目录
WORKDIR /app
# 3. 复制依赖文件并安装
COPY requirements.txt .
RUN pip install -r requirements.txt
# 4. 复制应用代码
COPY app.py .
# 5. 声明容器监听的端口(仅声明,实际映射在运行时指定)
EXPOSE 5000
# 6. 容器启动时执行的命令
CMD ["python", "app.py"]
逐条解释:
-
FROM:基础镜像,所有后续操作都在它的基础上叠加 -
WORKDIR:设置工作目录,后续命令和CMD都以此为基础 -
COPY:从宿主机复制文件到镜像中 -
RUN:在镜像构建时执行命令(安装依赖等) -
EXPOSE:文档性质的端口声明,真正的端口映射需要运行时的-p参数 -
CMD:容器启动时默认执行的命令
4.3 构建镜像
bash
docker build -t myapp:v1 .
-
-t myapp:v1:给镜像打标签,myapp是名称,v1是版本 -
.:表示Dockerfile在当前目录
输出会显示每一步的执行过程(Step 1/6 → Step 2/6 ...)。
4.4 运行容器
bash
docker run -d --name myapp-container -p 5000:5000 myapp:v1
验证:
bash
curl http://localhost:5000
# 输出:Hello Docker! (hostname: <容器ID>)
4.5 查看与管理镜像
bash
# 查看本地所有镜像
docker images
# 删除镜像(需先删除关联的所有容器)
docker rmi myapp:v1
# 给镜像打新标签
docker tag myapp:v1 myapp:latest
五、Docker常用命令速查
| 类别 | 命令 | 作用 |
|---|---|---|
| 镜像 | docker images |
查看本地镜像 |
| 镜像 | docker pull nginx:1.25 |
从仓库拉取镜像(1.25指定版本,不加则默认latest) |
| 镜像 | docker build -t name:tag . |
用Dockerfile构建镜像 |
| 镜像 | docker rmi 镜像名 |
删除镜像 |
| 容器 | docker run -d --name xxx -p 宿:容 镜像 |
启动新容器 |
| 容器 | docker ps / docker ps -a |
查看运行中/所有容器 |
| 容器 | docker exec -it 容器名 bash |
进入容器 |
| 容器 | docker start/stop/restart 容器名 |
启动/停止/重启容器 |
| 容器 | docker rm 容器名 |
删除容器 |
| 容器 | docker logs -f 容器名 |
跟踪查看日志 |
六、本篇小结
核心概念金三角:
-
镜像:构建一次,到处运行(相当于二进制可执行文件)
-
容器:镜像的运行实例,隔离但共享内核
-
仓库:镜像的存储和分发中心
最小化上手流程:
bash
docker run hello-world # 验证安装
docker run -d --name web -p 80:80 nginx # 后台启动Nginx
docker ps # 查看状态
docker exec -it web bash # 进入容器探索
构建自己的应用镜像:
bash
# 1. 写Dockerfile(FROM + COPY + RUN + CMD)
# 2. docker build -t app:v1 .
# 3. docker run -d -p 端口:端口 app:v1
动手练习
bash
# 1. 验证Docker安装
docker run hello-world
# 2. 启动Nginx容器并验证
docker run -d --name test-nginx -p 8088:80 nginx
curl http://localhost:8088
# 3. 进入容器看看Nginx的文件结构
docker exec -it test-nginx bash
# 在容器内:cat /usr/share/nginx/html/index.html
# 退出:exit
# 4. 停止并删除容器
docker stop test-nginx
docker rm test-nginx
# 5. 构建自己的应用镜像
mkdir ~/docker-test && cd ~/docker-test
# 按第四节的方法创建app.py、requirements.txt、Dockerfile
# docker build -t myapp:v1 .
# docker run -d --name myapp -p 5000:5000 myapp:v1
# curl http://localhost:5000
常见问题排查
端口已被占用:
text
Error: port is already allocated
换一个宿主机端口:-p 8081:80(宿主的8081映射到容器的80)。
镜像拉取失败:
text
Error response from daemon: pull access denied
检查镜像名是否正确,国内环境可配置镜像加速器(阿里云容器镜像服务提供免费加速器),配置方法搜索"Docker镜像加速"。
七、下篇预告
掌握了Docker的基本操作,下一篇我们将探索Linux中另一个重要的网络服务------DNS。DNS是互联网的"电话簿",下一篇我们将自己搭建一个内网DNS服务器,实现局域网内的域名解析,甚至可以劫持广告域名来屏蔽广告请求。
延伸思考 :Docker的"一次构建,到处运行"听起来完美,但实际中仍有"坑"------latest标签在不同时间拉取的镜像版本可能完全不同。生产环境推荐的做法是锁定具体版本(如nginx:1.25.3),确保所有环境跑的是完全相同的镜像。版本固定,才能让"可复现"真正落地。