简介:面向零基础开发者与运维人员的 Docker 入门手册,系统讲解容器化技术相对于传统虚拟机在启动速度、资源占用、隔离性等方面的优势,以及镜像、容器、仓库三个核心概念的关联。内容覆盖 Ubuntu 环境下安装 Docker Engine 与配置用户组的完整步骤,并详解镜像拉取、构建、删除以及容器启停、日志查看、进入容器调试等常用命令。实战部分以部署 WordPress 为例,逐步演示 docker-compose.yml 的编写方法与一键启动服务流程;数据管理章节则分别介绍数据卷和绑定挂载两种持久化方式,同时补充容器安全最佳实践与后续学习路径建议,包括官方文档、进阶方向与推荐书籍。整份资料为单个 PDF 文件,体积 760KB,目录结构清晰,便于随时查阅和按需定位。目前已有 1057 人学习下载,特别适合希望快速上手 Docker 并动手实践的新手。
1. Docker 新手入门指南:先别急着敲命令,把容器与镜像的关系想明白
如果你正打算照着《Docker 新手入门指南:从零开始掌握容器化技术》这份资料开始学,我先给你提个醒:多数人第一周卡住的不是命令记不住,而是没搞懂镜像和容器到底是两样什么东西,于是后面每一条命令都像在碰运气。这个标题真正要解决的,不是让你会背docker run,而是让你建立一条从镜像、容器、仓库到编排的完整链路——环境装好、服务跑起来、镜像能自己构建、出问题知道去哪看。适合被环境部署反复折腾过的新手,也适合那些看完零散教程却始终没跑通 CLI 的从业者。我先用这篇笔记,按同一套主线带你把每个环节踩一遍,尽量做到每一步你都跟得上。
2. 镜像与容器:先把"构建一次、到处运行"的底层逻辑弄清
2.1 镜像是只读模板,容器是它的运行实例
很多新手第一个搞混的点,就是把镜像当成"一个已经装好的系统"。镜像确实包含一个可运行的根文件系统,但它本身不运行任何东西,它是一份只读的模板;只有当你执行docker run时,Docker 才会基于这份模板创建一个带隔离环境的进程实例,这个实例才是容器。
我更愿意用"安装包与打开的程序"来类比:镜像像 Windows 里的安装包,体积固定、内容不可改;容器像安装包解压后正在运行的软件,有独立进程树、独立网络栈,也有自己的生命周期。你删除容器,镜像还在;你删除镜像,已经创建的容器不受影响,但再也无法用同样的模板新建容器了。
这里有一个新手很容易忽略的细节:容器不是"一台小电脑",它本质上是宿主机上的一个进程。它的文件系统、网络、PID 等信息是通过 Linux 内核的隔离机制"伪装"出来的独立视角,但底层实际用的还是宿主机的 CPU、内存、磁盘和内核。这也是为什么容器启动速度以毫秒到秒计,而虚拟机通常要几十秒。
# 拉取一个镜像 docker pull nginx:alpine # 查看本地已有的镜像 docker images上面docker pull把镜像从仓库下载到本地,docker images列出所有本地镜像。输出里能看到镜像的 TAG 和大小,nginx:alpine里的alpine是 tag,表示基于 Alpine Linux 的精简变体。新手不用急于把 tag 理解为版本号,它只是镜像的一个"标签",同一个镜像源可以打多个 tag。
2.2 分层存储与写时复制:为什么多个镜像可以共享几百兆
再看一层,镜像内部不是一个大文件,而是由多层只读层叠加而成。每一条会改变文件系统的 Dockerfile 指令,都可能产生一个新层。拉取镜像时,输出信息里那些Pull complete的条目,就是一层层独立下载的;多个镜像如果基于同一个基础镜像,就能共享底部的公共层,这也是为什么你拉了好几个镜像后,docker system df里的总占用往往小于各镜像大小之和。
运行容器时,Docker 会在镜像层之上再挂一个可写层。容器里对文件的修改、删除、新增全部发生在这个可写层,镜像层保持只读不动。这种机制叫写时复制(Copy-on-Write),带来的直接好处是:就算容器里改了文件,原始镜像也不会被破坏,多个容器基于同一镜像还能各自独立修改。
# 查看 nginx:alpine 由哪些层组成 docker history nginx:alpine --no-truncdocker history会列出镜像每一层对应的构建指令,--no-trunc防止输出被截断。你可以看到最上面几行是 Dockerfile 里的RUN、COPY产生的实际内容,底下的<missing>层来自基础镜像,不需要额外关注。建议新手跑一遍这条命令,直观感受一下"镜像 = 一层层文件系统快照"。
写时复制机制也埋下一个隐患:容器可写层与容器生命周期绑定,一旦容器被删除,可写层里写进去的数据也就跟着没了。这是很多新手数据丢失的第一大原因,后面避坑章会专门展开。
2.3 容器是进程,不是小虚拟机:隔离机制决定你会遇到哪些坑
容器能实现"隔离",靠的是 Linux 内核的 namespaces 和 cgroups 两套机制,分工完全不同。namespaces 负责"看不见":PID namespace 让容器只看到自己的进程,Network namespace 让容器拥有独立网络栈,Mount namespace 让容器看到独立的挂载视图;cgroups 负责"管得住":限制容器 CPU、内存、IO 配额,避免单个容器把宿主机资源吃满。
这些机制决定了容器与虚拟机的本质区别。我做了张表,方便你做技术选型时判断到底用哪种方案:
| 对比项 | 容器 | 虚拟机 |
|---|---|---|
| 隔离范围 | 进程级隔离 | 硬件级隔离 |
| 内核使用 | 共享宿主机内核 | 每个虚拟机有独立内核 |
| 启动速度 | 毫秒到秒级,秒内可用 | 通常几十秒甚至更久 |
| 镜像/磁盘体积 | 通常几十到几百 MB | 通常几 GB 起步 |
| 资源占用 | 低,只跑必要进程 | 高,系统本身占大头 |
| 隔离强度 | 较弱,内核漏洞可能跨容器 | 强,安全边界更清晰 |
这张表最要记住的是"共享内核"这行。正因为容器共享宿主机内核,你在 Linux 上能直接跑 Linux 容器;在 Windows 或 macOS 上,Docker Desktop 等于先在系统里搭了一个轻量 Linux 虚拟机,再让容器跑在那个虚拟机的内核上。很多新手在 Windows 上遇到容器网络、挂载盘"诡异失效"的问题,根源就在这里,不是 Docker 坏了,是中间多了一层虚拟化。
理解这层关系后,你再看docker run时压制网络、资源限制等高阶参数,就能明白它们背后到底在想隔离什么。接下来我们进入实操:先把跑通第一个容器的路径走完。
3. 本地跑通第一个容器:从装环境到复用命令的最小路径
3.1 安装与验证:先让 docker ps 这条命令在自己机器上能输出
装 Docker 的路径取决于你在什么系统上干活。我在 Linux 服务器上习惯用发行版自带的包,这样省事、少配置;Windows 和 macOS 则直接用 Docker Desktop,它会顺带处理虚拟化后端。但无论走哪条路,最终验证标准只有一个:docker ps能正常执行并输出一行表头。
在 Debian/Ubuntu 系的 Linux 上,常见做法是用系统自带包:
# 安装 docker.io 包 sudo apt update sudo apt install -y docker.io # 启动守护进程并设为开机自启 sudo systemctl enable --now docker # 把当前用户加入 docker 组,避免每条命令都加 sudo sudo usermod -aG docker $USER # 重新登录终端后验证 docker --version docker ps这段命令的要点是:systemctl enable --now同时完成"开机自启"和"立即启动"两件事;把用户加入 docker 组后,需要退出重新登录才生效,否则docker ps仍会报权限错误。用发行版自带包的好处是依赖由系统包管理器统一管理,缺点是版本可能比官方仓库稍微旧一点,但对新手学习完全够用。
Windows 和 macOS 上,装好 Docker Desktop 后,打开终端执行docker run hello-world,能打印出说明信息就代表通了。这里多说一句:装完如果拉镜像慢,优先考虑网络环境,也可以配置 registry mirror,但不要为了快随便用来历不明的一键脚本,安全问题比那几分钟等待严重得多。
3.2 跑一个 Nginx 容器入门:docker run 的五个参数代表什么
环境就绪后,第一个正式容器我建议选 Nginx,因为它无需写任何业务代码,启动即提供服务,非常适合用来验证"端口映射"和"容器生命周期"。
# 后台运行一个 nginx 容器,命名 web,映射宿主机 8080 端口到容器 80 端口 docker run -d --name web -p 8080:80 nginx:alpine # 查看正在运行的容器 docker ps # 在宿主机验证服务 curl http://localhost:8080 # 查看容器日志 docker logs web逐个拆解参数:-d表示后台运行,不占用当前终端;--name web给容器起名字,后续操作可以直接用名字代替容器 ID;-p 8080:80是端口映射,宿主机 8080 转发到容器内的 80,省略它则宿主机无法访问容器服务;nginx:alpine是镜像名加 tag,代表镜像来源和变体。
一个常见误区是把-p和 Dockerfile 里的EXPOSE混为一谈。EXPOSE只是镜像作者写下的"声明",告诉你镜像内服务默认监听哪个端口,并不产生实际映射;真正打开宿主机入口的只有-p或--network host等方式。所以如果你发现容器起来了但外部访问不到,先确认是不是漏了-p。
容器内的服务为什么会被宿主机访问到?这涉及前面说的 Network namespace:容器有独立网络栈,-p会在宿主机上开一个端口把流量转进容器的对应端口。这里的 Nginx 监听容器内 80 端口,所以映射目标必须是 80,不能想当然改成别的值。
3.3 日常必用的四个命令组:别急着背,先会用再理解
入门阶段不需要背几十条命令,先把下面这张表用熟,日常运维和排查就能撑起来:
| 命令 | 作用 | 常见场景 |
|---|---|---|
docker ps -a | 列出所有容器(含已退出) | 容器启动失败后看它的退出状态 |
docker logs -f web | 跟踪容器日志输出 | 确认服务是否正常启动、看报错 |
docker exec -it web sh | 进入容器内执行命令 | 手动查看容器里的文件、进程、网络 |
docker rm -f web | 强制删除容器 | 重新创建同名容器 |
docker system prune -f | 清理悬空资源和未使用镜像 | 磁盘空间不足时释放空间 |
# 进入运行中的 nginx 容器,启动一个 shell docker exec -it web sh # 在容器内查看进程 ps aux # 退出容器 exitdocker exec -it里的-i表示保持标准输入打开,-t分配一个伪终端,两者配合才能得到一个可交互的 shell。容器里可能没有 bash,但 Alpine 镜像都带 sh,所以固定用sh最稳。这一招在服务起不来时极为有用:进容器手动执行启动命令,能绕过 Docker 的启动配置,直接看到程序本身报什么错。
docker system prune -f我单独提醒一下:它会删除所有未被运行容器引用的镜像、停止的容器和构建缓存,新手第一次跑完会发现磁盘瞬间多出几个 GB,这是正常现象。但如果你有想保留但暂时没运行的容器,先确认清楚再执行,这命令没有后悔药。
4. 写第一份 Dockerfile:从"会用镜像"到"构建自己的镜像"
4.1 一个最小 Flask 示例:每条 Dockerfile 指令都在做什么
依赖别人的现成镜像只是第一步,用 Docker 的核心收益在于把自己的应用打包成镜像,让它在任何装了 Docker 的机器上以相同方式启动。这里用一个极小的 Flask 应用做例子,它只有两个代码文件,逻辑足够简单,方便把每条指令的作用看明白。
先准备应用文件app.py:
from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "hello docker" if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)再写依赖文件requirements.txt:
flask==3.0.3然后是与这两个文件同目录下的Dockerfile:
# 基础镜像使用 slim 变体,体积小且兼容性好 FROM python:3.12-slim # 设置容器内工作目录 WORKDIR /app # 先拷贝依赖文件,利用构建缓存加速 COPY requirements.txt . # 安装依赖,禁用 pip 缓存以减少镜像体积 RUN pip install --no-cache-dir -r requirements.txt # 再拷贝应用代码 COPY app.py . # 声明容器内服务监听端口 EXPOSE 8000 # 启动命令:必须以前台方式运行 CMD ["python", "app.py"]WORKDIR /app指定后续命令的默认目录,不存在会自动创建,之后所有容器内相对路径都以此为基准。COPY requirements.txt .先拷贝依赖文件,是为了让依赖安装层单独缓存——只要 requirements.txt 没变,后续构建就不需要重新下载依赖,这个顺序直接影响构建速度。
RUN pip install --no-cache-dir里的--no-cache-dir会阻止 pip 把下载的安装包缓存进镜像,能省下几十到几百 MB 空间。CMD必须写前台运行方式,["python", "app.py"]这种 JSON 数组形式不会经过 shell 解析,信号处理更干净;如果写成CMD python app.py,容器里的 1 号进程会变成 shell,信号转发行为会有差异。
构建并运行:
# 在当前目录下构建镜像,名字 flask-demo,tag v1 docker build -t flask-demo:v1 . # 运行容器并映射端口 docker run -d -p 8000:8000 --name flask-demo flask-demo:v1 # 验证服务 curl http://localhost:8000-t flask-demo:v1给镜像打标签,格式是"名字:版本号",版本号用 tag 表达。最后的.指定构建上下文为当前目录,Docker 会把整个上下文发给守护进程,所以目录里不该有的临时文件要尽早清理。访问地址是宿主机的 8000,因为-p已把宿主机 8000 映射到了容器 8000;应用代码里监听0.0.0.0是为了让容器内所有网卡都能收到请求,否则只能容器内部访问,宿主机端口映射也没用。
4.2 基础镜像怎么选:slim 和 alpine 的取舍
基础镜像是新手最容易纠结的选择题。以 Python 为例,官方仓库里常见有python:3.12、python:3.12-slim、python:3.12-alpine三种,体积和对不同坑的处理差异很大:
| 镜像 tag | 体积特点 | 适用建议 |
|---|---|---|
python:3.12 | 几 GB 级,包含完整构建工具 | 本地调试方便,生产环境体积过大 |
python:3.12-slim | 几百 MB,基于 Debian,带 glibc | 新手首选,兼容性最好 |
python:3.12-alpine | 几十 MB,基于 musl libc | 体积敏感时使用,可能遇到二进制兼容坑 |
alpine变体体积优势明显,但代价是它用的 C 库是 musl 而非主流的 glibc。很多预编译二进制包(比如某些数据处理库、带原生扩展的 SDK)是针对 glibc 打包编译的,在 alpine 里直接报symbol lookup error或exec format error,这属于典型的"为了体积省出事故"。
我一般建议新手默认选slim变体,先把功能跑通,再考虑体积优化。如果项目整体用 Go 或 Rust 这类静态编译语言,alpine 相对安全;如果是 Python、Node 这类带原生依赖的解释型语言,alpine 要谨慎。另外,如果只是在内部环境做快速验证,用完整版基础镜像也完全可以,体积问题留到生产前再处理。
4.3 构建缓存、.dockerignore 与多阶段构建:镜像瘦身的三板斧
新手构建镜像最常见的两个问题:每次构建都很慢,以及镜像莫名巨大。根因大多是没理解构建缓存机制,以及把不需要的文件也打进了镜像。
Docker 构建时逐条执行 Dockerfile 指令,每条指令生成一层;如果一条指令的内容和上下文都没变化,Docker 会直接复用之前构建好的层。这就是前面把COPY requirements.txt放在依赖安装之前的原因。如果颠倒顺序,哪怕代码改一行,整个依赖安装层也会被迫重新执行。
减少镜像体积的另一块是.dockerignore文件,它和.gitignore类似,声明哪些文件不进入构建上下文:
__pycache__/ *.pyc .git/ .venv/ node_modules/在项目根目录建一个.dockerignore后,上面这些目录不会随构建上下文发送给 Docker。否则你把几十 MB 的node_modules或.git目录复制进上下文,镜像里虽未必包含它们,但构建前的传输和构建时的 COPY 都会显著变慢。
多阶段构建则是体积优化的大杀器,尤其适合带编译步骤的语言。以 Go 为例:
# 第一阶段:编译 FROM golang:1.22 AS builder WORKDIR /src COPY go.mod . RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /out/myapp . # 第二阶段:只保留运行所需 FROM alpine:3.20 COPY --from=builder /out/myapp /usr/local/bin/myapp ENTRYPOINT ["myapp"]第一阶段用完整 Go 镜像完成编译,这个镜像可能超过 800 MB,但它只存在于构建过程里;第二阶段只把编译产物复制进一个几十 MB 的 alpine 镜像。最终产物里没有编译器、没有源码、没有任何多余的构建工具,攻击面也小得多。CGO_ENABLED=0是为了得到纯静态链接的二进制,避免运行阶段依赖 glibc。
这套组合拳打完之后,一个 Go 服务镜像通常能控制在 20 MB 以内,Python 服务也能从几个 GB 降到三四百 MB。看完这个对比,你大概能理解为什么很多团队把镜像体积作为交付质量的硬指标之一。
5. 容器化入门绕不开的 5 个坑:现象、原因、处理办法
5.1 容器启动后立刻退出,Exit 0 让人一头雾水
现象:用docker run启动容器,命令执行完,docker ps看不到这个容器,docker ps -a里显示Exited (0) 2 seconds ago。退出码是 0,看起来不像崩溃,但容器就是没了。
原因:容器的生命周期等于它的 1 号进程的生命周期。如果主进程在前台跑完所有逻辑后自己退出,容器也随之结束。新手常犯的错误是把本该常驻的服务写成一次性执行,或者启动命令里本来就有问题但恰好退出码是 0。
解决:先看日志docker logs <容器名>,但更有效的做法是直接交互式运行:docker run -it --rm <镜像名> sh,进容器后手动执行启动命令,看真实输出。如果是自研服务,优先确认是否以阻塞方式监听端口,比如 Flask 的app.run()、Nginx 的nginx -g 'daemon off;',绝对不能后台运行。用--rm的好处是调试完容器自动删除,不会留垃圾。
5.2 容器起来了,但服务访问不到,端口像黑洞
现象:curl http://localhost:8080连接被拒绝,但docker ps显示容器正在运行,docker logs也没有异常。
原因:两种情况最常见。一是只写了 Dockerfile 里的EXPOSE,没在docker run里加-p,宿主机端口根本没打开;二是加了-p,但应用监听的地址是127.0.0.1,只接受容器自身访问,不接受来自宿主机端口转发的流量。
解决:先执行docker port <容器名>查看实际端口映射;再用docker exec -it <容器名> sh进入容器,执行curl http://localhost:8000,如果能通但宿主机不通,基本可以断定是应用绑定地址问题。把应用监听地址改成0.0.0.0即可。另外-p 8000:8000左侧是宿主机端口,右侧是容器端口,写反也会出现"看起来映射了但根本不通"的现象。
5.3 镜像越来越大,构建越来越慢
现象:一个简单应用构建出的镜像超过 1 GB,每次docker build都要等好几分钟。
原因:基础镜像选成了完整版而非 slim;构建上下文里塞了node_modules、.git、缓存文件;依赖安装时没禁缓存;多个RUN指令各自产生大层,没有合理合并。
解决:先把基础镜像换成对应语言的-slim变体;再建.dockerignore排除无关目录;然后把依赖安装命令补上--no-cache-dir或对应语言的缓存禁用参数;最后检查多个RUN是否可以用&&合并。做一次优化通常能把体积降到原来的一半以下。
5.4 改了代码,重新构建后容器里还是旧代码
现象:修改了app.py,执行docker build和docker run,访问服务发现还是旧的逻辑。
原因:这是构建缓存命中的典型副作用。如果 Dockerfile 里COPY . .之前的某条指令没有变化,而COPY . .所在层因为构建上下文内容变了本来应该失效,但实际因为某层指令(比如RUN pip install)没变,缓存命中后可能跳过了代码层的刷新。另一个常见原因是构建时没有真正把新代码 COPY 进去,比如 dockerignore 误把源码目录排除了。
解决:先强制禁用缓存构建一次:docker build --no-cache -t flask-demo:v1 .,确认镜像是否正常;如果强制构建后代码对了,再回头调整 Dockerfile 的指令顺序,把最频繁变化的 COPY 放在最后。排查阶段不要依赖老旧镜像,直接删掉旧 tag 重建,别怕浪费那几分钟构建时间。
5.5 容器一删,数据全没了,文件像没存在过
现象:容器里程序写了一些数据,容器因为升级或误操作被docker rm删除,再重新启动一个新容器,数据不见了。
原因:数据写在了容器的可写层。前面第 2 章说过,可写层与容器生命周期绑定,容器删除时该层一并销毁。这不是 Bug,是设计如此。
解决:把数据放到容器之外。临时调试用绑定挂载:docker run -v $PWD/data:/app/data,宿主机目录和容器目录共享;长期稳定数据用命名卷:docker volume create mydata,然后docker run -v mydata:/app/data。命名卷由 Docker 管理,位置在宿主机 Docker 数据目录里,适合数据库、上传文件这类持久化数据。备份时可以直接打包卷目录,也可以用容器把卷内容打包出来:
# 将命名卷 mydata 打包到宿主机当前目录 docker run --rm -v mydata:/data -v $PWD:/backup alpine tar czf /backup/mydata.tar.gz -C /data .这里用了一个一次性容器来挂载命名卷和宿主机目录,tar打包后容器自动销毁。恢复时反向解包即可。数据卷这条线值得新手提前掌握,否则跑几次数据库容器就会踩坑。
6. 从单容器到多服务:用 docker compose 管起你的第一套环境
当你需要跑两个以上互相依赖的服务时,一条条docker run就变得难以维护了。常见做法是用 docker compose 把这些关系写进一个 YAML 文件,实现"一条命令起整套环境"。
services: web: build: . ports: - "8000:8000" volumes: - .:/app environment: - FLASK_ENV=development redis: image: redis:7-alpine在项目目录下执行:
docker compose up -dup -d后台拉起全部服务,-d即可docker compose logs -f跟踪日志,docker compose down停止并清理。文件里的volumes把当前目录挂载进容器,代码改动无需重新构建镜像即可生效,这套"挂载开发"模式适合日常调试。
我把一个工作习惯放在最后:接到新项目时,先别急着写 Dockerfile,而是先用docker run -v把一个现成基础镜像和源码挂载起来跑通,确认应用本身没问题后,再把启动参数一步步固化进 Dockerfile。这样能区分开"应用问题"和"容器化问题",排查时不至于两头猜。另外一个我用到现在的技巧是docker commit快速保存容器当前状态,在临时踩坑时当"后悔药"用,但我不建议把它当正式交付方式,镜像应该从 Dockerfile 可重复构建,而不是靠手工现场拍快照。这套路线走完,你至少能独立解决八成以上的容器日常问题;剩下的,就是多看日志、多进容器里看真实状态。希望帮到你。
本文还有配套的精品资源,点击获取