问题从哪来
先交代一个常见场景。你写好的代码在本地能跑,换一台机器就报错。同一个项目,同事的电脑上是 Node 16 加 npm 8,你的电脑装的是 Node 22,依赖装不上,服务起不来。更麻烦的是,一个完整的应用往往不只有代码:它还要 Node、Redis、MySQL、Nginx,每个组件有各自的版本要求。
这些环境差异不是代码本身的问题,但会真实地挡住部署。Docker 要解决的,就是「我的电脑能跑,你的电脑怎么跑」这一类问题。它把应用和运行环境打包成一个整体,让这个整体在任何装了 Docker 的设备上都能以相同的方式启动。
Docker 的基本思路
Docker 的核心可以浓缩成一句话:
Docker = 应用 + 运行环境
这里的「运行环境」包含操作系统层面的依赖、语言运行时、第三方服务,以及它们之间的版本关系。打包之后得到的单位可以在不同机器之间搬运,启动结果一致。
理解 Docker 需要先分清两个概念:镜像(image)和容器(container)。
可以借用光盘和 DVD 的关系来理解:镜像像一张光盘,是静态、只读的模板,里面装好了应用和它需要的环境;容器像放进播放器的 DVD,是镜像运行起来的实例。一个镜像可以启动多个容器,容器之间相互隔离。
镜像通常不是从零制作,而是从仓库拉取,动作和 git pull 类似。官方和社区维护了大量现成镜像,比如 node、redis、nginx、mysql。需要用哪个,先拉取对应镜像,再启动成容器。
一个具体例子:Nginx 反向代理 Node 服务
概念说完,用一个例子串起「拉镜像、启动容器、端口映射、挂载配置」这一整套动作。
demo 目录下有一个 Node 服务,代码很短:
javascript
const http = require('http')
const server = http.createServer((req, res) => {
res.end('hello world')
})
server.listen(1314, '0.0.0.0', () => {
console.log('server is running at http://0.0.0.0:1314')
})
它监听 1314 端口,收到请求就返回 hello world。
再有一份 Nginx 配置:
ini
events {}
http {
server {
listen 80;
location / {
proxy_pass http://host.docker.internal:1314;
proxy_set_header Host $host;
}
}
}
Nginx 监听 80 端口,把到达的请求转发给 host.docker.internal:1314,也就是宿主机上那个 Node 服务。
这里涉及运维里一个常见概念:反向代理。用户访问 http://localhost:80,浏览器只知道自己在和 80 端口通信,并不知道后端实际跑在 1314 端口。Nginx 站在前端,接收 80 端口的请求,再按配置转发到后端。对用户来说,后端服务的真实端口被藏起来了;对后端来说,多了一层统一入口,可以承接高并发并做分发。
启动这个 Nginx 用一条 docker run:
css
docker run --name my-nginx-demo -p 80:80 \
-v C:\Users\11293\Desktop\workspace\xt_ai\backend\docker\demo\nginx.conf:/etc/nginx/nginx.conf \
-d nginx
这条命令里的参数各管一件事,逐个拆开:
--name my-nginx-demo:给容器起一个名字,方便后续用名字而不是 ID 去操作它。-p 80:80:端口映射,左边是本机的 80 端口,右边是容器内的 80 端口。用户访问本机 80,被转给容器里的 80。-v 本机路径:/etc/nginx/nginx.conf:挂载卷,把本机上的nginx.conf文件映射到容器内的配置路径。这样改本机文件,容器读到的配置就跟着变,不用进容器里去改。-d nginx:以后台方式运行nginx这个镜像。-d是 detached,容器在后台常驻,不占当前终端。
把整条链路串起来看:
rust
用户浏览器 -> localhost:80 -> docker -p 端口映射 -> 容器内 80
-> nginx 读 /etc/nginx/nginx.conf(由 -v 挂载)-> 反向代理 -> 1314 端口 Node 服务
用容器跑 MySQL
同样的思路可以用到数据库上。本机装 MySQL,版本和配置都可能和服务器不一致。改用容器,一条命令就能起一个指定版本的 MySQL:
css
docker run -d --name mysql-demo -p 3307:3306 \
-e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
几个参数的含义和前面对得上:
-p 3307:3306:把本机 3307 映射到容器的 3306。本机可能已经装了 MySQL 占用了 3306,所以换一个对外端口。-e MYSQL_ROOT_PASSWORD=123456:设置环境变量,指定 root 密码。镜像启动脚本会读取这个变量完成初始化。mysql:8.0:指定镜像和版本。不同项目对 MySQL 版本要求不同,这里可以按需选择。
容器起来之后,可以进入它的 Linux 终端:
bash
docker exec -it mysql-demo /bin/bash
mysql -uroot -p123456
docker exec 在运行中的容器里执行命令,-it 打开一个交互式终端,/bin/bash 是进入 shell。进去之后再用 mysql 客户端连库。整个容器就是一个独立的小 Linux 环境,隔离了宿主机上已有的 MySQL。
常用命令
日常操作主要围绕镜像和容器两类:
makefile
docker pull nginx # 拉取镜像
docker run ... # 运行镜像为容器
docker stop $(docker ps -q) # 停止所有运行中的容器
docker rm $(docker ps -aq) # 删除所有容器
docker rmi nginx # 删除镜像
docker ps 列出运行中的容器,加 -a 列出包括已停止的;-q 只输出容器 ID。配合 $() 可以批量清理。镜像和容器是两层,删容器用 rm,删镜像用 rmi,先停再删的顺序不能乱。
收束
Docker 的价值落在两处:一是把「应用 + 运行环境」打包,消除不同机器之间的环境差异;二是把这种打包交给统一的命令(pull、run、stop),让部署动作可重复、可描述。上面的 Nginx 和 MySQL 两个例子,覆盖了端口映射、配置挂载、环境变量、进入容器这几类最常见的使用方式。理解了这些,再去看更复杂的编排工具,就有了一条能对上的基线。