第一次接触Docker,十个人里有八个会先被名字搞懵:镜像、容器、仓库、Dockerfile、Compose、Swarm……一堆术语排着队等你认脸。更尴尬的是,不少教程一上来就让你敲命令,容器确实跑起来了,可一问你"镜像和容器到底啥关系",又答不上来。这篇我就不按那个套路走,先花点篇幅把认知框架搭好,再用最少的命令带你亲手跑通两个应用,最后把新手踩得最多的几个坑一次性排掉。内容定位是入门第一篇,完全零基础可以看,断断续续用过Docker但概念模糊的朋友,也可以当一次概念校准。
1. 先搞清楚Docker到底解决了什么问题
1.1 你大概率经历过的"环境地狱"
我在带人的时候,开场必讲一个场景:你在自己电脑上开发了一个网站,本地跑得顺风顺水,代码一交给同事,立刻翻车。这个依赖版本不对,那个系统库缺失,还有人说"我这装的是另一个版本的运行时"。你过去一看,好家伙,光是把开发环境对齐就花了整整一下午。
这个现象有一个专门的名字,叫"环境地狱"或者"依赖地狱"。传统解决方案是虚拟机——装一个完整的操作系统,把所有环境配置好,然后整包分发。虚拟机能解决问题,但代价非常大:镜像文件动辄好几个GB,启动要等好几分钟,对硬盘和内存的占用更是夸张。相当于为了吃一碗泡面,专门买了一整套厨房设备,吃完还得收拾半天。
1.2 Docker的思路:只打包应用和它的随身物品
Docker的思路完全不一样。它不打包整个操作系统,只打包你的应用、应用依赖的运行库、配置文件、环境变量,以及启动时需要执行的命令。这些内容被打进一个标准化的"集装箱"里,在任何装有Docker引擎的机器上,都能以完全一致的方式运行。
打个比方:虚拟机是搬家公司,把你整个家连家具带装修整体搬过去;Docker则是给你一个行李箱,应用需要的所有"随身物品"都装进去,走到哪里,只要有统一的"行李箱支架"就能随时打开用。这个"行李箱支架"就是Docker引擎。因为只打包应用和必要依赖,Docker镜像通常只有几十到几百MB,启动快,资源占用小,分发也方便。
1.3 镜像、容器、仓库:三个必须分清的词
很多新手死磕命令之前,先被这三个词绕晕了。我平时用的对照是这样的:
| 概念 | 通俗类比 | 一句话说明 |
|---|---|---|
| 镜像(Image) | 安装光盘/模板 | 只读的、打包好的应用快照,包含代码、环境和配置 |
| 容器(Container) | 运行中的程序实例 | 镜像运行起来后的实体,可以启动、停止、删除 |
| 仓库(Registry) | 应用商店 | 集中存放镜像的地方,最常用的是Docker Hub |
三者的关系,最标准的理解是:镜像是一个静态模板,容器是模板运行出来的动态实例。一个镜像可以同时启动多个容器,就像一套火锅底料可以煮出好几锅火锅;你改了容器里的文件,不会改动底层的镜像;你删掉容器,镜像还在本地,随时能再启动一个。
提示:镜像本身是分层的、只读的,运行容器时会额外叠加一层可写的容器层。这就是"容器里改了文件,但镜像不受影响"的根本原因。
2. 把核心概念用大白话再拆一遍
2.1 镜像就是"安装包",但它是分层安装的
你可以把镜像理解成一个特殊的安装包,但它不是一次性把文件全塞进去,而是一层一层叠起来的。每个构建步骤生成一层,这一层只记录和上一层的差异。这样设计有两个好处:一是拉取镜像时,如果本地已经有一部分层,只需要下载缺失的层;二是多个镜像可以共用底层,节省磁盘空间。
比如你基于同一个基础镜像构建了十几个应用,这些应用共享同一个底层,磁盘上只会存一份底层数据。这就是为什么Docker镜像能在几百MB的体量内装下完整的应用环境,而传统虚拟机动不动就是几GB。
2.2 容器:就是一个被隔离的进程
千万别把容器想象成"轻量虚拟机",虽然行业里为了科普经常这么说,但严格来讲并不准确。容器的本质是一个运行在宿主机上的进程,只不过这个进程拥有自己独立的文件系统、网络栈、进程空间。它看到的"世界"是镜像打包好的内容,而不是宿主机完整的环境。
正因为容器本质上只是进程,所以它启动极快——不需要引导操作系统的过程。这也是Docker和虚拟机体验上最明显的差异:虚拟机是分钟级启动,容器是秒级、甚至毫秒级启动。
2.3 仓库、镜像名和版本标签
从仓库拉取镜像时,你看到的完整写法通常长这样:
docker pull nginx:1.25-alpine其中nginx是镜像名,1.25-alpine是标签,也就是版本号。镜像名前面还可以加仓库地址,比如docker.io/library/nginx,不过默认情况下Docker会自动补全,所以日常使用中只写镜像名加标签就够了。
新手最容易踩的坑是:不加标签。比如docker pull nginx拉到的其实是最新的官方镜像,但谁的电脑里存的是哪个版本,无法保证。团队协作或生产环境里,不写明确标签等于埋雷。我个人的习惯是,任何镜像都要带标签,哪怕写nginx:latest,也要明确这是你刻意选择的。
2.4 容器和虚拟机的对比
把两者的差异摊开看,会更直观:
| 对比项 | 虚拟机 | 容器 |
|---|---|---|
| 隔离级别 | 硬件级虚拟化 | 操作系统进程级隔离 |
| 资源占用 | 高,每个VM一套完整系统 | 低,共用宿主机内核 |
| 启动速度 | 分钟级 | 秒级 |
| 镜像大小 | 通常数GB | 通常几十MB到几百MB |
| 一致性 | 依赖虚拟机模板的维护 | 镜像即环境,天然一致 |
这里要注意,容器因为共享宿主机内核,所以内核相关的隔离能力比虚拟机弱一些。对绝大多数应用场景来说完全够用,但如果你的需求涉及强隔离或运行不同内核的系统,那还是得考虑虚拟机方案。工具没有绝对的好坏,只有适不适合。
3. 环境搭建与第一个容器
3.1 不同平台的安装方式
安装Docker这件事,不同平台差别很大,我说一下最常见的三种情况。
Linux:服务器上装Docker最简单,用官方脚本一条命令搞定。装完后记得把当前用户加入docker用户组,不然每条命令都要加sudo,新人在这里卡得最多。
Windows:安装Docker Desktop,它会自动帮你配置好底层虚拟化环境。新版默认使用WSL2后端,体验比较顺滑,缺点是比较吃内存,建议电脑至少16GB内存再开。
macOS:同样装Docker Desktop,分Intel芯片和Apple芯片两种安装包,一定要选对型号,选错了装完跑不起来。
3.2 验证安装是否成功
装完以后,打开终端,执行:
docker version能同时看到Client和Server两段版本信息,说明安装成功,Docker守护进程已经正常启动了。如果只看到Client、看不到Server,多半是Docker Desktop没启动,或者服务没起来。
再执行一条:
docker info能看到容器数量、镜像数量、存储驱动等系统信息。这条命令也是排查环境异常的第一道工具。
3.3 跑起你的第一个容器
验证环境没问题后,来跑第一个容器:
docker run hello-world这条命令会经历一个完整流程:本机没有hello-world镜像,于是Docker自动去仓库拉取,然后创建容器、运行程序,最后程序输出一段欢迎信息后退出。别看它简单,它用最短的路径演示了"拉取镜像—创建容器—运行程序"的完整链路。
看到输出后,用docker ps -a查看一下,你会发现刚才的容器状态是Exited。这很正常,hello-world这个镜像的任务就是打印一段信息然后退出,它本来就不是常驻服务。理解这一点很重要:容器的生命周期取决于容器内的主进程,主进程退出,容器随之结束。
3.4 镜像拉取失败的常见原因
第一次拉镜像,很可能遇到超时或者拉不动。常见原因有三个:网络波动、镜像仓库地址访问慢、DNS解析异常。最直接的验证方式是换一个网络环境再试一次。
如果网络本身没问题但就是慢,可以考虑配置镜像加速器。它的原理很简单:Docker拉镜像时,会先去配置好的镜像仓库下载,而不是每次都从默认的海外仓库拉取。配置方式是在Docker的配置文件中添加registry-mirrors字段,然后重启Docker服务。不同平台配置文件路径略有差异,Linux下通常是/etc/docker/daemon.json。
注意:配置镜像加速器只能解决"拉取慢"的问题,不能解决"镜像不存在"或"网络完全不通"的问题。如果换了好几个加速器都拉不动,重点检查宿主机的网络连通性和代理设置。
4. 实操:用Nginx跑通第一个Web服务
4.1 拉取并启动Nginx
光跑hello-world还不够有感觉,我们直接上一个真实场景:用Nginx跑一个Web服务。先把镜像拉到本地:
docker pull nginx:latest然后启动容器:
docker run -d -p 8080:80 --name web-demo nginx:latest命令拆开看:-d表示后台运行,-p 8080:80是端口映射,--name web-demo给容器起个名字,最后是镜像名。执行完以后,浏览器访问http://localhost:8080,就能看到Nginx的默认欢迎页。
第一次看到页面出来的时候,那种"我把一个服务装进了集装箱"的实感会很强烈。这个例子的意义在于:你只用了不到十秒钟,就部署了一个完整的Web服务器。放在以前,你得先装Nginx、配服务、处理端口防火墙,没有十分钟下不来。
4.2 端口映射到底是怎么一回事
很多人第一次看到-p 8080:80会犯迷糊:为什么是8080,为什么又是80?这里要讲清楚一个关键认知:容器内部有一套自己的网络空间,和宿主机是隔离的。Nginx在容器内部监听的是80端口,但你在宿主机上并不能直接访问容器内部的80,需要通过端口映射,把宿主机的某个端口"桥接"到容器的80端口。
写法的规则是"宿主机端口:容器端口"。访问http://localhost:8080时,请求先进到宿主机的8080端口,再由Docker转发给容器的80端口。右侧的80端口基本不用改,它取决于应用实际监听的端口;左侧的8080可以随意选,只要不跟宿主机已有端口冲突。
技巧:本地调试时,左侧端口默认习惯用8080、8081、3000这类好记的端口。但要记住,左侧端口本质上是宿主机资源,同一台机器上不能有两个容器同时占用同一个左侧端口。
4.3 常用容器生命周期命令
容器跑起来后,你马上会用到这些命令。我把它们分组整理:
# 查看运行中的容器 docker ps # 查看所有容器(包括已停止的) docker ps -a # 停止容器 docker stop web-demo # 启动已停止的容器 docker start web-demo # 重启容器 docker restart web-demo # 删除容器(必须先停止) docker rm web-demo # 强制删除运行中的容器 docker rm -f web-demo其中docker rm -f是新手的"后悔药",它直接终止并删除正在运行的容器。但要注意,-f是不可逆的,容器一旦删除,里面产生的数据就没了。后面我会单独强调数据持久化的问题,这里先记住这个风险点。
4.4 进入容器内部看一看
容器是隔离的,怎么查看容器内部的情况?用docker exec:
docker exec -it web-demo /bin/bash这个命令的意思是:在web-demo容器里执行/bin/bash,-it表示交互式终端。进去以后,你会发现自己变成了一个"缩小版"的Linux环境,里面只有Nginx运行所需的文件和工具。可以看看熟悉的目录结构:
ls /usr/share/nginx/html/ cat /etc/nginx/nginx.conf看完用exit退出。这个命令在排查问题时非常重要,因为很多问题需要进容器内部才能定位。
4.5 容器删除后,数据去哪了
再强调一次:容器删除后,容器层里的所有数据都会消失。这包括你写入的文件、安装的软件、产生的日志。这一点和传统服务器差异很大,传统服务器上文件写在硬盘里,删了进程数据还在;容器里,进程和数据是"绑"在一起的。
解决思路是使用数据卷(Volume),把数据挂载到宿主机上,让容器和数据的生命周期解耦。数据卷的具体用法我会在入门第二篇详细展开,这里先建立一个意识:凡是需要长期保存的数据,都不能只放在容器内部。
5. 用Dockerfile构建自己的第一个镜像
5.1 准备一个最简单的前端页面
拉取现成镜像只是第一步,真正体现出Docker威力的是构建自定义镜像。我们做一个最简单的场景:把自己写的静态页面打包成镜像。
先创建一个项目目录,写一个index.html:
mkdir docker-demo && cd docker-demo<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>我的第一个Docker镜像</title> </head> <body> <h1>Hello, Docker!</h1> <p>这个页面跑在容器里。</p> </body> </html>5.2 编写Dockerfile
接下来创建一个文件叫Dockerfile,这就是构建镜像的"配方"。请记住:Dockerfile的文件名必须是Dockerfile,Docker会默认在构建目录里找它。
FROM nginx:latest COPY index.html /usr/share/nginx/html/ EXPOSE 80三行指令逐一说清楚:
FROM:指定基础镜像。你的镜像要基于哪个镜像来构建,这里用的是官方Nginx镜像。COPY:把宿主机当前目录下的index.html复制到镜像内的指定路径。这个路径就是Nginx存放网站文件的默认目录。EXPOSE:声明容器内应用监听的端口。注意,它只起声明作用,真正让外部访问还是要靠运行时的-p映射。
5.3 构建并运行
在docker-demo目录下执行:
docker build -t my-web:v1 .-t my-web:v1给镜像命名并打上版本标签,末尾的.表示使用当前目录作为构建上下文。构建完成后,查看一下镜像:
docker images能看到my-web:v1出现。然后启动它:
docker run -d -p 8081:80 --name my-demo my-web:v1浏览器访问http://localhost:8081,就能看到你自己写的页面了。从写文件到页面上线,整个过程不超过两分钟。这就是容器化部署最直观的体验:环境配置跟着镜像走,代码交付即环境交付。
5.4 常用Dockerfile指令补充
入门阶段,先认识下面这组指令就够用了:
| 指令 | 作用 | 示例 |
|---|---|---|
FROM | 指定基础镜像 | FROM node:18 |
RUN | 在构建时执行命令 | RUN apt-get update && apt-get install -y curl |
COPY | 复制宿主机文件到镜像 | COPY app.py /app/app.py |
ADD | 复制文件,支持解压和远程URL | ADD package.tar.gz /app/ |
WORKDIR | 设置工作目录 | WORKDIR /app |
ENV | 设置环境变量 | ENV APP_PORT=3000 |
CMD | 指定容器启动时执行的命令 | CMD ["python", "app.py"] |
EXPOSE | 声明暴露端口 | EXPOSE 80 |
这里还需要提醒一个细节:RUN和CMD形似但完全不同。RUN是构建镜像时执行的,它的结果会写入镜像层;CMD是容器启动时执行的,它不写入镜像层。搞混这两者,经常导致"构建时明明没问题,一运行容器就秒退"的情况。
6. 新手必经的坑与排查思路
6.1 端口被占用
启动容器时报错,提示driver failed programming external connectivity on endpoint,这就是端口冲突。排查方法很简单,先看已经有谁占用了端口:
docker ps如果宿主机的8081被其他容器占了,换成新端口重新启动即可。如果宿主机的端口被非Docker进程占了,找到那个进程并处理,或者干脆换端口。我个人的习惯是开发环境多试几个高位端口,避开常见的8080、9090。
6.2 拉取镜像失败或超时
报错信息通常是dial tcp ... connect: connection timed out。第一步先确认网络是否通,最简单的办法是执行docker pull时换一个小镜像试试。如果小镜像能拉下来,说明网络没问题,问题出在特定镜像上。
第二步检查是否配置了加速器。加速器配置完记得重启Docker服务,否则不生效。如果配置后仍然拉不动,很可能加速器节点本身不稳定,换一个再试。千万不要一次配置多个加速器,配置项是数组但服务端只会按顺序尝试,配置过多反而会拖慢首轮查找。
6.3 容器启动后立刻退出
这是新人遇到最多的诡异现象:容器一启动就退出了,docker ps里根本看不到,只能用docker ps -a看到状态是Exited。遇到这种情况,第一反应一定要是:查日志。执行:
docker logs container_name日志里会直接告诉你原因。绝大多数情况下,问题出在启动命令上。容器内部没有常驻前台进程,主进程跑完就退出,容器自然就结束了。比如镜像是nginx但你用CMD echo "hello"覆盖了它的默认启动命令,echo输完就退出,容器也就退出了。
记住一个黄金法则:让容器进程保持前台运行,不要用类似nohup ... &的方式把主进程放到后台。容器是为"前台进程"设计的,任何让主进程后台化的操作都会导致容器秒退。
6.4 日志查看与进入容器
日志是排查一切问题的第一现场:
# 查看全部日志 docker logs container_name # 实时跟随日志输出 docker logs -f container_name # 只看最后100行 docker logs --tail 100 container_name如果日志看不出问题,再进容器内部排查:
docker exec -it container_name /bin/bash进容器后先看进程列表ps aux,再看应用配置文件路径是否正确。这套组合拳能解决至少80%的入门阶段问题。
6.5 入门阶段命令速查表
最后给你一张速查表,遇到操作不确定时拿出来看一眼:
| 想做的事 | 命令 |
|---|---|
| 拉取镜像 | docker pull 镜像名:标签 |
| 查看本地镜像 | docker images |
| 运行容器 | docker run -d -p 宿主机端口:容器端口 --name 名字 镜像名:标签 |
| 查看运行中容器 | docker ps |
| 查看所有容器 | docker ps -a |
| 停止容器 | docker stop 名字 |
| 删除容器 | docker rm 名字或docker rm -f 名字 |
| 查看容器日志 | docker logs 名字 |
| 进入容器 | docker exec -it 名字 /bin/bash |
| 查看镜像构建历史 | docker history 镜像名 |
结尾
说点实在的。我带过不少新手,发现入门阶段最大的障碍其实不是命令,而是心智模型。只要想清楚"镜像是一个只读模板,容器是模板的运行实例,仓库是镜像的存储库"这三者关系,后面学什么都是一马平川。这篇教程写的所有操作,我都建议你亲手敲一遍,尤其是构建自定义镜像那部分,哪怕只是改一行标题文字,也会让你对"镜像即环境"这句话有完全不同的体会。
入门第一篇到这里就收尾了。数据卷、网络模式、Compose多容器编排这些内容,会在后面的教程里逐个展开。你先把今天这篇里的命令跑熟,把概念理顺,后续内容上手会快得多。