第2章:Docker 镜像 - 应用的标准化封装
在上一章,我们像逛超市一样,从 Docker Hub(仓库)上拿了一个 hello-world
(镜像),并用它运行了一个容器。我们真正的目标是为自己的应用程序穿上 Docker 这件"标准制服"。
要做到这一点,我们必须学会制作自己的镜像。本章就是你的"裁缝入门指南",教你如何使用 Dockerfile
这张"图纸",为你的应用量体裁衣,打造一个完美的 Docker 镜像。
2.1 深入理解镜像:分层的艺术
在动手之前,我们先来揭秘 Docker 镜像一个极其重要的特性------分层 (Layered) 结构。
想象一下你在用 Photoshop 或 Figma 画画,你的作品是由多个图层叠加而成的:背景层、人物层、文字层... 每一层只包含一部分内容,但叠加在一起就构成了完整的图像。修改时,你通常也只会修改某一个图层,而不是整个画布。
Docker 镜像也是如此!
一个 Docker 镜像并非一个巨大的、单一的文件,而是由一堆只读的层 (read-only layers) 堆叠而成。
- 基础镜像 (Base Image) :任何镜像都始于一个基础层,比如
ubuntu:20.04
或node:16
。这为你提供了一个基本的操作系统环境。 - 指令层 (Instruction Layers) :你在此之上所做的每一个修改------比如安装一个软件、拷贝一段代码、设置一个环境变量------都会创建一个新的图层。
当你基于这个镜像启动一个容器时,Docker 会在所有只读图层的最顶上,再添加一个可写的容器层 (writable container layer)。你在容器内做的所有修改(如创建新文件、修改已有文件),都发生在这个顶层,而不会影响到底下任何一个只读的镜像层。
这种分层结构带来了巨大的好处:
- 高效存储 :如果你的多个镜像都基于同一个
ubuntu
基础镜像,那么在你的电脑上,这个巨大的基础层只需存储一份。 - 快速分发:当你更新应用代码时,你只需要重新构建并推送那一个包含新代码的图层,而不是整个镜像。这使得镜像的上传和下载速度大大加快。
- 版本控制:每一层都记录了一次变更,这使得镜像的版本管理和回滚变得非常容易。
2.2 Dockerfile:给应用安个"家"
我们已经知道了镜像是分层的,那么,我们该如何定义这些层,告诉 Docker 如何一步步地构建出我们想要的镜像呢?
答案就是 Dockerfile。
Dockerfile 是一个纯文本文件,里面包含了一系列指令 (Instructions) 。它就像一份"装修施工单",你按照顺序在里面写下每一条指令,docker
就会严格按照这个顺序,一步步地执行,最终"装修"出一个完整的镜像。
一个最基本的 Dockerfile
通常包含以下几个核心指令:
指令 | 作用 | 解释 |
---|---|---|
FROM |
FROM <image>:<tag> |
指定基础镜像。这是你所有装修工作的起点,必须是第一条指令。 |
WORKDIR |
WORKDIR /path/to/workdir |
设置工作目录 。后续的指令(如 RUN , COPY )都会在这个目录下执行。 |
COPY |
COPY <src> <dest> |
拷贝文件。将主机上的文件或目录,拷贝到镜像的指定路径中。 |
RUN |
RUN <command> |
执行命令 。在镜像构建过程中执行任意 shell 命令,如 RUN npm install 。 |
EXPOSE |
EXPOSE <port> |
声明端口。告诉外界这个容器内的应用会监听哪个端口,但不起实际映射作用。 |
CMD |
CMD ["executable", "param1", "param2"] |
容器启动命令。指定当容器启动时,默认要执行的命令。 |
别被这些指令吓到,它们都非常直观。接下来,我们将通过一个实战项目把它们串起来。
2.3 [实战] 为一个简单的 Node.js Web 应用编写 Dockerfile
是时候动手了!我们将为我们的小册子主线项目------一个极简的 Node.js Express Web 应用------编写它的第一个 Dockerfile。
1. 准备应用代码
首先,在你的工作目录下创建一个名为 my-app
的文件夹。在 my-app
文件夹中,创建两个文件:
app.js
(我们的应用主文件)package.json
(项目描述与依赖文件)
package.json
json
{
"name": "my-app",
"version": "1.0.0",
"description": "A simple Node.js app for our little book.",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js"
},
"dependencies": {
"express": "^4.17.1"
}
}
app.js
javascript
const express = require('express');
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
// App
const app = express();
app.get('/', (req, res) => {
res.send('Hello, Docker World! This is our first image.');
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
这个应用非常简单:使用 Express 框架启动一个 Web 服务器,监听在 8080
端口,当有访问根路径 /
的请求时,返回一句问候。
2. 编写 Dockerfile
在 my-app
文件夹的根目录下,创建一个名为 Dockerfile
(没有后缀名!) 的文件,并写入以下内容:
dockerfile
# 1. 选择一个官方的 Node.js 运行时作为我们的基础镜像
# 我们选择了 16 这个长期支持版本
FROM node:16
# 2. 在镜像中创建一个目录来存放我们的应用代码
# 并将其设置为工作目录
WORKDIR /usr/src/app
# 3. 拷贝 package.json 和 package-lock.json (如果存在)
# 我们将这两个文件分开拷贝,是为了更好地利用 Docker 的缓存机制
COPY package*.json ./
# 4. 安装我们的应用依赖
# 只有当 package.json 文件发生变化时,这一层缓存才会失效
RUN npm install
# 5. 将我们项目目录下的所有文件,都拷贝到工作目录中
COPY . .
# 6. 告诉 Docker,我们的应用在容器内会监听 8080 端口
EXPOSE 8080
# 7. 定义容器启动时要执行的命令
CMD [ "node", "app.js" ]
让我们逐行拆解一下这份"装修施工单":
FROM node:16
: 找一个已经装好了 Node.js 16 的"毛坯房"。WORKDIR /usr/src/app
: 在房子里开辟一个/usr/src/app
的房间作为我们的主战场。COPY package*.json ./
: 先把"依赖清单" (package.json
) 搬进去。RUN npm install
: 按照清单,把所有需要的"家具"(项目依赖)都安装好。COPY . .
: 把我们写的"电器"(应用代码app.js
)搬进去。EXPOSE 8080
: 在墙上贴个标签,写上"这个房间的电话分机号是 8080"。CMD [ "node", "app.js" ]
: 写一张"开机指南",告诉别人启动容器后,要运行node app.js
这个程序。
2.4 docker build
命令详解
有了"图纸" (Dockerfile
) 和"原材料" (应用代码),我们就可以请 docker
这个"施工队"来建造镜像了。
打开你的命令行,确保你当前位于 my-app
文件夹内(也就是 Dockerfile
所在的目录),然后执行以下命令:
bash
docker build -t my-app:1.0 .
docker build
: 这是构建镜像的命令。-t my-app:1.0
:-t
参数用来给我们的镜像打上一个标签 (Tag) ,格式是name:tag
。这就像给我们的镜像起了个名字叫my-app
,版本是1.0
。这是一个非常好的习惯!.
: 最后一个.
表示构建上下文 (Build Context) 。它告诉 Docker:"嘿,你需要的Dockerfile
和所有源代码,都在当前这个目录下。"
执行后,你会看到 Docker 严格按照 Dockerfile
里的指令,一步步地执行,并输出日志。当看到 Successfully tagged my-app:1.0
时,就代表你的第一个专属镜像构建成功了!
你可以运行 docker images
来查看你本地的所有镜像,应该能看到我们刚刚创建的 my-app:1.0
。
2.5 将你的镜像推送到 Docker Hub
现在这个镜像只存在于你自己的电脑上。如果你想分享给同事,或者部署到服务器上,就需要把它推送到一个仓库 (Repository)。我们这里使用官方的 Docker Hub。
-
注册并登录:
- 去 Docker Hub 网站注册一个账号。
- 在你的命令行中,使用
docker login
命令登录。它会提示你输入用户名和密码。
-
给镜像打上新标签 : 为了能推送到你自己的账号下,你需要给镜像重新打一个符合
username/repository:tag
格式的标签。bash# 将 my-app:1.0 复制一份,并命名为 <你的用户名>/my-app:1.0 docker tag my-app:1.0 <your-dockerhub-username>/my-app:1.0
请务必将
<your-dockerhub-username>
替换成你自己的 Docker Hub 用户名。 -
推送镜像:
bashdocker push <your-dockerhub-username>/my-app:1.0
等待推送完成。现在,任何人(包括你自己未来的服务器)都可以通过
docker pull <your-dockerhub-username>/my-app:1.0
来拉取这个镜像了!
2.6 本章小结 & 避坑指南
干得漂亮!你已经从一个镜像的消费者,成功进阶为一名创造者。
-
本章回顾:
- 我们理解了镜像是分层的,这带来了高效和复用。
- 我们学会了使用
Dockerfile
这个"图纸"来定义镜像的构建过程。 - 我们掌握了
FROM
,WORKDIR
,COPY
,RUN
,EXPOSE
,CMD
等核心指令。 - 我们亲手为一个 Node.js 应用构建了镜像,并用
docker build
命令完成了"施工"。 - 我们学会了如何将自己的成果
push
到 Docker Hub 与世界分享。
-
避坑指南:减小镜像体积的几个技巧
-
选择更小的基础镜像 :不要动不动就用
ubuntu
。对于 Node.js 应用,node:16-alpine
这样的alpine
版本会比默认的node:16
小得多。Alpine Linux 是一个极简的 Linux 发行版,非常适合做容器基础镜像。 -
利用好
.dockerignore
文件 :在你的项目根目录下创建一个.dockerignore
文件(语法和.gitignore
一样),把不需要拷贝到镜像里的文件和目录(如node_modules
,.git
,*.log
)都写进去。这能有效防止不必要的文件被打包进镜像,既减小了体积,也避免了敏感信息泄露。 -
合并
RUN
指令 :Dockerfile 中的每一个RUN
指令都会创建一个新的图层。你可以使用&&
将多个命令合并到一条RUN
指令中,以减少图层数量。例如:dockerfile# 不推荐 RUN apt-get update RUN apt-get install -y curl # 推荐 RUN apt-get update && apt-get install -y curl
-
在下一章,我们将学习如何运行我们自己创建的这个镜像,并深入探索 docker run
命令的更多强大功能。