教程
🚀

运维与 DevOps

Docker、Kubernetes、CI/CD 与云原生实践。

Docker 容器入门:把应用装进集装箱

理解容器是什么,并用 Docker 把一个小应用打包、运行起来,掌握最常用的镜像与容器命令。

· 更新于 2026-07-08 阅读量 --

先讲个你一定遇到过的场景:你辛辛苦苦写好了一个网站,在「自己的电脑」上跑得好好的,一点问题没有。结果发给同事,同事一运行就报错:“缺这个库""版本不对""环境变量没配”。你挠着头说:“可我这儿明明能跑啊!”

这就是软件开发里出了名的”在我机器上能跑”难题。问题不在你的代码,而在于运行环境:操作系统版本、装的软件、配置文件、依赖库……只要有一丁点不一样,程序就可能抽风。

Docker 就是来解决这个痛点的。它做的事情很简单也很聪明:把你的”应用代码 + 它需要的运行环境 + 所有依赖”全部打包成一个标准化的”盒子”。这个盒子到了任何装了 Docker 的电脑上,打开就能跑,而且跑出来的结果一模一样。

打个比方:传统方式像寄”散装零件”,对方拿到还得自己组装、自己找螺丝;Docker 像寄一个”打包好的快递箱”,里面东西齐全、说明书明确,对方拆开插电就能用。

学完这一篇,你不需要懂太多底层原理,只要能亲手做到三件事:

  1. 明白容器镜像到底是什么关系。
  2. 写一个最简单的 Dockerfile,把一个小应用装进”集装箱”。
  3. 用最常用的几条命令,把应用构建、运行、查看、停止。

我们慢慢来,一步一步走。

容器是什么

先分清两个容易混的词:镜像和容器

很多教程一上来就把人绕晕,是因为没说清两个词。我们用”做菜”来打比方:

  • 镜像(Image):相当于一份”菜谱 + 食材清单”,是一份静态的、只读的模板。它告诉你这个盒子里面该装什么、怎么启动。它本身不会跑,只是个”蓝图”。
  • 容器(Container):相当于你照着菜谱真正做出来的那”一盘菜”。它是镜像运行起来之后的活实例,是真在占用内存、跑着进程的实体。

一句话记住:镜像是不动的模板,容器是跑起来的实例。 同一个镜像,可以同时跑出好多个容器,就像同一份菜谱可以做出好几盘菜。

容器和虚拟机有啥不一样

你可能听过”虚拟机(VM)“。它们都算”隔离运行”的技术,但差别很大,看这张表就清楚了:

对比项虚拟机(VM)容器(Container)
隔离层级虚拟一整套硬件 + 操作系统只隔离应用进程,共享宿主机内核
启动速度慢,要开机(几十秒到几分钟)快,像开一个程序(秒级)
占用空间大,动辄几个 GB小,常常只有几十 MB
资源开销高,每个 VM 都要独立系统低,多个容器共用内核
比喻在房子里再盖一栋独立房子同一个大通间里用隔板隔出小房间

关键点在于:容器共享宿主机的操作系统内核,所以又轻又快。它不像虚拟机那样”假装自己是一台完整电脑”,而是”在已有的电脑上划出一块独立空间来跑应用”。

Docker 本身又是什么

Docker 是上面这套容器技术最流行的一套”工具箱”。它负责:把镜像造出来(构建)、把容器跑起来(运行)、管理这些盒子的生老病死。我们后面敲的命令,基本都是 docker 开头。

小提示:Docker 需要安装在你的电脑上(Windows / macOS / Linux 都支持)。安装后,命令行里敲 docker --version,能显示版本号就说明装好了。去官网下载 Docker Desktop 按向导装即可。

第一个 Dockerfile

理解了概念,我们动手。我们用一个最简单的 Node.js 小应用当例子——哪怕你没学过 Node.js 也没关系,照着做就行,重点在 Docker 怎么把它”装进箱子”。

先准备一个最小的应用

在任意空文件夹里,新建两个文件。

第一个是 package.json(告诉系统这是个 Node 项目):

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  }
}

第二个是 app.js(一个最朴素的网页服务,访问它会显示一句话):

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('你好,我正运行在 Docker 容器里!\n');
});

server.listen(3000, () => {
  console.log('服务已启动,监听 3000 端口');
});

这个小程序干的事很简单:启动一个网页服务,监听 3000 端口,谁访问就回一句”你好,我正运行在 Docker 容器里!“。

写 Dockerfile

接下来是重头戏——Dockerfile。它就是一个普通的文本文件(没有后缀名),里面是一行行”构建指令”,Docker 照着它一步步把镜像造出来。

在同样的文件夹里新建一个名为 Dockerfile 的文件,内容如下:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

别怕,我们逐行拆开讲,每一行都对应一个动作:

  • FROM node:20-alpine:指定”基础镜像”。意思是”我这个箱子,先以别人已经做好的 node:20-alpine 为底座”。它里面已经装好了 Node.js 20 和一个超精简的 Linux(alpine 只有几 MB)。你不用自己从零装系统,站在巨人肩膀上。
  • WORKDIR /app:设定”工作目录”。后面所有操作都在这个 /app 文件夹里进行,相当于 cd /app
  • COPY package*.json ./:把宿主机上的 package.json 复制进容器的当前目录。先只复制这个文件,是为了下一步能利用”缓存”(下面解释)。
  • RUN npm install:在容器里执行命令,安装依赖。这里我们的小例子其实没外部依赖,但写上它是标准流程,真实项目这一步会装很多包。
  • COPY . .:把宿主机当前目录的其余所有文件(包括 app.js)复制进容器。
  • EXPOSE 3000:声明”这个容器打算用 3000 端口对外提供服务”。注意:它只是个”说明书上的标注”,真正把端口放出来还要靠运行时参数(后面会说)。
  • CMD ["node", "app.js"]:容器启动时默认执行的命令,也就是”跑起我们的应用”。

为什么要把 COPY 分两步

你可能会问:为什么不直接 COPY . . 一次复制完,非要先把 package.json 复制进来、npm install、再复制其余文件?

这是 Docker 的一个聪明之处——层缓存。Docker 会把每一步做成一个”层”,只要这一步的输入没变,下次构建就直接复用缓存,不用重来。

把”改动频繁的代码”和”很少变动的依赖清单”分开:你每次改 app.js 重新构建时,package.json 没变,于是 npm install 那一步直接复用缓存,省下大量时间。小项目感觉不到,大项目能省几分钟甚至更久。记住这个习惯就好。

构建与运行

文件齐了,我们开始真正”造箱子”和”开箱子”。所有命令都在放那几个文件的文件夹里执行。

第一步:构建镜像

docker build -t my-app .

逐段解释这条命令:

  • docker build:告诉 Docker”我要构建一个镜像”。
  • -t my-app-t--tag 的缩写,给镜像起个名字(标签),叫 my-app。不写的话 Docker 会给你一个乱七八糟的名字,不好记。
  • .:最后这个点表示”构建上下文”的目录,也就是当前文件夹。Docker 会把这个文件夹的内容发给”构建引擎”,Dockerfile 里的 COPY 就是从这个目录取文件。

执行后你会看到一行行输出,看到 FINISHEDnaming to ... my-app 就说明镜像造好了。

第二步:查看本地镜像

docker images

你会看到 my-app 已经躺在你的本地镜像仓库里了,包括它的 ID、大小和创建时间。

第三步:运行容器

docker run -p 3000:3000 my-app

逐段解释:

  • docker run:基于镜像启动一个容器。
  • -p 3000:3000:端口映射,格式是 宿主机端口:容器端口。意思是”把我电脑的 3000 端口,接到容器里的 3000 端口”。这样你在浏览器访问自己电脑的 3000,请求就会被转给容器里的应用。
  • my-app:用哪个镜像来跑,就是刚才构建的那个。

运行后终端会打印 服务已启动,监听 3000 端口。这时打开浏览器,访问 http://localhost:3000,页面上就会显示 你好,我正运行在 Docker 容器里!。恭喜!你刚刚把一个应用完整地装进集装箱、并在隔离环境里跑起来了。

让容器在后台跑

上面那种跑法,终端被占住了,按 Ctrl+C 容器就停了。想让它”后台默默跑”,加一个 -d(detach,分离)参数:

docker run -d -p 3000:3000 my-app

执行后会返回一长串容器 ID,然后终端立刻回到可输入状态,容器在后台运行。

给容器起个好记的名字

默认容器名是一串随机词(比如 quirky_turing)。用 --name 自己起名更方便管理:

docker run -d -p 3000:3000 --name my-web my-app

以后停止、查看日志都能直接用 my-web 这个名字,不用去抄那一长串 ID。

常用命令

跑起来之后,日常打交道就是下面这几条命令。我们把它们分成”看""管""清”三类,配着例子讲。

查看类

docker ps            # 查看正在运行的容器
docker ps -a         # 加上 -a,连已停止的容器也列出来
docker images        # 查看本地所有镜像

管理类

docker logs my-web      # 查看容器日志(就是应用打印的那些话)
docker stop my-web      # 停止容器
docker start my-web     # 再次启动已停止的容器
docker restart my-web   # 重启容器

docker logs 非常有用——应用报错了、没反应了,第一件事就是看日志。加上 -f 还能”实时跟踪”新日志,类似 tail -fdocker logs -f my-web

清理类

容器和镜像都会占磁盘,用久了要清理:

docker rm my-web        # 删除一个已停止的容器
docker rmi my-app       # 删除一个本地镜像
docker system prune     # 一键清理:所有停止的容器 + 悬空镜像 + 网络(慎用)

注意:docker rm 只能删”已停止”的容器;想强制删正在跑的,加 -f。删镜像前,要确保没有容器还在用它。

进到容器里面看看

有时候你想进容器内部,像进一间小房间看看里面到底有什么文件。用 exec 开一个终端:

docker exec -it my-web sh
  • -i 保持输入打开,-t 分配一个伪终端,合起来就是”交互式终端”。
  • sh 是容器里自带的轻量 shell(alpine 没有 bash,用 sh)。

进去后你可以 lscat app.js 随便看。看完输入 exit 退出,容器本身不受影响,还在后台跑着。

一个完整的小流程串起来

我们把前面学的串成一条标准工作流,你以后做项目基本就是这套节奏:

  1. 写好应用代码(app.js 等)和 Dockerfile
  2. docker build -t my-app . 构建镜像。
  3. docker run -d -p 3000:3000 --name my-web my-app 后台跑起来。
  4. 浏览器访问 http://localhost:3000 验证效果。
  5. 出问题就 docker logs my-web 看日志排查。
  6. 不用了 docker stop my-web,彻底不要了 docker rm my-web

这套流程走顺了,你就已经掌握了 Docker 的日常 80% 用法。

常见坑位提醒

新手最容易踩的几个坑,提前给你标出来:

  • 端口没映射就访问不了:只写 docker run my-app 而忘了 -p,浏览器永远打不开,因为容器的端口没接到你电脑上。记住 EXPOSE 只是声明,真正放行靠 -p
  • 改了代码没重新构建:你改了 app.js,但没重新 docker build,直接 docker run,跑的还是旧镜像里的旧代码。改代码后一定要重新构建(或考虑挂载卷,进阶内容)。
  • 容器名冲突:第二次 docker run --name my-web 时如果同名容器还在,会报”名字已存在”。先 docker rm 旧的,或换名字。
  • 镜像越积越多:反复构建会产生很多悬空镜像,定期 docker system prune 清理。

这一篇你该记住的

  • 容器是跑起来的实例,镜像是造容器的静态模板,一个镜像可以跑出多个容器。
  • 容器相比虚拟机更轻、更快,因为它共享宿主机内核,只隔离应用进程。
  • Dockerfile 是构建镜像的”菜谱”,FROM 定底座、WORKDIR 定目录、COPY 拷文件、RUN 装依赖、CMD 定启动命令。
  • docker build -t 名字 . 造镜像,docker run -p 主机端口:容器端口 名字 跑容器。
  • 日常四件套:docker ps 看、docker logs 查、docker stop 停、docker rm 删。
  • 改了代码记得重新构建,端口要用 -p 真正映射出来。

当你已经能把单个应用稳稳地装进集装箱并跑起来之后,下一步自然会想:如果我的项目需要同时跑好几个服务(比如网站 + 数据库),一个个手动 docker run 太累了,有没有办法用一份配置文件把它们一起编排起来?这就是我们下一篇要聊的 docker-compose 多容器编排。