Docker刚出来那两年,很多人觉得它是“运维才需要玩的东西”,搞业务开发的碰都不碰。但只要你试过在一台新电脑上把NodeJS项目跑起来,装完Node、再配环境变量、再发现版本不对、再发现某个原生依赖编译失败……折腾一个下午之后,你会无比渴望有一个东西能把这些破事全部打包隔离。Docker就是干这个的。这篇内容不搞那种很玄的理论轰炸,而是从一个实际需求出发:给你一个NodeJS程序,我们从零开始装Docker、写镜像配置、构建镜像、把程序跑起来。整个过程我尽量按照实际操作的时间顺序来讲,什么地方容易踩坑、什么地方可以偷懒,我都会指出来。这套流程你要是走通了,后面玩转数据库、Redis、Nginx这些容器化部署,基本都是一通百通。1. 动手之前:先别急着敲命令,把三个核心概念搞清楚很多人学Docker失败,不是命令敲得少,而是对着教程复制粘贴了半天,完全不知道自己在干什么。所以哪怕你是急着上手的性子,我也建议你先用五分钟把下面几件事捋明白,这比多敲十遍命令都管用。1.1 镜像和容器:一个像“安装包”,一个像“运行中的程序”这个概念我每次都会用生活里的例子来讲。你把镜像理解成手机里的“安装包”,容器就是“已经安装好并且正在运行的App”。安装包是静态的,躺在那里不能干活;容器是动态的,它跑起来了,占资源,会出日志,可以被停止、删除。所以整个Docker的工作流就是:先准备好一个镜像,然后基于这个镜像去创建并运行容器。我们要做的NodeJS程序,最终跑起来的时候,就是以“容器”的形式存在的。这个理解一旦建立,后面看到docker run就不会懵了——它的意思其实就是“基于某个安装包,把App启动起来”。1.2 镜像的分层结构:为什么Docker能这么省空间镜像还有一个关键特性,它是一层一层堆叠起来的。每个Dockerfile指令都会生成一个新层,Docker会缓存这些层。这也是为什么你在自己电脑上构建过的镜像,再次构建时会非常快;连同一个基础镜像的多个不同项目,也可以共享底层文件,而不是各存一份。这对NodeJS项目直接的影响就是:基础镜像的层是共享的,你只需要把项目代码那一层打进去。所以不要动不动就去下载一个几百MB的所谓“全家桶镜像”,尽量选体积小、适合跑生产的基础镜像,比如node:20-alpine。这也是后面实操里我会优先选的类型。1.3 镜像仓库:Docker世界的“应用商店”镜像不会凭空产生,你第一步需要一个基础镜像,比如NodeJS官方提供的镜像。这些镜像放在一个叫“镜像仓库”的地方,最知名的是Docker Hub,上面放着海量官方和社区维护的镜像。用docker pull命令可以把远程的镜像拉到本地。但对于国内环境来说,直接访问Docker Hub有时候会比较慢,这就涉及镜像加速的配置。这是个非常实际的体验问题,我后面会专门讲一下配置方法,免得你卡在第一步。1.4 用Docker跑NodeJS的完整链路把概念串起来,我们要做的事就非常清晰了:在电脑上安装好Docker环境。准备一个最简单的NodeJS程序(含代码和依赖描述文件)。编写一个Dockerfile,告诉Docker怎么把我们的程序变成镜像。执行docker build构建出项目镜像。执行docker run基于镜像启动容器,让NodeJS服务在容器里跑起来。通过浏览器或命令访问这个服务,验证整个链路通了。后面所有内容都是顺着这六步展开的,你心里有一条线,就不容易在细节里迷路。2. 安装Docker:不同系统上的操作差异安装Docker本身不复杂,但不同操作系统上的安装方式和后续权限管理不太一样,这里分开说。2.1 Linux系统上安装(以Ubuntu为例)Linux下的安装,主流方式是用官方提供的脚本,一条命令搞定:curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh这个脚本会自动检测你的发行版,然后把Docker引擎装好。装完之后,Docker是以root权限运行的,所以每次敲docker命令都需要加sudo。为了方便日常操作,你可以把当前用户加入docker组,之后就不用在每条命令前面敲sudo了:sudo usermod -aG docker $USER newgrp docker注意这一操作有个安全风险:加入docker组后,你的用户权限几乎等同于root,因为Docker本身可以挂载宿主机目录。自家开发机这么用没问题,公司服务器上要慎重。提示:装完之后立刻检查一下,执行docker version,看到Client和Server两段都正常输出,说明引擎在运行。如果只有Client有信息、Server报错,多半是Docker守护进程没起来,Linux下用sudo systemctl enable --now docker启动一下。2.2 macOS和Windows上的安装macOS和Windows推荐直接安装官方桌面版。这个软件包自带Docker引擎、命令行工具以及一个图形化管理界面,对新手非常友好。安装过程基本就是一路下一步,但有一个关键前置条件:Windows上必须开启硬件虚拟化(一般BIOS里默认打开),并且需要确保Windows的“虚拟机平台”功能处于启用状态。很多人装完死活启动不了,回头一看就是这一步漏了。macOS相对省心,只要是近几年的机器,直接装就行。这两个平台的桌面版安装完成后,菜单栏或任务栏会出现一个小鲸鱼图标,点击后看到Engine running就说明环境已经就绪。2.3 安装后的三个验证动作不管哪个平台,装完我都建议做一个“最小化验证”,确认环——境是通的:docker version # 查看客户端和服务端版本 docker info # 查看运行环境详细信息 docker run hello-world # 镜像运行最小化测试第三条命令会拉取一个官方测试镜像,然后打印一段说明文字。如果这段文字能正常打印出来,说明你拉取镜像、创建容器、运行容器整个链路都没有问题。很多人直接跳过这个测试开始搞业务,最后遇到问题了才想起来排查环境,反而浪费时间。我自己的习惯是:装完立刻跑一遍这三条,确认环境的“地基”是稳的,再往上盖楼。3. 准备一个NodeJS服务并编写Dockerfile现在进入正题:假设我们要把一个NodeJS HTTP服务打进Docker里。3.1 先准备一个最简单的NodeJS程序为了演示,我们写一个极简的HTTP服务。新建一个项目文件夹,比如就叫docker-node-demo,在里面创建app.js:const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello from Dockerized NodeJS!'); }); app.listen(port, () => { console.log(`App listening at http://localhost:${port}`); });再创建package.json:{ "name": "docker-node-demo", "version": "1.0.0", "main": "app.js", "scripts": { "start": "node app.js" }, "dependencies": { "express": "^4.18.2" } }本地如果没有装NodeJS也没关系,我们最终的目的是让这个程序在Docker容器里运行。但写Dockerfile之前,我建议你至少知道这段代码是干什么用的——它会监听3000端口并返回一行文本。3.2 Dockerfile逐行拆解在项目文件夹里新建一个名为Dockerfile的文件(注意没有后缀),内容如下:FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npm.taobao.org COPY . . EXPOSE 3000 CMD ["npm", "start"]一行一行来看。FROM node:20-alpine:指定基础镜像。20是Node主版本,alpine表示这是一个基于 Alpine Linux 的极简版本,体积要比默认的node:20小很多,下载更快,跑起来也更省内存。如果项目里有用到比较冷门的原生模块,可能需要换回标准版,但绝大多数项目用alpine都没问题。WORKDIR /app:设置镜像里的工作目录。后面所有相对路径都会基于这个目录。这相当于在容器里创建了一个/app文件夹,并把它作为我们的“项目根目录”。COPY package*.json ./:先把依赖描述文件复制进去。注意这里复制的是package.json和package-lock.json(如果有)。单独把这两个文件先复制进去是有讲究的——Docker的每一层都有缓存,只要这一层的输入没变,构建时就直接用缓存,不会再执行一次。如果我们在代码改动之后仍然能命中依赖层的缓存,构建速度会快非常多。RUN npm install --registry=https://registry.npm.taobao.org:在镜像里安装依赖。我特意加了一个 registry 参数,替换成国内可用的npm镜像源,这样下载依赖的速度会有质的飞跃。你自己操作的时候,也可以把这个镜像源地址换成自己用着顺手的。COPY . .:把整个项目目录里的文件复制进镜像,覆盖到刚才的/app目录下。前面已经单独复制过依赖文件并安装了依赖,这里再把代码复制进去,相当于“代码变、依赖不变”的缓存效果。EXPOSE 3000:声明容器内服务使用的端口。注意它只是“声明”,并不会真的把端口映射到宿主机。真正让外部访问到服务,需要在docker run时用-p参数做端口映射。CMD ["npm", "start"]:设置容器启动时默认执行的命令。这里用了数组格式,它直接执行npm这个可执行程序,参数是start。一个镜像只能有一个CMD,谁最后写谁生效。3.3 .dockerignore:一个很容易被忽略但能救命的文件如果你的项目里恰好装了node_modules,并且没有.dockerignore文件,那么执行COPY . .时会把本地那一堆依赖包一起塞进镜像里。这不仅让镜像体积膨胀,而且在Linux和Windows的环境差异下,本地node_modules里可能包含不适合容器运行的二进制文件,最终结果就是镜像构建成功,但容器运行时报各种奇怪的错。解决方案非常简单。在项目里新建一个.dockerignore文件,内容类似这样:node_modules .git *.log .DS_Store这样COPY . .时就会自动忽略这些目录。无论是镜像体积还是构建速度,都会明显改善。这一步属于“做一次受益终身”的习惯。4. 构建镜像、加载并运行容器到这里,所有准备工作做完了。下面进入真正让Docker“干活”的阶段。4.1 用docker build构建项目镜像在项目文件夹里执行:docker build -t docker-node-demo:v1 .解释一下参数:-t docker-node-demo:v1:给镜像命名并打标签。docker-node-demo是仓库名,v1是标签,通常用来标识版本。命名和标签可以随便起,最好是语义化,比如myproject:20250101。最后的.:指定构建上下文路径。这个.代表当前目录,Docker会把当前目录里没有被.dockerignore忽略的文件打包发送给Docker守护进程,作为构建上下文。第一次构建会拉取NodeJS基础镜像,这时候能明显感觉到网络的影响。如果拉取卡住,大概率是Docker Hub访问慢,建议先配置镜像加速器,然后再试。构建过程中,你会看到一步步执行FROM、WORKDIR、COPY、RUN、CMD的输出。每一步完成,右上角会出现类似[OK]的提示,表示这一层构建成功。构建完成后,用docker images查看本地镜像列表,能看到刚才构建出来的镜像,通常体积在200MB上下。4.2 用docker run加载并运行容器镜像构建好之后,运行容器只需要一条命令:docker run -d -p 8080:3000 --name my-node-app docker-node-demo:v1参数拆开看:-d:后台运行容器,不占用当前终端。-p 8080:3000:端口映射。格式是“宿主机端口:容器内端口”。这里的意思是把容器内的3000端口映射到宿主机的8080端口。--name my-node-app:给容器起一个名字,方便后续管理。最后是镜像名,也就是我们刚构建好的docker-node-demo:v1。执行完后,用docker ps查看运行中的容器。只要看到my-node-app在列表里,说明容器已经正常起来了。然后在浏览器访问http://localhost:8080,就能看到 “Hello from Dockerized NodeJS!” 这行字。注意:容器内部的服务监听的是3000端口,也就是我们代码里app.listen(port)的port,这个值没有变。所以-p 8080:3000的右侧要跟代码里监听的端口保持一致。如果把右侧写成了8080,服务在容器里监听3000,映射就失效了。4.3 查看日志和进入容器排查问题程序如果在容器里崩了,或者根本没起来,第一件要做的事就是看日志:docker logs my-node-app日志里会显示NodeJS进程打出的所有输出,包括我们的App listening at...,以及任何报错信息。如果发现容器没有任何输出,多半是启动命令出了问题,或者npm install阶段就失败了。有时候,日志看不出明显问题,但你怀疑容器内部环境不对。这时候可以进入容器查看现场:docker exec -it my-node-app shexec是在运行中的容器里再执行一个命令,-it表示用交互式终端操作,sh是启动一个交互式shell。进入容器后,你可以像登录一台服务器一样到处看看,检查文件有没有复制进去、依赖有没有装上、环境变量对不对。这个实操习惯在排查问题时会帮你节省大量时间。很多新手只会反复改代码重新构建,却不愿意进入容器里看一眼实际状态,结果绕大圈。5. 常见问题与排查技巧实录下面这些问题,是我实际带人做的时候遇到的高频问题,整理成速查表会直观一些。现象常见原因解决思路构建时拉取基础镜像卡住Docker Hub网络延迟高配置镜像加速器,重试docker build容器启动后立即退出启动命令报错或进程主动退出执行docker logs查看运行日志端口访问不通端口映射写错或服务监听地址错误检查-p格式,代码中监听0.0.0.0而非127.0.0.1npm install 非常慢默认下载源网络慢Dockerfile中指定国内npm镜像源镜像体积异常大项目依赖被误复制进镜像确认.dockerignore是否生效docker: permission denied当前用户不在docker组usermod -aG docker $USER后重新登录构建时能安装依赖,运行时报模块找不到package.json和lock文件不一致删除本地lock后重新build,或确保lock文件被正确复制5.1 端口冲突怎么办如果本地的8080端口已经被其他程序占用,运行docker run时会报地址占用错误。两个解决方向:换一个宿主机端口,比如-p 8081:3000;或者先检查并停掉占用端口的进程。我自己更推荐前者,因为更简单,也避免了误杀其他服务的风险。5.2 镜像构建慢的问题怎么破构建慢主要分两块:基础镜像下载慢和npm依赖安装慢。前者通过配置镜像加速器解决,后者通过在Dockerfile里指定国内的npm镜像源解决。这两个慢的问题如果不处理,新手很容易以为是自己操作错了,反复重装Docker,实际上跟Docker本身无关。5.3 容器起来了但访问不到服务这个问题最隐蔽。很多人按教程把容器跑起来了,docker ps也显示正常,但浏览器访问就是打不开。如果端口映射没有问题,那十有八九是NodeJS服务监听的地址不对。在容器里,代码用app.listen(port)监听,默认是监听所有网络接口的,这没问题;但如果你在代码里显式写了app.listen(port, '127.0.0.1'),那服务只会监听容器内部地址,宿主机代理过来的流量进不来。写NodeJS服务的时候,监听地址不要硬编码成127.0.0.1。5.4 如何自己排查“莫名挂掉”的容器容器如果不断重启,你可以先看一眼重启次数:docker ps -a看到STATUS列有Restarting字样,说明容器确实在崩溃循环里。这时候不要再反复docker run,先跑docker logs把错误信息拿出来。绝大多数崩溃问题,日志里都会直接写清楚原因,剩下的交给排查思路就行。6. 把单个容器进阶成“服务编排”:一点经验之谈如果你把上面的NodeJS服务跑通了,你会发现一个事实:Docker本身并不难,难的是用Docker的时候怎么管理这些容器、网络、存储和依赖关系。一个真实的业务项目,往往不只一个服务:前端、后端、数据库、缓存中间件……它们之间需要相互通信,端口要合理分配,环境变量要集中管理。这时候再用一条条docker run去启动容器,效率太低了,而且特别容易记混参数。我的建议是,把这篇入门内容做完之后,下一站直接学习docker-compose。你在项目里写一个docker-compose.yml,把所有服务的镜像、端口、环境变量、数据卷都声明好,然后一条docker-compose up -d就把整套环境拉起来。你会在配置里用到一个叫“网络”的东西,服务之间用“服务名”互相访问,不需要再记一堆IP地址。我个人在实际操作中的体会是:Docker入门阶段的关键不在于命令背得熟,而在于建立“镜像—容器—仓库”“构建—运行—排查”这条完整链路的心智模型。一旦你脑子里有了这条链路,所有新概念都可以挂到熟悉的位置上——数据卷解决持久化、容器编排解决多服务协同、镜像构建优化解决交付效率。慢慢你会发现,Docker不是给运维专用的工具,而是每一个写代码的人都应该具备的基本功。
Docker刚出来那两年,很多人觉得它是“运维才需要玩的东西”,搞业务开发的碰都不碰。但只要你试过在一台新电脑上把NodeJS项目跑起来,装完Node、再配环境变量、再发现版本不对、再发现某个原生依赖编译失败……折腾一个下午之后,你会无比渴望有一个东西能把这些破事全部打包隔离。Docker就是干这个的。这篇内容不搞那种很玄的理论轰炸,而是从一个实际需求出发:给你一个NodeJS程序,我们从零开始装Docker、写镜像配置、构建镜像、把程序跑起来。整个过程我尽量按照实际操作的时间顺序来讲,什么地方容易踩坑、什么地方可以偷懒,我都会指出来。这套流程你要是走通了,后面玩转数据库、Redis、Nginx这些容器化部署,基本都是一通百通。1. 动手之前:先别急着敲命令,把三个核心概念搞清楚很多人学Docker失败,不是命令敲得少,而是对着教程复制粘贴了半天,完全不知道自己在干什么。所以哪怕你是急着上手的性子,我也建议你先用五分钟把下面几件事捋明白,这比多敲十遍命令都管用。1.1 镜像和容器:一个像“安装包”,一个像“运行中的程序”这个概念我每次都会用生活里的例子来讲。你把镜像理解成手机里的“安装包”,容器就是“已经安装好并且正在运行的App”。安装包是静态的,躺在那里不能干活;容器是动态的,它跑起来了,占资源,会出日志,可以被停止、删除。所以整个Docker的工作流就是:先准备好一个镜像,然后基于这个镜像去创建并运行容器。我们要做的NodeJS程序,最终跑起来的时候,就是以“容器”的形式存在的。这个理解一旦建立,后面看到docker run就不会懵了——它的意思其实就是“基于某个安装包,把App启动起来”。1.2 镜像的分层结构:为什么Docker能这么省空间镜像还有一个关键特性,它是一层一层堆叠起来的。每个Dockerfile指令都会生成一个新层,Docker会缓存这些层。这也是为什么你在自己电脑上构建过的镜像,再次构建时会非常快;连同一个基础镜像的多个不同项目,也可以共享底层文件,而不是各存一份。这对NodeJS项目直接的影响就是:基础镜像的层是共享的,你只需要把项目代码那一层打进去。所以不要动不动就去下载一个几百MB的所谓“全家桶镜像”,尽量选体积小、适合跑生产的基础镜像,比如node:20-alpine。这也是后面实操里我会优先选的类型。1.3 镜像仓库:Docker世界的“应用商店”镜像不会凭空产生,你第一步需要一个基础镜像,比如NodeJS官方提供的镜像。这些镜像放在一个叫“镜像仓库”的地方,最知名的是Docker Hub,上面放着海量官方和社区维护的镜像。用docker pull命令可以把远程的镜像拉到本地。但对于国内环境来说,直接访问Docker Hub有时候会比较慢,这就涉及镜像加速的配置。这是个非常实际的体验问题,我后面会专门讲一下配置方法,免得你卡在第一步。1.4 用Docker跑NodeJS的完整链路把概念串起来,我们要做的事就非常清晰了:在电脑上安装好Docker环境。准备一个最简单的NodeJS程序(含代码和依赖描述文件)。编写一个Dockerfile,告诉Docker怎么把我们的程序变成镜像。执行docker build构建出项目镜像。执行docker run基于镜像启动容器,让NodeJS服务在容器里跑起来。通过浏览器或命令访问这个服务,验证整个链路通了。后面所有内容都是顺着这六步展开的,你心里有一条线,就不容易在细节里迷路。2. 安装Docker:不同系统上的操作差异安装Docker本身不复杂,但不同操作系统上的安装方式和后续权限管理不太一样,这里分开说。2.1 Linux系统上安装(以Ubuntu为例)Linux下的安装,主流方式是用官方提供的脚本,一条命令搞定:curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh这个脚本会自动检测你的发行版,然后把Docker引擎装好。装完之后,Docker是以root权限运行的,所以每次敲docker命令都需要加sudo。为了方便日常操作,你可以把当前用户加入docker组,之后就不用在每条命令前面敲sudo了:sudo usermod -aG docker $USER newgrp docker注意这一操作有个安全风险:加入docker组后,你的用户权限几乎等同于root,因为Docker本身可以挂载宿主机目录。自家开发机这么用没问题,公司服务器上要慎重。提示:装完之后立刻检查一下,执行docker version,看到Client和Server两段都正常输出,说明引擎在运行。如果只有Client有信息、Server报错,多半是Docker守护进程没起来,Linux下用sudo systemctl enable --now docker启动一下。2.2 macOS和Windows上的安装macOS和Windows推荐直接安装官方桌面版。这个软件包自带Docker引擎、命令行工具以及一个图形化管理界面,对新手非常友好。安装过程基本就是一路下一步,但有一个关键前置条件:Windows上必须开启硬件虚拟化(一般BIOS里默认打开),并且需要确保Windows的“虚拟机平台”功能处于启用状态。很多人装完死活启动不了,回头一看就是这一步漏了。macOS相对省心,只要是近几年的机器,直接装就行。这两个平台的桌面版安装完成后,菜单栏或任务栏会出现一个小鲸鱼图标,点击后看到Engine running就说明环境已经就绪。2.3 安装后的三个验证动作不管哪个平台,装完我都建议做一个“最小化验证”,确认环——境是通的:docker version # 查看客户端和服务端版本 docker info # 查看运行环境详细信息 docker run hello-world # 镜像运行最小化测试第三条命令会拉取一个官方测试镜像,然后打印一段说明文字。如果这段文字能正常打印出来,说明你拉取镜像、创建容器、运行容器整个链路都没有问题。很多人直接跳过这个测试开始搞业务,最后遇到问题了才想起来排查环境,反而浪费时间。我自己的习惯是:装完立刻跑一遍这三条,确认环境的“地基”是稳的,再往上盖楼。3. 准备一个NodeJS服务并编写Dockerfile现在进入正题:假设我们要把一个NodeJS HTTP服务打进Docker里。3.1 先准备一个最简单的NodeJS程序为了演示,我们写一个极简的HTTP服务。新建一个项目文件夹,比如就叫docker-node-demo,在里面创建app.js:const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello from Dockerized NodeJS!'); }); app.listen(port, () => { console.log(`App listening at http://localhost:${port}`); });再创建package.json:{ "name": "docker-node-demo", "version": "1.0.0", "main": "app.js", "scripts": { "start": "node app.js" }, "dependencies": { "express": "^4.18.2" } }本地如果没有装NodeJS也没关系,我们最终的目的是让这个程序在Docker容器里运行。但写Dockerfile之前,我建议你至少知道这段代码是干什么用的——它会监听3000端口并返回一行文本。3.2 Dockerfile逐行拆解在项目文件夹里新建一个名为Dockerfile的文件(注意没有后缀),内容如下:FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npm.taobao.org COPY . . EXPOSE 3000 CMD ["npm", "start"]一行一行来看。FROM node:20-alpine:指定基础镜像。20是Node主版本,alpine表示这是一个基于 Alpine Linux 的极简版本,体积要比默认的node:20小很多,下载更快,跑起来也更省内存。如果项目里有用到比较冷门的原生模块,可能需要换回标准版,但绝大多数项目用alpine都没问题。WORKDIR /app:设置镜像里的工作目录。后面所有相对路径都会基于这个目录。这相当于在容器里创建了一个/app文件夹,并把它作为我们的“项目根目录”。COPY package*.json ./:先把依赖描述文件复制进去。注意这里复制的是package.json和package-lock.json(如果有)。单独把这两个文件先复制进去是有讲究的——Docker的每一层都有缓存,只要这一层的输入没变,构建时就直接用缓存,不会再执行一次。如果我们在代码改动之后仍然能命中依赖层的缓存,构建速度会快非常多。RUN npm install --registry=https://registry.npm.taobao.org:在镜像里安装依赖。我特意加了一个 registry 参数,替换成国内可用的npm镜像源,这样下载依赖的速度会有质的飞跃。你自己操作的时候,也可以把这个镜像源地址换成自己用着顺手的。COPY . .:把整个项目目录里的文件复制进镜像,覆盖到刚才的/app目录下。前面已经单独复制过依赖文件并安装了依赖,这里再把代码复制进去,相当于“代码变、依赖不变”的缓存效果。EXPOSE 3000:声明容器内服务使用的端口。注意它只是“声明”,并不会真的把端口映射到宿主机。真正让外部访问到服务,需要在docker run时用-p参数做端口映射。CMD ["npm", "start"]:设置容器启动时默认执行的命令。这里用了数组格式,它直接执行npm这个可执行程序,参数是start。一个镜像只能有一个CMD,谁最后写谁生效。3.3 .dockerignore:一个很容易被忽略但能救命的文件如果你的项目里恰好装了node_modules,并且没有.dockerignore文件,那么执行COPY . .时会把本地那一堆依赖包一起塞进镜像里。这不仅让镜像体积膨胀,而且在Linux和Windows的环境差异下,本地node_modules里可能包含不适合容器运行的二进制文件,最终结果就是镜像构建成功,但容器运行时报各种奇怪的错。解决方案非常简单。在项目里新建一个.dockerignore文件,内容类似这样:node_modules .git *.log .DS_Store这样COPY . .时就会自动忽略这些目录。无论是镜像体积还是构建速度,都会明显改善。这一步属于“做一次受益终身”的习惯。4. 构建镜像、加载并运行容器到这里,所有准备工作做完了。下面进入真正让Docker“干活”的阶段。4.1 用docker build构建项目镜像在项目文件夹里执行:docker build -t docker-node-demo:v1 .解释一下参数:-t docker-node-demo:v1:给镜像命名并打标签。docker-node-demo是仓库名,v1是标签,通常用来标识版本。命名和标签可以随便起,最好是语义化,比如myproject:20250101。最后的.:指定构建上下文路径。这个.代表当前目录,Docker会把当前目录里没有被.dockerignore忽略的文件打包发送给Docker守护进程,作为构建上下文。第一次构建会拉取NodeJS基础镜像,这时候能明显感觉到网络的影响。如果拉取卡住,大概率是Docker Hub访问慢,建议先配置镜像加速器,然后再试。构建过程中,你会看到一步步执行FROM、WORKDIR、COPY、RUN、CMD的输出。每一步完成,右上角会出现类似[OK]的提示,表示这一层构建成功。构建完成后,用docker images查看本地镜像列表,能看到刚才构建出来的镜像,通常体积在200MB上下。4.2 用docker run加载并运行容器镜像构建好之后,运行容器只需要一条命令:docker run -d -p 8080:3000 --name my-node-app docker-node-demo:v1参数拆开看:-d:后台运行容器,不占用当前终端。-p 8080:3000:端口映射。格式是“宿主机端口:容器内端口”。这里的意思是把容器内的3000端口映射到宿主机的8080端口。--name my-node-app:给容器起一个名字,方便后续管理。最后是镜像名,也就是我们刚构建好的docker-node-demo:v1。执行完后,用docker ps查看运行中的容器。只要看到my-node-app在列表里,说明容器已经正常起来了。然后在浏览器访问http://localhost:8080,就能看到 “Hello from Dockerized NodeJS!” 这行字。注意:容器内部的服务监听的是3000端口,也就是我们代码里app.listen(port)的port,这个值没有变。所以-p 8080:3000的右侧要跟代码里监听的端口保持一致。如果把右侧写成了8080,服务在容器里监听3000,映射就失效了。4.3 查看日志和进入容器排查问题程序如果在容器里崩了,或者根本没起来,第一件要做的事就是看日志:docker logs my-node-app日志里会显示NodeJS进程打出的所有输出,包括我们的App listening at...,以及任何报错信息。如果发现容器没有任何输出,多半是启动命令出了问题,或者npm install阶段就失败了。有时候,日志看不出明显问题,但你怀疑容器内部环境不对。这时候可以进入容器查看现场:docker exec -it my-node-app shexec是在运行中的容器里再执行一个命令,-it表示用交互式终端操作,sh是启动一个交互式shell。进入容器后,你可以像登录一台服务器一样到处看看,检查文件有没有复制进去、依赖有没有装上、环境变量对不对。这个实操习惯在排查问题时会帮你节省大量时间。很多新手只会反复改代码重新构建,却不愿意进入容器里看一眼实际状态,结果绕大圈。5. 常见问题与排查技巧实录下面这些问题,是我实际带人做的时候遇到的高频问题,整理成速查表会直观一些。现象常见原因解决思路构建时拉取基础镜像卡住Docker Hub网络延迟高配置镜像加速器,重试docker build容器启动后立即退出启动命令报错或进程主动退出执行docker logs查看运行日志端口访问不通端口映射写错或服务监听地址错误检查-p格式,代码中监听0.0.0.0而非127.0.0.1npm install 非常慢默认下载源网络慢Dockerfile中指定国内npm镜像源镜像体积异常大项目依赖被误复制进镜像确认.dockerignore是否生效docker: permission denied当前用户不在docker组usermod -aG docker $USER后重新登录构建时能安装依赖,运行时报模块找不到package.json和lock文件不一致删除本地lock后重新build,或确保lock文件被正确复制5.1 端口冲突怎么办如果本地的8080端口已经被其他程序占用,运行docker run时会报地址占用错误。两个解决方向:换一个宿主机端口,比如-p 8081:3000;或者先检查并停掉占用端口的进程。我自己更推荐前者,因为更简单,也避免了误杀其他服务的风险。5.2 镜像构建慢的问题怎么破构建慢主要分两块:基础镜像下载慢和npm依赖安装慢。前者通过配置镜像加速器解决,后者通过在Dockerfile里指定国内的npm镜像源解决。这两个慢的问题如果不处理,新手很容易以为是自己操作错了,反复重装Docker,实际上跟Docker本身无关。5.3 容器起来了但访问不到服务这个问题最隐蔽。很多人按教程把容器跑起来了,docker ps也显示正常,但浏览器访问就是打不开。如果端口映射没有问题,那十有八九是NodeJS服务监听的地址不对。在容器里,代码用app.listen(port)监听,默认是监听所有网络接口的,这没问题;但如果你在代码里显式写了app.listen(port, '127.0.0.1'),那服务只会监听容器内部地址,宿主机代理过来的流量进不来。写NodeJS服务的时候,监听地址不要硬编码成127.0.0.1。5.4 如何自己排查“莫名挂掉”的容器容器如果不断重启,你可以先看一眼重启次数:docker ps -a看到STATUS列有Restarting字样,说明容器确实在崩溃循环里。这时候不要再反复docker run,先跑docker logs把错误信息拿出来。绝大多数崩溃问题,日志里都会直接写清楚原因,剩下的交给排查思路就行。6. 把单个容器进阶成“服务编排”:一点经验之谈如果你把上面的NodeJS服务跑通了,你会发现一个事实:Docker本身并不难,难的是用Docker的时候怎么管理这些容器、网络、存储和依赖关系。一个真实的业务项目,往往不只一个服务:前端、后端、数据库、缓存中间件……它们之间需要相互通信,端口要合理分配,环境变量要集中管理。这时候再用一条条docker run去启动容器,效率太低了,而且特别容易记混参数。我的建议是,把这篇入门内容做完之后,下一站直接学习docker-compose。你在项目里写一个docker-compose.yml,把所有服务的镜像、端口、环境变量、数据卷都声明好,然后一条docker-compose up -d就把整套环境拉起来。你会在配置里用到一个叫“网络”的东西,服务之间用“服务名”互相访问,不需要再记一堆IP地址。我个人在实际操作中的体会是:Docker入门阶段的关键不在于命令背得熟,而在于建立“镜像—容器—仓库”“构建—运行—排查”这条完整链路的心智模型。一旦你脑子里有了这条链路,所有新概念都可以挂到熟悉的位置上——数据卷解决持久化、容器编排解决多服务协同、镜像构建优化解决交付效率。慢慢你会发现,Docker不是给运维专用的工具,而是每一个写代码的人都应该具备的基本功。