Docker 入门,从概念到日常使用

你有没有经历过这种绝望时刻。

在自己电脑上跑得好好的项目,发给同事,对方一跑就报错。「我这边能跑啊」「那你环境不对」「我环境没问题啊」。这种对话,每天都在无数开发群里上演。

装个数据库要配半天,换台电脑又得重来。项目 A 要 Node 18,项目 B 要 Node 20,版本管理切到精神错乱。

说实话,第一次有人跟我说 Docker 的时候,我是抗拒的。又来一个工具要学?我已经装了一堆环境了,npm、pip、conda、nvm,够多了。学不动了,真的。。。

但后来我发现,Docker 跟我想的不一样。它不是「又一个要装的东西」,而是「一个让你不用再装一堆东西的东西」。

坦率的讲,写 Docker 教程这个事情,我也犹豫了一阵。网上 Docker 入门教程一搜一大把,我能写出什么不一样的东西?但后来想通了,教程的价值不在于信息有多全,在于你踩过的坑能不能帮别人少走弯路。所以这篇我会把那些教程里不怎么提的弯路都写进去。

这是 Docker 入门的第一篇,从零开始,把概念、安装、镜像管理、容器操作串成一条线。不讲虚的,每一步都有命令可以照着敲。第二篇会讲怎么自己造镜像和多容器编排,但在那之前,先把基础打稳。

容器到底是什么

Docker 这个名字,不是随便取的。

它来自海运集装箱。英文里 container 这个词,既是集装箱,也是容器。Docker 创始人选这个名字,就是想让软件打包这件事,像海运集装箱一样标准化。

1950 年代之前,海运货物是一件一件搬的。茶叶用木箱,棉花用麻袋,机器用木板钉。每艘船的货舱不一样,每个港口的工人得摸索着装。一艘船在港口能耗好几天,光装卸成本就吃掉利润的一大块。

后来有个叫 Malcolm McLean 的人,搞了个标准化集装箱。不管里面装什么,外面的尺寸统一。船、起重机、卡车、火车,全部围绕这个标准来设计。一艘船在港口的停留时间从几天缩短到几小时,全球贸易直接起飞。

Docker 做的事情一模一样。在你搞 Docker 之前,部署一个应用是这样的,装运行环境、装依赖库、改配置文件、设端口、调权限。每台服务器不一样,每次部署都得摸索着来。「它在我电脑上能跑」这句话,跟「这批茶叶用木箱装没问题的」,何其相似。

Docker 之后,应用打包成一个标准镜像,扔到任何装了 Docker 的机器上,一条命令就能跑起来。不管底下的操作系统是什么,不管宿主机装了什么乱七八糟的东西,容器里跑的环境跟你开发时一模一样。

这是生产力层面的飞跃。当部署的单位从「一堆配置文件 + 一份安装文档」变成「一个标准镜像」,开发者和运维之间的交接成本就降了一个数量级。以前部署是手艺活,每个老师傅有自己的诀窍,Docker 把它变成了流水线上的标准件。

理解了这个背景,三个核心概念就好记了。

镜像,英文叫 image。你可以把它当成一张装好系统的光盘,里面打包了运行环境、代码、依赖库、配置文件。一切跑起来需要的东西都在里面,它是只读的模板。

容器,英文叫 container。镜像是模板,容器是这个模板跑起来的实例。一张光盘可以装到无数台电脑上跑,一个镜像也可以同时跑无数个容器。容器里面的进程觉得自己独占一整个系统,其实只是宿主机上的一个隔离进程。

仓库,英文叫 registry。存镜像的地方,Docker Hub 是最大的公共仓库,类似应用商店。你从上面拉镜像下来用,也可以把自己的镜像推上去分享。

很多人会把容器和虚拟机搞混。长得有点像,但底层完全不同。

容器架构

宿主机操作系统

Docker Engine

容器1: 应用

容器2: 应用

虚拟机架构

宿主机操作系统

Hypervisor (VMware / VirtualBox)

VM1: 完整 Guest OS + 应用

VM2: 完整 Guest OS + 应用

虚拟机每个都带一套完整的操作系统,你跑两个 VM 就有两份内核在跑。重,启动慢,吃资源。一台 16G 内存的机器跑四个虚拟机就开始喘了。

容器不一样,它共享宿主机的内核,只打包应用层和它需要的依赖。轻,秒级启动,一台机器跑几十上百个容器不费劲。你跑十个容器,它们共用一个内核,各自隔离互不干扰。

这个区别是理解后续一切的基础。容器不是更小的虚拟机,是另一种东西。

为什么开发者都该懂 Docker?我自己感受最深的有三个痛点。

第一个,环境一致性。开发环境跑通了,到测试环境又炸了,到生产环境再炸一次。Docker 把环境本身打包成镜像,哪里跑都一样。这个 works on my machine 的千古难题,到这里算是有了正解。

第二个,依赖冲突。项目 A 要 Node 18,项目 B 要 Node 20。你在一台机器上同时装两个版本,nvm 切来切去,迟早精神崩溃。Docker 每个项目跑各自的容器,版本互不干扰,各回各家。

第三个,部署复杂度。以前写一份部署文档,十几步安装步骤,少一步就炸。现在一条 docker pull 加一条 docker run,完事。

这三个痛点,Docker 给的解法都一样,把环境打包成镜像,标准化,到哪都一样跑。

在你电脑上装好 Docker

Docker 的安装分两条线。

Windows 和 Mac 用户,装 Docker Desktop。它是一个桌面应用,安装包下载下来双击装就行。Windows 版会用 WSL2 做后端,你不用特别操心这个,装的时候它会自动处理。Mac 版有 Apple Silicon 和 Intel 两个版本,下载的时候选对就行。

Linux 用户,装 Docker Engine。各个发行版的包管理器都能装,但推荐用 Docker 官方的一键脚本。

# Ubuntu / Debian 一键安装
curl -fsSL https://get.docker.com | sudo sh

装完之后,把当前用户加到 docker 组里,这样不用每次都 sudo。

sudo usermod -aG docker $USER

加完重新登录一下终端生效。

不管你用哪个平台,装完之后跑三条命令验证一下。

docker version
docker info
docker run hello-world

docker version 看客户端和服务端是不是都就绪了。如果你看到 Server 那一段有版本号输出,说明 Docker 引擎在正常跑。如果只看到 Client 没有 Server,多半是 Docker 服务没启动,Windows 上检查一下 Docker Desktop 有没有在后台跑,Linux 上跑一下 sudo systemctl start docker。

docker info 看系统信息,确认引擎的运行状态。

最有意思的是第三条。docker run hello-world,它会干好几件事。先在本地找 hello-world 这个镜像,找不到,就去 Docker Hub 拉,拉下来之后跑一个容器,输出一段欢迎信息,然后容器自动退出。

就这么一条命令,你已经完成了从仓库拉镜像、在本地创建容器、运行容器这三个动作。虽然是最简单的例子,但 Docker 的核心流程你已经跑通了一遍。

镜像管理那些事

hello-world 那条命令跑完之后,你本地已经有了一个镜像。来看看它长什么样。

docker images

输出会列出你本地所有镜像,包括镜像名、tag、ID、创建时间和大小。hello-world 的镜像大概就 13KB,特别小。

镜像管理是你日常最频繁的操作之一,几个核心命令先过一遍。

docker pull nginx:1.25            # 从 Docker Hub 拉镜像,不写 tag 默认 latest
docker images                       # 列出本地所有镜像
docker rmi nginx:1.25              # 删掉本地镜像
docker tag nginx:1.25 mynginx:v1   # 给镜像打个新标签

pull 就是从仓库下载镜像到本地。nginx:1.25 这个写法里,nginx 是镜像名,1.25 是 tag。tag 不写的话默认是 latest,但说真的我不建议你无脑用 latest。你今天 pull 的 latest 是 1.25,明天官方更新了,latest 变成 1.26,你重新 pull 一遍行为就变了。生产环境一定要锁版本,这是个很多人踩过的坑,不锁版本的人迟早会被 latest 背刺 = =

rmi 删镜像。如果这个镜像还有容器在用,删不掉,得先把关联的容器删了才行。这是一个保护机制,防止你误删正在用的镜像。

tag 给镜像打标签。同一个镜像可以有多个 tag,类似 Git 的 tag。打标签是后面自己构建镜像时会频繁用到的操作。

这里有一个很重要的概念,镜像分层。

Layer 1: Debian 基础层 (只读,可共享)

Layer 2: apt install nginx (只读)

Layer 3: COPY 配置文件 (只读)

Layer 4: CMD 启动指令 (只读)

Layer 5: 可写层 (容器运行时产生)

一个镜像不是一整块文件,而是由多个只读层叠起来的。每层对应 Dockerfile 里的一条指令。容器启动的时候,Docker 在最上面加一个可写层,容器里产生的修改写在这个层里。容器删了,可写层也没了。

这个设计有一个很大的好处,共享。你 pull 了 nginx:1.25,它的 Debian 基础层跟 Redis 的 Debian 基础层可能是同一个,本地已经有了就不会重复下载。所以你第一个镜像拉了 150MB,第二个基于同一个基础系统的镜像可能只多了几 MB。

你想想看,这个分层设计其实跟 Git 的 commit 链很像。每条 Dockerfile 指令生成一层,就像每次 commit 产生一个快照。改了上层的指令,只有那一层需要重建,下面的层有缓存可以直接用。这个缓存机制在后面写 Dockerfile 的时候会变得非常关键,第二篇会展开讲。

Docker Hub 上有海量公共镜像。官方镜像比如 nginx、redis、mysql、node、python,都经过了官方维护,可以直接信任。社区镜像质量参差不齐,用之前看一眼下载量和最近更新时间。

镜像名前面带斜杠的,比如 library/nginx、bitnami/redis,斜杠前面是用户名或组织名。不带斜杠的 library/nginx 是 Docker 官方维护的,library 可以省略,直接写 nginx 就行。

容器的完整生命周期

镜像有了,接下来就是跑容器了。这是你日常用得最多的操作。

先看一条完整的 docker run 命令,它几乎涵盖了你日常需要的所有参数。

docker run -d \
  --name myweb \
  -p 8080:80 \
  -v /data/html:/usr/share/nginx/html \
  -e NGINX_PORT=80 \
  nginx:1.25

逐个拆。-d 是后台运行,不加的话容器会在前台跑,你关了终端容器就停了。–name 给容器取个名字,不取的话 Docker 会给你随机生成一个,类似 happy_einstein 这种,很有梗但不好记,排查问题的时候打名字打半天???-p 做端口映射,宿主机 8080 映射到容器 80。-v 挂载数据卷或目录,宿主机 /data/html 映射到容器里的 nginx 静态文件目录。-e 设环境变量。

外部请求

宿主机 :8080

Docker NAT 转发

容器 :80

端口映射这块多说两句。你容器里 nginx 监听的是 80 端口,但这个 80 是容器内部的端口,外部访问不到。-p 8080:80 的意思是,把宿主机的 8080 端口映射到容器的 80 端口。外部请求到宿主机的 8080,Docker 通过 NAT 规则转发到容器的 80。你在浏览器里访问 localhost:8080,实际打的是容器里的 nginx。

如果你只是想容器之间互相访问,不需要外部访问,可以不映射端口,直接用容器内部 IP 通信就行。不过网络这块细节比较多,第二篇会展开讲。

容器的生命周期,其实就几个状态。

docker start

docker stop

docker start

docker rm

docker rm -f

create + start

created (已创建)

running (运行中)

stopped (已停止)

已删除

docker run

docker run 其实做了两步,先 docker create 创建容器,再 docker start 启动。所以 docker run 是一步到位的。容器跑起来之后,你可以 docker stop 停掉它,停掉的容器还可以 docker start 重新启动。确认不需要了,docker rm 删掉。

日常控制命令就这几个,start、stop、restart、rm。stop 是优雅停止,先发 SIGTERM 让容器里的进程自己收尾,10 秒后还没退就 SIGKILL 强杀。如果你想立刻杀掉,用 docker kill,但我不推荐,等于直接拔电源,容器里的数据可能写一半。

rm 删容器。加 -f 可以删运行中的容器,等于先 kill 再 rm。加 -v 会同时删关联的匿名数据卷。用的时候小心一点,确认容器名没错再删,别手滑。

排查问题的时候,有三个命令是你的救命稻草。

docker exec -it myweb bash      # 进入运行中的容器
docker logs -f myweb            # 跟踪容器日志
docker inspect myweb             # 看容器完整配置

exec 可以进入一个正在运行的容器,在里面执行命令。-it 是交互式终端的意思,配合 bash 就是进入容器的 shell。容器里如果没有 bash,可以试 sh。这个东西你进去之后可以像在本地一样操作,看文件、跑命令、改配置,非常方便。

logs 看容器启动以来输出的所有日志。-f 是 follow,持续跟踪新输出,跟 tail -f 一个意思。–tail 100 只看最后 100 行。很多时候你的容器跑不起来或者行为异常,先看 logs 八成能找到原因。

inspect 是核武器级别的排查工具。它会输出这个容器所有的配置信息,IP 地址、挂载了哪些目录、连了哪个网络、设了什么环境变量、启动命令是什么,全在里面。信息量很大,输出是 JSON 格式,你可以配合 jq 来过滤。

还有两个命令也常用。

docker ps          # 看正在运行的容器
docker ps -a       # 看所有容器,包括已停止的
docker stats       # 实时看所有容器的 CPU、内存、网络 IO 占用

ps 不加参数只显示运行中的容器。很多人 docker run 了一个容器,发现 ps 看不到,以为没跑起来,其实可能是容器启动后就退出了,加 -a 就能看到。

这块踩坑提醒几个。

容器删了数据就没了。你在容器里产生的所有文件修改,都写在那个可写层里,容器一删全没了。这个我前面讲分层的时候提过,可写层跟着容器走。数据持久化怎么做,第二篇会讲 Volume 和 Bind Mount。

端口被占用会报 bind: address already in use。意思是宿主机的 8080 端口已经被别的进程占了。换个端口就行,-p 8081:80。

-it 和 -d 不要混用。-it 是交互式前台,-d 是后台。你用 -it 进去调试可以,想后台跑就用 -d。两个一起加在需要交互的命令上,行为会比较奇怪。

这篇就到这

到这里,你已经能做这些事了。从 Docker Hub 拉镜像,用 docker run 跑容器,管理容器的生命周期,进入容器排查问题,看日志看资源占用。

「它在我电脑上能跑」这个老问题,用现有镜像跑服务,算是解决了一半。

另一半是,你自己写的项目怎么打包成镜像。你的 Node 应用、Python 脚本、Go 服务,怎么也变成一个 docker pull + docker run 就能跑起来的东西。怎么管数据、配网络、做多容器编排。

这些是第二篇的内容。

以上,既然看到这里了,如果觉得不错,随手点个赞或收藏吧,如果想第一时间收到推送,也可以关注一下~
谢谢你看我的文章,我们下次再见。

更多推荐