为什么需要容器化?
想象一下这个场景:你第一天到新公司报到,接手了一个五年前用 Vue2 写的老项目。项目文档上写着"请使用 Node 16 + npm 8 运行"。你打开自己的电脑,发现装的是 Node 22 ------ 你尝试直接运行,项目报错;你尝试降级 Node,但电脑上还有其他项目依赖新版本。你陷入了两难。
这就是软件开发中经常遇到的"环境不一致"问题。每个项目都有自己依赖的运行环境:特定的 Node 版本、特定的数据库版本、特定的系统库。如果每次切换项目都要重新配置环境,不仅效率低下,还容易产生各种意想不到的问题。
Docker 就是为了解决这个问题而生的工具。它能把你的应用程序和它所需要的所有依赖环境打包在一起,形成一个独立的"容器"。这个容器可以在任何安装了 Docker 的设备上运行,而且运行结果完全一致。就像海运中使用标准集装箱来运输货物一样,不管里面装的是什么,都可以用同样的方式装载、运输和卸载。
Docker 的核心概念
理解 Docker 需要掌握两个基本概念:镜像(Image) 和 容器(Container) 。
镜像可以理解为一个"模板"或"快照"。它包含了运行某个应用所需要的全部内容:代码、运行环境、系统工具、库文件等。镜像是只读的,你可以在任何地方下载和分享它。
容器则是镜像的一个"运行实例"。当你基于一个镜像启动容器时,Docker 会在镜像之上添加一个可写的文件层,让你可以在容器内进行操作和修改。你可以随时启动、停止、删除容器,而不会影响原始的镜像文件。
打个比方:镜像就像是游戏光盘,里面刻录了完整的游戏内容;容器就像是把光盘放入游戏机运行起来,你可以操作游戏,但光盘本身的内容不会改变。你可以用同一张光盘在多个游戏机上运行出同样的游戏体验。
第一个 Docker 容器:Nginx 反向代理
让我们通过一个实际案例来理解 Docker 的使用。假设你有一个 Node.js 写的 Web 服务,运行在 1314 端口。但用户访问网站时通常使用 80 端口(HTTP 默认端口)或 443 端口(HTTPS 默认端口)。你需要在用户和你的 Node 服务之间加一个"中间人",负责把 80 端口的请求转发给 1314 端口。
这个"中间人"就是反向代理服务器,我们选用 Nginx 来实现。下面一步步来看如何用 Docker 完成这个任务。
第一步:编写 Node.js 服务
首先,我们创建一个最简单的 Node.js HTTP 服务:
javascript
javascript
// index.js
const http = require('http');
const server = http.createServer((req, res) => {
res.end('hello world');
});
server.listen(1314, '0.0.0.0', () => {
console.log('node service run on 1314');
});
这段代码启动了一个 HTTP 服务器,监听 1314 端口。0.0.0.0 表示监听所有网络接口,这样容器内外都能访问到这个服务。
第二步:配置 Nginx 反向代理
接下来,我们需要写一个 Nginx 配置文件,让它监听 80 端口并把请求转发给我们的 Node 服务:
nginx
ini
events {}
http {
server {
listen 80;
location / {
proxy_pass http://host.docker.internal:1314;
proxy_set_header Host $host;
}
}
}
这里有几个关键点需要解释:
listen 80表示 Nginx 监听容器内的 80 端口location /表示匹配所有路径的请求proxy_pass指定了转发目标地址host.docker.internal是一个特殊的域名,在容器内部可以用来访问宿主机(就是运行 Docker 的那台电脑)
为什么要用 host.docker.internal?因为我们的 Node 服务是运行在宿主机上的,而 Nginx 运行在容器内。容器有自己独立的网络空间,不能直接通过 localhost 访问到宿主机上的服务。Docker 提供了 host.docker.internal 这个特殊的 DNS 名称来解决这个问题。
第三步:用 Docker 启动 Nginx
准备工作就绪后,我们通过以下命令启动 Nginx 容器:
bash
bash
docker run \
--name my-nginx-demo \
-p 80:80 \
-v /path/to/nginx.conf:/etc/nginx/nginx.conf \
-d nginx
让我们逐行拆解这个命令做了什么:
docker run:基于一个镜像启动新容器--name my-nginx-demo:给容器起个名字,方便后续管理-p 80:80:端口映射。将宿主机的 80 端口映射到容器的 80 端口。当用户在浏览器访问http://localhost时,实际上访问的是宿主机的 80 端口,然后 Docker 把这个请求转发给容器内的 80 端口-v /path/to/nginx.conf:/etc/nginx/nginx.conf:卷挂载。把宿主机上的 Nginx 配置文件映射到容器内的对应位置。这样你修改配置文件后重启容器就能生效,不需要重新构建镜像-d nginx:-d表示后台运行,nginx是镜像名称。如果本地没有这个镜像,Docker 会自动从 Docker Hub 下载
启动成功后,访问 http://localhost,你会看到浏览器显示 "hello world"。请求流程是这样的:
用户浏览器 → 宿主机 80 端口 → Docker 端口映射 → 容器 80 端口 → Nginx 监听 → 反向代理转发 → 宿主机 1314 端口 → Node 服务响应
这就是一个完整的反向代理流程。反向代理的好处是:用户不需要知道后端服务具体运行在哪个端口,甚至不需要知道后端有多少台服务器在提供服务。
第四步:管理 Docker 容器的常用命令
随着你启动的容器越来越多,学会管理容器变得很重要。这里列出几个常用命令:
查看运行中的容器:
bash
docker ps
查看所有容器(包括已停止的):
bash
css
docker ps -a
停止一个运行中的容器:
bash
arduino
docker stop 容器名或容器ID
删除一个已停止的容器:
bash
bash
docker rm 容器名或容器ID
停止并删除所有容器:
bash
javascript
docker stop $(docker ps -q)
docker rm $(docker ps -aq)
删除一个镜像:
bash
docker rmi 镜像名
MySQL 容器化部署
除了 Web 服务器,数据库也是常见的容器化对象。我们来看如何用 Docker 运行 MySQL:
bash
css
docker run -d \
--name mysql-demo \
-p 3307:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
这个命令和启动 Nginx 类似,但多了几个新参数:
-p 3307:3306:把宿主机的 3307 端口映射到容器的 3306 端口(MySQL 默认端口)。这样做是为了避免和宿主机上已有的 MySQL 服务冲突-e MYSQL_ROOT_PASSWORD=123456:设置环境变量。MySQL 镜像会读取这个变量来初始化 root 用户的密码mysql:8.0:指定了镜像名称和标签(版本号)
容器启动后,你可以通过 localhost:3307 来连接这个 MySQL 服务。
进入容器内部
有时候你需要进入容器内部进行调试或执行命令。比如你想在 MySQL 容器中执行 SQL 命令:
bash
bash
# 进入容器的终端
docker exec -it mysql-demo /bin/bash
# 进入容器后,连接 MySQL
mysql -uroot -p123456
docker exec 命令可以在运行中的容器内执行新命令。-it 表示交互式终端,/bin/bash 是要执行的 shell。进入容器后,你就像登录了一台 Linux 服务器一样,可以使用各种系统命令。
运维视角:Docker 在部署中的价值
从运维的角度来看,Docker 带来了几个显著的好处:
环境一致性:开发、测试、生产环境使用相同的镜像,避免了"在我电脑上能跑"的尴尬。开发人员本地验证通过的镜像,直接推到生产环境使用。
快速部署 :以前部署一个应用需要安装各种依赖、配置环境变量、调整系统参数,可能需要几个小时。现在只需要 docker pull 拉取镜像,docker run 启动容器,几分钟就能完成。
资源隔离:容器之间共享宿主机内核,但拥有独立的文件系统、网络栈和进程空间。一个容器不会影响另一个容器的运行。
版本管理 :每个镜像都带有版本标签,可以随时回滚到旧版本。mysql:8.0 和 mysql:5.7 可以在同一台机器上和平共存。
TypeScript 高级类型:让代码更安全
聊完 Docker,我们来谈谈 TypeScript 中的一些高级类型工具。这些工具在实际项目中非常实用,可以帮助你写出更安全的代码。
什么是工具类型?
工具类型是 TypeScript 内置的一些泛型类型,它们可以对已有类型进行转换,生成新的类型。常见的有 Pick、Omit、Partial、Record、Exclude 等。
Pick:从类型中挑选部分属性
假设你有一个用户信息类型:
typescript
typescript
interface User {
id: number;
name: string;
age: number;
email: string;
}
在用户列表页面,你可能只需要展示 id 和 name;在详情页面,你可能需要全部字段;在修改资料页面,你又不希望暴露 id 和 email。这时候就可以用 Pick 来创建一个只包含特定属性的新类型:
typescript
ini
type UserPreview = Pick<User, 'id' | 'name'>;
const user: UserPreview = {
id: 1,
name: '张三'
};
Pick<T, K> 的含义是:从类型 T 中挑选出键名为 K 的属性,组成一个新的类型。K 必须是 T 的键名之一。
Omit:排除部分属性
与 Pick 相反,Omit 用于排除某些属性:
typescript
ini
type UserSafe = Omit<User, 'email'>;
const safeUser: UserSafe = {
id: 2,
name: '李四',
age: 30
};
这里的 UserSafe 去掉了 email 字段,适合在不需要敏感信息的场景下使用。
Omit 的内部实现很有意思,它等价于:
typescript
r
type MyOmit<T, K> = Pick<T, Exclude<keyof T, K>>;
我们一步步拆解这个过程:
keyof T获取类型 T 的所有键名,得到'id' | 'name' | 'age' | 'email'Exclude<U, K>从联合类型 U 中剔除 K,得到'id' | 'name' | 'age'Pick<T, ...>从 T 中选取剩下的这些键,得到排除 email 后的类型
所以 Omit<User, 'email'> 就是先从 User 的键名中去掉 'email',再挑选剩余的键构建新类型。
Partial:将所有属性变为可选
当你需要更新一个对象的某些字段时,Partial 非常有用:
typescript
ini
type PartialUser = Partial<User>;
// 可以只传部分字段
const patchUser: PartialUser = {
name: '王五',
age: 25
};
// 甚至可以传空对象
const emptyUser: PartialUser = {};
Partial 会把类型中所有属性都变成可选的,这在 API 的 PATCH 请求中很常见------客户端只需要发送要修改的字段即可。
Record:构建键值对类型
Record 用于定义键值对的结构:
typescript
ini
type Dict = Record<string, number>;
const scores: Dict = { math: 90, english: 85 };
Record<K, V> 表示键的类型为 K,值的类型为 V 的对象。这在定义映射关系时非常有用,比如 HTTP 状态码与错误信息的映射:
typescript
typescript
type ErrorMsgMap = Record<number, string>;
const errorMessages: ErrorMsgMap = {
400: '请求参数错误',
401: '未登录,请重新登录',
403: '权限不足',
404: '资源未找到',
500: '服务器内部错误'
};
function getErrorMsg(code: number): string {
return errorMessages[code] ?? '未知错误';
}
Exclude:处理联合类型
Exclude 用于从联合类型中排除某些成员:
typescript
ini
type AllFields = 'id' | 'name' | 'age' | 'email';
type WithoutEmail = Exclude<AllFields, 'email'>;
// 结果:'id' | 'name' | 'age'
注意 Exclude 和 Omit 的区别:Exclude 处理的是联合类型 ,Omit 处理的是对象类型。
更多工具类型
-
ReturnType:获取函数的返回值类型typescript
bashfunction getUser() { return { id: 1, name: '赵六' } } type UserType = ReturnType<typeof getUser>; // 结果:{ id: number; name: string } -
Readonly:将所有属性变为只读 -
Required:将所有可选属性变为必选
总结
本文从实际问题出发,介绍了 Docker 容器化的核心概念和实践操作。我们通过 Nginx 反向代理和 MySQL 部署两个实例,学习了镜像、容器、端口映射、卷挂载等关键概念。容器化技术让应用部署变得更加标准化和可预测,是现代软件开发中不可或缺的工具。
同时,我们也学习了 TypeScript 中的几个高级类型工具。这些工具可以帮助你在开发过程中更好地约束数据类型,提前发现潜在的错误。类型系统越完善,运行时出现类型相关 bug 的可能性就越小。
希望这篇文章能帮你建立对 Docker 和 TypeScript 工具类型的初步认识。动手实践是最好的学习方式,建议你按照本文的步骤在自己的电脑上操作一遍,遇到问题时多查阅官方文档。