简介面向零基础开发者与运维人员的 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.pyfrom flask import Flask app Flask(__name__) app.route(/) def hello(): return hello docker if __name__ __main__: app.run(host0.0.0.0, port8000)再写依赖文件requirements.txtflask3.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-demotag 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_ENABLED0 go build -o /out/myapp . # 第二阶段只保留运行所需 FROM alpine:3.20 COPY --frombuilder /out/myapp /usr/local/bin/myapp ENTRYPOINT [myapp]第一阶段用完整 Go 镜像完成编译这个镜像可能超过 800 MB但它只存在于构建过程里第二阶段只把编译产物复制进一个几十 MB 的 alpine 镜像。最终产物里没有编译器、没有源码、没有任何多余的构建工具攻击面也小得多。CGO_ENABLED0是为了得到纯静态链接的二进制避免运行阶段依赖 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_ENVdevelopment 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 可重复构建而不是靠手工现场拍快照。这套路线走完你至少能独立解决八成以上的容器日常问题剩下的就是多看日志、多进容器里看真实状态。希望帮到你。本文还有配套的精品资源点击获取