
Docker这个词我在新手期听了不下三十遍。每次看到教程里拉个镜像、起个容器这种话我都是一脸懵镜像是个什么东西容器又是什么为什么我要多学一套新名词才能部署一个程序后来被工作里在我电脑上是好的啊这句话反复毒打之后我才真正静下心来把Docker啃了一遍。回头看这东西的底层逻辑其实非常简单只是很多教程默认你已经懂了一堆前置概念才让菜鸟觉得难。这篇就是给纯新手写的超详细入门不扯虚的不堆术语。我会从为什么要用Docker开始一直讲到镜像、容器、数据卷、Dockerfile、Compose这些核心操作每一步都有命令、有解释、有我当时踩过的坑。你只需要一台能联网的电脑跟着走一遍就能把Docker用起来。1. 别急着装Docker先想清楚它到底帮你省了什么1.1 环境不一致这道坎Docker是怎么填平的先讲一个我自己的惨痛经历。早年间我做一个模拟项目X本地开发环境是某版本的Python搭配某个系统版本跑得好好的。结果换了一台机器依赖装不上数据库连不上整整折腾了两天才发现是底层库版本对不上。这种问题在团队里太常见了A同学的代码在B同学的电脑上报错C同学说我这边明明能跑最后大家围在一起猜环境差异。Docker解决的就是这个问题。它的思路很朴素把程序运行所需要的整个环境——操作系统底层的依赖、运行时、代码、配置——打包成一个标准化的箱子。这个箱子在任何装了Docker的机器上打开之后里面的环境是完全一致的。你在本地怎么跑服务器上就怎么跑中间没有我明明这样装了的偏差。这么说吧传统部署像是在每家餐厅后厨现买食材、现调酱料口味全看厨师临场发挥。而Docker是把整个后厨连锅带灶一起端过来谁拿到手炒出来的菜都是一个味儿。1.2 用披萨配方和烤箱理解镜像与容器菜鸟最容易卡住的就是镜像和容器这两个词。我第一次听的时候脑子里的画面是一面镜子和一个装水的罐子完全对不上号。后来我找到一个还算贴切的类比镜像是一份披萨配方冷冻面饼容器是放进烤箱烤出来的那份披萨。配方本身是不能吃的但它是标准化的——不管谁照着这份配方做出来的饼底、馅料都完全一样。这个配方就是镜像Image它是只读的你就算烤了十次配方也不会变。而每次烤出来的披萨就是容器Container它是活生生的实例有自己的状态可以被吃掉停止、扔掉删除再重新烤一个。所以镜像是有几百兆甚至几个G的模板文件容器是镜像运行起来后正在跑的进程。容器的本质是进程不过这个进程拥有自己独立的文件系统、网络和进程空间看起来像一台小小的虚拟机但它不是虚拟机。1.3 什么场景该用Docker什么场景别硬上我也见过有人什么都往Docker里塞然后抱怨怎么这么难用。Docker适合的场景其实挺明确的需要快速搭建一套开发环境比如MySQL、Redis、消息队列一条命令就起好。想让应用在开发、测试、生产环境之间无缝迁移。要跑一些可能会污染系统环境的实验性软件。需要同时运行同一个软件的不同版本互不干扰。不适合的场景也有如果你的应用是带图形界面的桌面软件而且重度依赖显示器硬件那Docker会非常别扭如果程序需要依赖宿主机上特殊的硬件驱动比如某些显卡加速、特殊USB设备也要谨慎。Docker适合的是服务端程序、命令行工具、数据处理任务这类东西把它理解成一个部署服务的工具箱会更准确。2. 装好Docker菜鸟最容易踩的三个坑2.1 不同系统的安装路线与验证方法我不打算把安装命令抄一遍因为官方文档写得很清楚而且各大系统差异不小。这里只提醒菜鸟容易卡住的地方。Linux环境最简单包管理器里基本都有装完直接可用。macOS和Windows则建议装图形化的桌面版客户端因为它会帮你把底层虚拟机环境一起搞定省得自己配置。Windows用户特别注意一定要提前把系统自带的虚拟化功能WSL2或Hyper-V打开否则安装中途会报错而且报错信息相当劝退。装完之后怎么确认成功了打开终端输入docker version如果能看到Client和Server两段信息说明服务已经跑起来了。这里有个新手常犯的错只装了命令行客户端后台服务没启动执行任何docker命令都会报无法连接。如果遇到这个情况先去启动Docker的后台服务再验证。第二个验证命令是docker run hello-world这个命令会拉取一个极小的测试镜像并运行它如果输出了Hello from Docker!那恭喜你环境彻底通了。2.2 更换镜像源先把下载慢这个问题解决我刚装好Docker的时候第一个感觉就是下载慢到怀疑人生。拉一个普通镜像要等半天进度条一动不动。这是因为默认的镜像仓库服务器在海外国内访问确实慢。解决办法是配置镜像加速器。Docker的配置文件位置在Linux上是/etc/docker/daemon.jsonmacOS和Windows上在图形化客户端的设置面板里也能找到。往配置里加一段类似这样的内容{ registry-mirrors: [https://你的加速地址] }这里你的加速地址填什么以你实际能访问到的可用服务为准。填完之后重启Docker生效。配置完成后再拉镜像我测下来基本上能快一个数量级。建议装好Docker第一步就做这件事不然后面每个操作都在等下载热情很快就没了。2.3 安装后的环境检查清单我把自己后来每次装完新机器的检查顺序列一下你照着过一遍就行用docker version确认客户端和服务端都正常。用docker info看一眼容器总数、镜像总数确认配置加载成功。改完镜像源之后用docker pull拉一个常用镜像实测速度。执行一次docker run hello-world把完整的拉镜像→建容器→运行→退出流程走通。这套检查做完你才算真正把Docker装活了。很多人在这一步没确认干净就急着往下学后面遇到问题根本分不清是环境问题还是命令问题。3. 核心三件套镜像、容器、仓库一次讲透3.1 镜像不是一个大文件是分层叠加的只读模板很多人以为镜像是类似压缩包的单一文件其实没那么简单。镜像由一层一层的只读文件系统叠加而成每一层都代表Dockerfile里的一条指令。打个比方像做千层蛋糕每一层都是独立的有奶油层、蛋糕层、水果层。Docker构建的时候如果发现某一层和之前构建过的一模一样就直接复用缓存不用重做。这也是为什么稍微改一下代码重新构建镜像往往很快——大部分底层依赖层都被缓存了真正重新生成的只有最上面几层。查看本地已经下载的镜像docker images字段包括仓库名、标签Tag、镜像ID、创建时间、大小。镜像ID是唯一的但同一个镜像可以有多个不同标签。比如nginx:latest和nginx:1.25可能指向同一份镜像内容只是标签不同。3.2 容器是运行起来的镜像副本容器基于镜像运行。每执行一次docker runDocker就会基于指定镜像创建一个新的可写层这就是容器的草稿纸。镜像本身只读容器里对文件的所有修改都写在这一层草稿纸上。这也就解释了一个困扰新手的问题为什么我在容器里改了文件容器一删改的东西全没了因为草稿纸跟着容器一起被删了。想让数据保留到容器之外就得用后面要讲的数据卷。查看当前正在运行的容器docker ps如果想连已经退出的容器也一起看加-a参数。每个容器有自己的ID和名字名字是Docker随机生成的你也可以在创建时用--name指定一个后面操作起来会方便很多。3.3 仓库和命名规范镜像仓库Registry是存放镜像的云上服务最出名的是Docker官方维护的Docker Hub。docker pull nginx这条命令背后其实是从Docker Hub拉取名为nginx的镜像。完整的镜像名称通常是仓库地址/用户名/镜像名:标签。比如library/nginx:1.25其中library是官方镜像的默认用户名空间平时可以省略。如果你用的是私有仓库前面还要加上地址前缀例如myregistry.example.com/myapp:v1.0。菜鸟阶段你只要知道不写标签默认拉latest而latest的含义是最近更新并不代表稳定版本。实际项目中指定明确版本号比用latest稳得多后文会有细说。4. 从拉取到运行用Nginx完成第一个可访问的容器4.1 拉取镜像之前先想好版本标签学致用最快的办法是立刻跑一个真实的东西。Nginx是个极轻量的Web服务器拉下来就能用而且你能直观看到容器跑起来和能访问到服务之间的关系。拉取命令docker pull nginx:1.25注意我特意指定了1.25这个标签而不是直接docker pull nginx。原因前面提过latest会变今天拉的和三个月后拉的可能不是同一个版本。你照着教程做的时候如果教程写的是latest而你已经提前拉过旧版本后面排查问题很容易被版本差异带偏。4.2 docker run的常用参数逐个拆解运行一个Nginx容器docker run -d --name my-nginx -p 8080:80 nginx:1.25这条命令看起来只有一行里面的参数新手一定要逐个搞明白-d后台运行不占用当前终端。不加这个参数的话日志会刷满屏幕而且一关终端容器就停了。--name my-nginx给容器取个名字后面docker stop my-nginx比docker stop 一串乱码ID好记多了。-p 8080:80端口映射。冒号左边是宿主机的端口右边是容器内部的端口。Nginx默认监听容器内的80端口但宿主机是隔离的所以必须把宿主机的8080端口和容器内的80端口打通。nginx:1.25镜像名加标签。跑起来之后打开浏览器访问http://localhost:8080看到Nginx的欢迎页说明容器已经正常对外提供服务了。这时候再执行docker ps你会在列表里看到这个容器端口那栏显示0.0.0.0:8080-80/tcp这就证明映射生效了。4.3 端口映射、日志查看、进入容器的完整闭环容器在后台跑着你要怎么管理它查看日志是排查问题的第一手段docker logs my-nginxdocker logs后可以加-f参数像Linux的tail -f一样持续刷新输出日志。调试的时候非常有用。而docker logs和直接看文件日志还不一样它抓的是容器内主进程的标准输出。所以如果你的应用打日志是用print或者控制台输出Docker都能帮你收集起来。有时候要进容器内部看看情况比如检查配置文件在不在、目录结构长什么样docker exec -it my-nginx /bin/bash这段要拆开理解exec是在运行中的容器里执行一条新命令-it是让这条命令以交互模式运行后面跟的是要执行的命令。如果容器里没有bash换成/bin/sh通常都能找到。Debian系的镜像默认带bash但很多精简镜像只有sh进去之后命令提示符会变成#而不是$别觉得奇怪。等你想关掉容器docker stop my-nginx再彻底删除docker rm my-nginx到这里你已经把拉镜像→跑容器→看日志→进容器→停容器→删容器这条完整链路走通了。这套操作每天都会用到熟练之后基本就是肌肉记忆。5. 必须学会的数据持久化容器删了数据不能跟着没5.1 为什么容器一删数据就没了我学Docker的转折点是有一次跑了个带数据的服务往里面写了不少内容然后随手docker rm把容器删了重新再建发现数据全没了。那瞬间特别崩溃但也正是这个教训让我彻底理解了容器的本质。前面说过容器销毁时它那个可写层也会被一起丢掉。这设计其实是有意的容器被设计成无状态的这样想停就停、想删就删、想重新创建就重新创建一切都能快速恢复。但现实中的应用几乎都离不开数据——数据库的存储文件、用户上传的图片、应用生成的日志。这些一旦随着容器删除就是重大事故。5.2 数据卷与目录挂载两种方式的取舍Docker提供两种持久化方案。第一种叫数据卷Volume由Docker管理存储位置。创建方式docker volume create my-data运行容器时挂载docker run -d --name my-app -v my-data:/app/data myapp:latest这样容器里的/app/data目录就是Docker管理的持久化存储区容器删了数据卷还在下次新建容器再挂载同一个卷数据就找回来了。第二种叫目录挂载Bind Mount直接把宿主机的某个目录映射进容器docker run -d --name my-app -v /home/me/app-data:/app/data myapp:latest两者最大的区别数据卷是Docker帮你管目录位置跨机器备份、迁移时更规范目录挂载则允许你直接指定一个自己能找到的宿主机路径改代码、看日志时方便。菜鸟阶段我的建议是数据库、消息队列这类需要保存数据的用数据卷开发调试时需要直接改文件看效果的用目录挂载。两者之间的取舍你别纠结太深用多了自然能体会。5.3 一个MySQL容器挂载的完整例子把数据库跑在容器里是Docker最常见的使用场景之一。这里给一个完整的MySQL挂载示例docker run -d \ --name mysql-dev \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEtestdb \ -v mysql-data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.0拆开解释一下两个-e参数它们是用来给容器传递环境变量的MySQL官方镜像会读取这些变量来自动创建数据库、设置初始密码。-v mysql-data:/var/lib/mysql表示把名为mysql-data的数据卷挂载到容器内的数据目录这是MySQL真正存储库文件的地方。这样操作之后哪怕你跑docker rm -f mysql-dev docker run -d --name mysql-dev2 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEtestdb \ -v mysql-data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.0用同一个数据卷换了一个全新的容器数据库里的表和数据全都还在。这个模式在做开发环境重置时特别有用要想恢复干净环境直接删容器建新容器十秒钟搞定。注意MySQL官方镜像首次启动时如果发现数据目录已存在且不为空就不会重新执行初始化脚本。所以用旧数据卷跑新容器时MYSQL_ROOT_PASSWORD这些参数其实是无效的密码以数据卷里存的老密码为准。这个细节容易让人以为密码是改成功了结果怎么连都连不上。6. 自己写Dockerfile把一个Flask小应用变成镜像6.1 Dockerfile的基础指令跑别人的镜像只是第一步真正的门槛是把自己的应用镜像化。用一个小Flask应用演示整个流程。假设你的项目目录my-flask-app下有一个app.pyfrom flask import Flask app Flask(__name__) app.route(/) def hello(): return Hello from Docker!项目里还有个requirements.txt里面写着flask。现在创建一个叫Dockerfile的文件内容如下FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 5000 CMD [python, app.py]一条条来看FROM基础镜像。你的应用是这个地基上盖起来的。WORKDIR设定容器内的当前工作目录。没有这个目录系统会自动创建。COPY requirements.txt .把依赖清单拷进容器。RUN构建阶段执行的命令这里用来装Python依赖。我后面加的镜像源参数是为了在国内网络下装得快点。EXPOSE 5000声明容器内应用监听5000端口。注意这只是声明真正访问时仍然要配合-p做端口映射。CMD [python, app.py]容器启动时要执行的程序。写完之后构建docker build -t my-flask-app:v1 .这个命令会读取当前目录下的Dockerfile逐步执行并把每一步的结果固化成镜像层最后生成名为my-flask-app:v1的镜像。运行docker run -d --name my-flask -p 5000:5000 my-flask-app:v1浏览器打开http://localhost:5000看到Hello from Docker!你的第一个自建镜像就算成功了。6.2 多阶段构建用来控制镜像体积新手刚开始写Dockerfile最容易犯的毛病是——随便拉一个大基础镜像把编译工具链也都装进去构建出来的镜像体积动辄好几个G。虽然没有硬性要求但在内网环境下分发镜像体积直接决定速度。多阶段构建是控制体积的常见手段。核心思路第一阶段负责编译第二阶段只拷贝编译产物最终镜像是干净的。我用一段Go应用举例FROM golang:1.21 AS builder WORKDIR /build COPY . . RUN go build -o myapp FROM debian:bookworm-slim WORKDIR /app COPY --frombuilder /build/myapp . CMD [./myapp]第一阶段把源码编译成二进制第二阶段用一个精简的Debian镜像只拷贝二进制进去运行。这样最终镜像里没有Go编译器、没有源码、没有缓存文件体积能小一大半。6.3 构建时容易遇到的坑缓存、权限、CMD vs ENTRYPOINT构建镜像时Docker会按Dockerfile的每条指令逐层执行并且尽量复用之前构建过的缓存层。这个机制用好了能极大加快构建速度用不好则会让你排查问题时抓狂。最典型的坑COPY . .在这条指令之前没有把文件变化考虑进去。Docker的缓存判断是看这一层输入的文件有没有变化如果你先COPY代码再RUN安装依赖那么只要代码一变后面的RUN也会失效依赖全部重装。正确的习惯是先COPY不常变的依赖清单文件执行RUN安装最后再COPY源码。依赖文件不变时不管源码改多少次安装依赖的这层缓存都能命中。另外两个容易懵的指令是CMD和ENTRYPOINT。简单理解ENTRYPOINT定义容器的主程序CMD是传入主程序的默认参数。你跑docker run myimage --help的时候如果镜像用的是CMD [python, app.py]那--help会直接替换掉整个CMD报错说--help找不到如果用ENTRYPOINT [python]加CMD [app.py]则--help会被追加成python --help运行正常。菜鸟阶段记一条经验启动命令固定写ENTRYPOINT参数可变部分写CMD如果你只写一个CMD要明白别人运行容器时是可以把它整个覆盖掉的。7. Docker Compose一条命令拉起一套完整环境7.1 为什么不用一条条docker run等你开始弄稍微复杂一点的系统比如你的Flask应用需要依赖MySQL和Redis这个时候如果用docker run一条条建容器不仅命令写起来长而且容器之间要联调网络、配置依赖关系很快会变得不可维护。Docker Compose就是干这个的用一个YAML文件把你要启动的所有服务、网络、数据卷一次性定义好然后用一条命令全部拉起来。它本质上是一个多容器的编排工具对于本地开发环境来说体力活全被它包了。7.2 compose文件基础写法在my-flask-app项目里新建docker-compose.ymlversion: 3.8 services: web: build: . ports: - 5000:5000 depends_on: - db - redis environment: - DB_HOSTdb db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: appdb volumes: - db-data:/var/lib/mysql redis: image: redis:7-alpine volumes: db-data:解释几个关键点。version这行现在其实可以不写新的Compose已经自动兼容写下来主要是给老教程对照用不写也不会有问题。services下面定义三个服务web是Flask应用build: .表示用当前目录的Dockerfile构建db和redis直接用它官方镜像。depends_on让web服务先等数据库和缓存起来再启动避免应用一启动连不上数据库直接闪退。最下面的volumes声明了一个命名数据卷db-data供MySQL使用。启动全部服务docker compose up -d-d表示后台运行。查看状态docker compose ps停止并删除所有容器docker compose down这里有个细节docker compose down默认不会删除数据卷所以你的数据库数据还是安全的。想连数据一起清理干净才需要加参数-v。7.3 常用生命周期命令使用Compose之后你会频繁用到这几个命令docker compose config验证配置文件语法是否正确相当于执行前先检查一遍。docker compose logs -f把所有服务的日志合并显示这样能同时看应用和数据库的报错。docker compose exec web bash进入某个服务运行中的容器。docker compose build构建镜像如果服务配置了build才会有实际效果。docker compose up -d --build改完代码后一键重新构建并启动。我自己的开发习惯是在项目根目录放一个docker-compose.yml把一个完整开发环境需要的所有中间件和服务都写进去。新同事入职只需要克隆代码执行docker compose up -d一台跟团队完全一致的环境就跑起来了。这个体验用传统方式配置环境是给不了的。8. 菜鸟避坑清单我当年栽过的跟头你直接绕开8.1 时区问题容器默认是UTC新手很容易踩的一个细节容器里跑出来的程序打印的时间跟本地时间差8个小时甚至更多。原因是很多基础镜像默认时区是UTC也就是格林尼治标准时间。国内应用打印日志、写入数据库的时间戳全都是错的。解决办法有两个方向。一个是在运行容器时设置环境变量docker run -e TZAsia/Shanghai ...在Compose里就是environment: - TZAsia/Shanghai另一个是复制宿主机的时区文件进容器。不过对菜鸟来说环境变量的方式最省事只要基础镜像里有tzdata这个包一般都能生效。8.2 端口冲突和容器重名我见过好几个人卡在同一个报错上port is already allocated。原因很简单你宿主机的8080端口已经被别的容器占用了。解决办法最直观是换一个宿主机端口比如把8080:80改成8081:80。这里要强调的是冒号右边的80是容器内固定端口不要轻易改左边的宿主机端口才随便换。另外查询谁占用了端口用docker ps看端口列就行也能用系统命令查。容器重名也是高频报错提示信息基本是name already in use。解决方法要么docker rm删掉旧容器要么给新容器换一个--name。8.3 进入容器后缺这个缺那个用精简镜像跑容器进去后发现没有vim、没有curl甚至没有bash会有点懵。原因是为了让镜像尽量小官方镜像都只装运行所需的最基本工具。处理思路不是硬装而是换个问法你到底想在里面干什么如果是为了调试网络用docker exec在容器里装一把临时的curl也不是不行但要意识到容器本来就是用完即扔的不必纠结保留现场。如果频繁要进容器检查文件建议在Dockerfile里把常用工具装进去代价是镜像体积变大了一些但对开发环境来说值得。8.4 镜像瘦身的几个土办法我刚开始自己打镜像时体积控制得一塌糊涂。后来总结出几个老土但有效的办法基础镜像选带slim或alpine标签的版本。同样是Pythonpython:3.9-slim比python:3.9小不少。用pip install --no-cache-dir安装依赖避免把pip缓存塞进镜像层。.dockerignore文件非常有用。在项目里创建一个.dockerignore把__pycache__、.git、node_modules这类目录排除掉避免被COPY . .带进镜像。多阶段构建前面已经讲过是终极手段。我见过最夸张的一次一个同事的应用镜像从3个G压到300M就是靠这些土办法组合完成的。别嫌麻烦镜像体积越小拉取越快磁盘占用越少处处受益。Docker这套东西最反直觉的地方就是它看起来概念很多但实际用起来每个概念都对应一个非常具体的动作。你不需要一开始就把所有命令背下来只要先跑通一次构建镜像→运行容器→管理数据的流程自然就记住大半了。我学了这么久最大的体会是别怕删容器别怕反复重建Docker设计出来就是让你折腾的。越折腾你对它怎么工作的理解越深。