从零开始:用Docker容器化你的第一个Node.js应用
1. 引言
在现代软件开发中,Docker已成为不可或缺的工具。它通过容器化技术,将应用及其依赖环境打包在一起,实现了"一次构建,处处运行"的梦想。今天,我将带大家完成一个完整的Docker实践:容器化一个简单的Node.js应用,从编写代码到运行容器,一步步掌握Docker的核心操作。
2. 项目准备
首先,我们创建一个简单的Node.js项目,仅包含两个文件:

2.1. 应用代码 (index.js)
javascript
console.log("你好,世界!")
这是一个最简单的Node.js程序,输出一句问候语。
2.2. Docker镜像定义文件 (Dockerfile)
dockerfile
让我们解析一下这个Dockerfile:
- FROM node:14-alpine:指定基础镜像为Node.js 14的Alpine Linux版本,这是一个轻量级的Linux发行版
- COPY index.js /index.js:将本地的index.js文件复制到容器内的根目录
- CMD node /index.js:设置容器启动时默认执行的命令
3. 构建Docker镜像
有了Dockerfile,我们就可以创建自定义的Docker镜像了。
3.1. 构建命令
powershell
docker build -t hello-docker .
这个命令的含义是:
docker build:启动镜像构建过程-t hello-docker:为镜像打上标签(名称),便于后续引用.:使用当前目录作为构建上下文,Docker会查找当前目录下的Dockerfile
3.2. 构建日志
powershell
PS E:\hello-world\hello-docker> docker build -t hello-docker .
[+] Building 369.5s (7/7) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 103B 0.0s
=> [internal] load metadata for docker.io/library/node:14-alpine 263.8s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 29B 0.0s
=> [1/2] FROM docker.io/library/node:14-alpine@sha256:434215b487a329c9e867202ff89e704d3a75e554822e07f3e0c0f9e606121b33 103.1s
=> => resolve docker.io/library/node:14-alpine@sha256:434215b487a329c9e867202ff89e704d3a75e554822e07f3e0c0f9e606121b33 0.1s
=> => sha256:434215b487a329c9e867202ff89e704d3a75e554822e07f3e0c0f9e606121b33 1.43kB / 1.43kB 0.0s
=> => sha256:4e84c956cd276af9ed14a8b2939a734364c2b0042485e90e1b97175e73dfd548 1.16kB / 1.16kB 0.0s
=> => sha256:0dac3dc27b1ad570e6c3a7f7cd29e88e7130ff0cad31b2ec5a0f222fbe971bdb 6.44kB / 6.44kB 0.0s
=> => sha256:0dac3dc27b1ad570e6c3a7f7cd29e88e7130ff0cad31b2ec5a0f222fbe971bdb 6.44kB / 6.44kB 0.0s
=> => sha256:f56be85fc22e46face30e2c3de3f7fe7c15f8fd7c4e5add29d7f64b87abdaa09 3.37MB / 3.37MB 63.4s
=> => sha256:8f665685b215c7daf9164545f1bbdd74d800af77d0d267db31fe0345c0c8fb8b 37.17MB / 37.17MB 90.6s
=> => sha256:e5fca6c395a62ec277102af9e5283f6edb43b3e4f20f798e3ce7e425be226ba6 2.37MB / 2.37MB 52.7s
=> => sha256:561cb69653d56a9725be56e02128e4e96fb434a8b4b4decf2bdeb479a225feaf 448B / 448B 95.7s
=> => extracting sha256:f56be85fc22e46face30e2c3de3f7fe7c15f8fd7c4e5add29d7f64b87abdaa09 0.6s
=> => extracting sha256:8f665685b215c7daf9164545f1bbdd74d800af77d0d267db31fe0345c0c8fb8b 11.2s
=> => extracting sha256:e5fca6c395a62ec277102af9e5283f6edb43b3e4f20f798e3ce7e425be226ba6 0.3s
=> => extracting sha256:561cb69653d56a9725be56e02128e4e96fb434a8b4b4decf2bdeb479a225feaf 0.0s
=> [2/2] COPY index.js /index.js 2.0s
=> exporting to image 0.2s
=> => exporting layers 0.1s
=> => writing image sha256:be987f77136525684c13002b3a8d43cc6cd4d62986f7997128b51946aad6d03c 0.0s
=> => naming to docker.io/library/hello-docker 0.0s
View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/pnwg2u6n5xocvatlj87a6v6fj
1 warning found (use docker --debug to expand):
- JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 3)

3.3. 构建过程解析
- Docker首先下载基础镜像
node:14-alpine(如果本地不存在) - 然后执行COPY指令,将index.js文件复制到镜像中
- 最后设置默认的启动命令
- 构建完成后,生成了一个ID为
be987f771365的新镜像
这里Docker给出了一个有用的警告:建议使用JSON格式的CMD指令来避免信号处理问题。虽然不影响当前使用,但在生产环境中需要注意这一点。
4. 查看本地镜像
构建完成后,我们可以查看本地已有的Docker镜像:
powershell
docker images
日志输出:
powershell
PS E:\hello-world\hello-docker> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-docker latest be987f771365 8 minutes ago 119MB
hello-world latest 74cc54e27dc4 10 months ago 10.1kB
hub.oepkgs.net/openeuler/nginx <none> 1d992e662bfc 13 months ago 747MB

输出显示我们刚刚创建的hello-docker镜像大小为119MB,相比基础镜像已经很小了。同时可以看到系统中还有其他镜像,比如经典的hello-world测试镜像。
5. 运行容器
镜像只是静态的模板,要运行应用需要从镜像创建容器:
powershell
docker run hello-docker
日志输出:
powershell
PS E:\hello-world\hello-docker> docker run hello-docker
你好,世界!

执行后,终端立即输出了"你好,世界!"。这意味着:
- Docker从
hello-docker镜像创建了一个新容器 - 容器内执行了
node /index.js命令 - Node.js解释器执行了我们的代码,输出结果到终端
- 程序执行完毕,容器自动停止
6. 查看容器
powershell
PS E:\hello-world\hello-docker> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
PS E:\hello-world\hello-docker>
powershell
PS E:\hello-world\hello-docker> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9fadd3ad3798 hello-docker "docker-entrypoint.s..." 54 seconds ago Exited (0) 52 seconds ago affectionate_burnell

运行docker ps查看当前正在运行的容器,发现没有结果。这是因为我们的应用执行完就退出了,容器也随之停止。
但使用docker ps -a查看所有容器(包括已停止的),可以看到刚刚运行的容器:
- 状态为"Exited (0)",表示正常退出
- 54秒前创建,52秒前退出
- 有一个随机生成的名字"affectionate_burnell"
这就是容器与虚拟机的关键区别之一:容器是为了运行特定进程而存在的,当进程结束,容器的使命也就完成了。
7. 实践总结
通过这个简单的实践,我们完成了Docker的核心工作流:
- 编写应用代码 → 2. 定义Dockerfile → 3. 构建镜像 → 4. 运行容器
在这个过程中,我们学到了几个关键点:
- 镜像与容器的关系:镜像是静态的模板,容器是镜像的运行实例
- 分层构建:Docker镜像采用分层结构,每一条指令都会创建一个新的层
- 轻量级特性:容器随内部进程的结束而停止,资源占用更高效
- 环境一致性:无论在哪里运行,容器内部环境都完全相同