恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Node.js应用Docker容器化部署实战:从Dockerfile到Compose全攻略
首页
资讯中心
/
Node.js应用Docker容器化部署实战:从Dockerfile到Compose全攻略
Node.js应用Docker容器化部署实战:从Dockerfile到Compose全攻略
发布时间:2026/10/8 2:26:04
你有没有遇到过这种场景本地npm run dev跑得正欢代码一推到服务器上就各种起不来——端口被占、Node 版本对不上、环境变量丢了、依赖装到一半网络抽风。我前两年部署 Node.js 应用时就被这些破事磨到没脾气直到把所有服务迁到 Docker 容器化部署才算真正把本地能跑和线上能跑这两件事统一起来。这篇文章围绕 Node.js 应用如何用 Docker 一步步完成容器化部署把环境准备、Dockerfile 编写、多容器编排以及我实际踩过的坑完整过一遍。适合刚接触容器化的 Node.js 开发者也适合正在给团队整理部署规范的读者。先交代一下我的场景方便你对照。我用的是一个 Express 写的 API 服务业务依赖 MySQL 和 Redis本地开发环境是 Windows Docker Desktop线上是一台 2 核 4G 的 Ubuntu 20.04 云服务器。下面的操作和踩坑记录基本都来自这套环境但思路是通用的换成别的 Node.js 框架或者别的 Linux 发行版也一样能套。1. 直接在服务器上跑 Node.js 的痛点和容器化之后的样子1.1 裸机部署为什么总在最后一公里翻车很多 Node.js 项目一开始的部署方式都很原始服务器装好 Node把代码 git clone 下来npm install一把梭然后node app.js后台跑。这套流程在小项目里确实能用但一旦项目复杂起来问题就全冒出来了。首先是 Node 版本不一致。我本地用的 Node 18服务器上是 16代码里用了某个新 API本地测试全过上线就 500。你总不能要求每个环境都手动对齐版本更别说团队里还有人用 Windows 有人用 macOS。其次是系统依赖的坑有些 npm 包需要编译原生模块比如bcrypt、sharp这类服务器上缺python、缺build-essential、缺各种.so库npm install直接报错。还有进程守护的问题裸机上node app.js一旦崩了没人管要么靠pm2这类工具还得额外配置开机自启。最后是回滚困难出了问题得重新拉代码、重新装依赖一次发布就是半小时起步。这些还不是最恶心的最恶心的是数据库。MySQL、Redis 装在服务器上版本和本地不一样配置也不一样连字符集、时区这种小问题都能让你排查一晚上。1.2 Docker 介入后整套部署逻辑变成了什么样Docker 做的事情说白了就是把应用连同它的运行环境一起打包。Node 版本、npm 依赖、系统库、配置文件、启动命令全部写进一个镜像里镜像跑到哪应用的行为就是一样的。本地测过的镜像线上拉下来跑结果不会有两样。容器化之后我的部署流程变成了这样本地写好Dockerfile和docker-compose.ymldocker compose up一键启动整套环境应用 MySQL Redis测试没问题后把镜像推到镜像仓库服务器上docker compose pull docker compose up -d回滚就是docker compose down docker compose up -d换一个之前的镜像标签。这个流程的好处不仅仅是省事更关键的是它让部署变成了一件可预期、可复制、可回滚的事。不再依赖某个人在服务器上手动敲命令、手工改配置环境问题在镜像构建阶段就暴露了而不是等上线之后才炸。我个人的体会是容器化最大的价值不在技术本身而在于它把部署这件事从玄学变成了流程。1.3 你要准备的目录结构和基础依赖开始写 Dockerfile 之前先把项目结构理清楚。下面是我常用的目录组织方式my-node-app/ ├── src/ # 源码 ├── package.json ├── package-lock.json ├── .dockerignore # 构建镜像时忽略的文件 ├── Dockerfile ├── docker-compose.yml # 多容器编排 └── .env # 环境变量不要提交到 git基础依赖就是 Node.js 20 LTS 和 Docker。Node 版本我建议直接上 20虽然是奇数版本号但 LTS 支持周期长而且 18 到 20 的 API 变化不大迁移成本很低。Docker 的话本地开发用 Docker Desktop 最省心服务器上装 Docker Engine 就行具体安装方法下一节细说。2. 环境准备Ubuntu 上装 Node.js 20 和 Docker 引擎以及绕不开的权限坑2.1 安装 Node.js 20 LTS 的两种方式服务器上装 Node.js我推荐用 NodeSource 的二进制仓库而不是 apt 自带的版本。Ubuntu 20.04 自带的 Node 是 10.x老得没法用Ubuntu 22.04 自带的是 12.x也偏旧。直接apt install nodejs装出来的版本不够新某些依赖会报 engine 不兼容。用 NodeSource 装 Node 20 的完整命令如下# 下载并执行 NodeSource 的安装脚本 curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - # 安装 Node.js sudo apt-get install -y nodejs # 验证版本 node -v # v20.x.x npm -v # 10.x.x这里有个细节sudo -E的意思是保留你当前的环境变量NodeSource 脚本需要读取一些代理或镜像配置如果不加-E在部分网络环境下可能下载失败。另外装完之后你可能还想装yarn或pnpm那就corepack enableNode 20 自带了 Corepack不需要额外装。2.2 安装 Docker Engine 与启动校验Ubuntu 上装 Docker Engine 的官方步骤是先卸掉可能存在的旧版本然后添加 Docker 官方 apt 源再安装。完整命令如下# 卸载旧版本如果有 sudo apt-get remove docker docker-engine docker.io containerd runc # 安装依赖 sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg # 添加 Docker 的 GPG 密钥和 apt 源 sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null # 安装 Docker Engine sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 启动并设置开机自启 sudo systemctl enable docker sudo systemctl start docker # 验证 sudo docker run hello-world装完之后有个非常常见的坑直接用docker ps会报permission denied while trying to connect to the Docker daemon socket。这是因为 Docker 的 socket 文件/var/run/docker.sock默认只允许 root 用户和docker用户组的成员访问而你在安装后还是普通用户。2.3 permission denied 错误是怎么来的怎么彻底解决这个错误几乎每个新手都会遇到报错信息长这样docker: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/create: dial unix /var/run/docker.sock: connect: permission denied.根因就是当前用户不在docker用户组里。解决方式很简单把用户加进 docker 组# 创建 docker 用户组通常已存在 sudo groupadd docker # 把当前用户加入 docker 组 sudo usermod -aG docker $USER # 重新登录或执行下面的命令让组生效 newgrp docker # 验证 docker ps注意上面三条命令的顺序不能反先groupadd如果组存在会报错但没关系再usermod -aG。我之前犯过一个错在虚拟机的共享文件夹里跑 Docker宿主机权限映射混乱导致同样的报错这个跟用户组无关得检查目录挂载权限。另外如果是公司的 CentOS 服务器Docker 版本比较老可能没有docker compose子命令只有docker-compose在usermod之后可以用docker-compose version验证。3. Dockerfile 的写法与构建细节从能跑到跑得稳3.1 为什么推荐多阶段构建很多人写 Dockerfile 就是最简单的单阶段写法FROM node:20 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . CMD [node, app.js]这个写法在小项目里没问题但生产环境不够用。单阶段构建最大的问题是镜像体积大基础镜像里包含了完整的系统工具链、npm 缓存、甚至源码一个 Node 项目的镜像动不动就 800MB 起步。多阶段构建可以把编译/安装依赖和最终运行分开最终镜像只保留运行时需要的东西。我的 Node.js 项目 Dockerfile 长期长这样# 第一阶段安装依赖 构建 FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --omitdev # 第二阶段精简运行时 FROM node:20-alpine ENV NODE_ENVproduction WORKDIR /app COPY --frombuilder /app/node_modules ./node_modules COPY . . # 非 root 用户运行 RUN addgroup -S appgroup adduser -S appuser -G appgroup USER appuser EXPOSE 3000 CMD [node, app.js]这里面有几个关键点用npm ci而不是npm install。npm ci会严格按照package-lock.json安装速度快且可复现而npm install可能会因为 semver 范围拉取到更新的依赖版本这在生产环境是隐患。先复制package*.json再复制源码是为了利用 Docker 的缓存机制。只要依赖文件没变npm ci那层就能命中缓存构建速度提升非常明显。多阶段构建最终的镜像只包含node_modules和代码体积通常能控制在 200MB 以内node:20-alpine基础镜像本身只有几十 MB。用adduser创建非 root 用户运行 Node 进程这是安全基线。虽然容器有隔离性但万一有 RCE 漏洞root 权限的危害会大得多。3.2 时区、健康检查、环境变量这些细节别漏了如果你部署的应用有日志时间错乱问题十有八九是容器时区没设置。Debian 系基础镜像默认 UTC 时间而国内服务器大部分是 CSTUTC8。我的做法是在 Dockerfile 里显式设置时区环境变量ENV TZAsia/Shanghai在 alpine 镜像上还需要安装 tzdata 包否则上面这行不生效。更稳妥的做法是直接用ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtimealpine 里同样先装tzdata。健康检查我也建议加上。很多人以为 MySQL 是健康的Node 进程起来了就万事大吉其实容器启动成功 ≠ 服务可用。可以在应用的启动入口实现一个/health接口返回{ status: ok }然后在 Dockerfile 里声明HEALTHCHECK --interval30s --timeout5s --start-period10s --retries3 \ CMD wget --no-verbose --tries1 --spider http://localhost:3000/health || exit 1--start-period很关键它告诉 Docker 在容器启动后前 10 秒不要算入健康检查失败次数因为 Node 应用启动也需要时间不然很容易出现容器起来了但应用还没就绪导致的误判。环境变量方面不要把密钥写死进 Dockerfile 或镜像里。正确做法是运行时通过-e参数或docker-compose的environment字段传入敏感信息用env_file或者密钥管理服务。镜像里写死数据库密码等于把密码发给所有能拉取镜像的人。3.3 构建与启动的常用命令Dockerfile 写好后构建和运行的命令如下# 构建镜像注意最后的点不能丢 docker build -t my-node-app:1.0.0 . # 运行容器映射宿主端口 3000 到容器端口 3000 docker run -d --name my-node-app -p 3000:3000 --env-file .env my-node-app:1.0.0 # 查看日志 docker logs -f my-node-app # 进入容器排查 docker exec -it my-node-app sh实际操作中有几个教训我先说为敬docker build的上下文默认是当前目录如果你忘了写.dockerignore会把node_modules、.git、dist等目录全部打成上下文构建时上传几百 MB 文件速度极慢。我的.dockerignore内容很简单node_modules dist .git .gitignore *.log .DS_Store .env构建时如果项目有npm run build的步骤记得把构建产物也复制到最终阶段。我之前犯过最蠢的错误就是在 builder 阶段npm run build但最终阶段只复制了node_modules和源码没复制dist容器跑起来直接 404。docker run的--restart always参数建议加上这样容器异常退出后 Docker 会自动拉起比手写 pm2 的守护逻辑方便得多。4. 容器编排与联通MySQL、Redis 怎么接多容器怎么管4.1 用 docker-compose 把 Node.js 和 MySQL 串起来项目一旦依赖数据库单容器跑 Node 就不够了。这时候用docker-compose做多容器编排最合适它能把应用、MySQL、Redis 声明在同一个文件里一条命令全部起。我的docker-compose.yml大概长这样services: app: build: . ports: - 3000:3000 environment: - NODE_ENVproduction - DB_HOSTmysql - DB_PORT3306 - DB_USERappuser - DB_PASSWORDapppass - DB_NAMEappdb - REDIS_HOSTredis - REDIS_PORT6379 depends_on: mysql: condition: service_healthy mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORDrootpass - MYSQL_DATABASEappdb - MYSQL_USERappuser - MYSQL_PASSWORDapppass ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redis_data:/data volumes: mysql_data: redis_data:这里有几个重要的点depends_on不再是简单的启动顺序控制。新版本 compose 支持condition: service_healthy意思是只有 MySQL 健康检查通过之后才启动 app避免应用起来时数据库还没就绪导致连接失败重试。MySQL 数据要挂载到命名卷mysql_data否则容器一删数据就全没了。Redis 同理挂载redis_data。DB_HOSTmysql这种写法是在 compose 网络里直接用服务名访问而不是127.0.0.1。这一点很多人会搞混后面我单独展开说。4.2 容器内访问数据库的坑localhost 不是你以为的 localhost这是整个容器化部署里最容易踩的坑没有之一。本地开发的时候Node 连数据库用的是localhost:3306到了容器里你还这么写就会发现连接失败。原因在于compose 会为每个服务创建一个独立的容器每个容器有自己独立的网络命名空间。localhost指向的是容器自身而不是宿主机更不是 MySQL 容器。要想在 app 容器里访问 MySQL 容器必须用 compose 网络里的服务名mysql作为主机名。所以应用代码里的数据库配置必须支持通过环境变量注入const dbConfig { host: process.env.DB_HOST || localhost, port: Number(process.env.DB_PORT) || 3306, user: process.env.DB_USER || root, password: process.env.DB_PASSWORD, database: process.env.DB_NAME };然后在 compose 里设置DB_HOSTmysql这样 app 容器才能通过服务名mysql找到数据库。同样的坑也出现在 Redis 上REDIS_HOST要设成redis不是localhost。还有一个容易被忽略的坑如果你在宿主机上装了 MySQL而且已经占了 3306 端口compose 里再映射3306:3306就会报端口冲突。解决办法是把宿主端口改掉比如3307:3306或者干脆不映射宿主端口只在 compose 内部网络访问。我实际遇到过的另一类问题是 MySQL 8.0 默认认证插件是caching_sha2_password老版本 Node.js 的 mysql 驱动不认识报ER_NOT_SUPPORTED_AUTH_MODE。解决办法要么是升级驱动到 mysql2要么在创建用户时指定mysql_native_password。建议直接升驱动别在 MySQL 配置里走回头路。4.3 日志、环境变量与后续发布策略docker-compose 起来之后日志管理是个容易被忽视的问题。默认情况下容器的标准输出和标准错误会写到 Docker 的 json-file 日志里如果不做限制日志文件会无限增长把磁盘撑爆。建议在 compose 里配置 log rotationlogging: driver: json-file options: max-size: 10m max-file: 3这个配置的意思是单个日志文件最大 10MB最多保留 3 个文件超了自动轮转清理。磁盘 4G 的小服务器尤其需要这个。发布策略上我现在的做法是本地构建镜像打 tag推到镜像仓库服务器上docker compose pull docker compose up -d。如果没搭镜像仓库也可以把镜像docker save成 tar 包再拷贝到服务器docker load这个方法在离线环境特别管用。回滚就简单了把.env里指定的镜像 tag 改回上一个版本再docker compose up -d即可。5. 实测中最容易翻车的几个问题与排查思路5.1 镜像下载慢或拉取失败拉取官方镜像慢几乎是国内 Docker 用户的共同记忆。docker pull node:20可能要等十分钟中间还可能超时。解决办法是配置镜像加速器。Docker 的配置文件在/etc/docker/daemon.jsonLinux或 Docker Desktop 的设置界面Windows/MacLinux 下可以这样改sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json -EOF { registry-mirrors: [https://docker.m.daocloud.io] } EOF # 重启 Docker 生效 sudo systemctl daemon-reload sudo systemctl restart docker配置完可以docker info查看Registry Mirrors字段确认生效。如果拉取还是慢检查是不是当前网络环境对 Docker Hub 的连接有问题用docker pull hello-world测试一下最小镜像。镜像加速器本身是公开的基础设施服务使用上没有任何问题但要注意选那些稳定、口碑好的。5.2 docker 服务启动失败与虚拟化支持docker: Cannot connect to the Docker daemon这类报错除了权限问题还可能是 Docker 服务本身没起来。Linux 下先用systemctl status docker看服务状态journalctl -u docker看详细日志。新手最容易忽略的是虚拟机里跑 Docker Desktop 的场景。Windows 下启动 Docker Desktop 时报virtualization support not detected基本可以确定是 Hyper-V 或 WSL2 没有正确开启。先去 BIOS 确认虚拟化技术Intel VT-x / AMD-V是打开的然后确认 Windows 功能里虚拟机平台和适用于 Linux 的 Windows 子系统两个选项都勾上了最后执行wsl --update更新一下 WSL 内核。步骤做完重启电脑Docker Desktop 基本就能正常启动了。Linux 服务器上还有一个常见的坑Docker 服务启动失败但日志里看不出明显错误这时候检查一下磁盘空间。镜像、容器、日志、卷这些东西累积起来很容易把根分区占满Docker 写日志写到一半就挂了。df -h一看根目录 100%清理几个旧镜像后 Docker 马上就能起来。5.3 容器网络不通的第一反应容器跑起来了但应用之间互相访问不了或者宿主机访问不到容器里的端口。我的排查顺序是固定的先确认容器都在同一个网络里docker network ls和docker inspect container_name看 Networks 部分在 app 容器里 ping 数据库服务名docker exec -it app_container ping mysqlalpine 里可能没有 ping先apk add iputils检查端口映射docker ps看PORTS列3000/tcp - 0.0.0.0:3000表示宿主所有网卡的 3000 端口都映射到了容器防火墙排查云服务器上很多所谓网络不通其实是安全组没有放行端口这个跟 Docker 没有任何关系但特别容易误判。还有一个隐蔽的坑容器里访问宿主机的服务不能写localhost要写宿主机的内网 IP。而容器要对外提供服务必须显式映射端口-p或ports配置。Docker 的默认 bridge 网络对外部不可见不映射端口宿主机是访问不到的。5.4 常用命令速查与排查口诀下面这张表是我长期实践后整理了贴在笔记里的你可以直接抄走场景命令查看所有容器含停止的docker ps -a查看容器日志实时docker logs -f container进入容器docker exec -it container /bin/sh查看容器详情docker inspect container构建镜像docker build -t name:tag .启动 composedocker compose up -d停止并删除 compose 服务docker compose down查看磁盘占用docker system df清理无用镜像容器docker system prune -a重启 Docker 服务sudo systemctl restart docker排查的时候记住一个口诀先看权限再看网络最后查日志。权限问题报错最明显网络问题要先确认服务名和端口有没有写对日志能告诉你应用层到底发生了什么。很多问题从日志里一眼就能看出来但你如果一上来就改代码、改配置反而会把问题越搞越乱。5.5 关于忘记保存容器修改和绕过镜像直接改配置的提醒这个我必须单独拿出来说因为几乎每个新手都栽过跟头。容器是临时性的你在docker exec进去后对容器内部做的任何修改比如装了 vim、改了配置、建了临时文件只要容器一删除全部丢失。这是 Docker 的设计理念——一切以镜像为不可变单位。所以正确的操作方式是需要任何持久化修改要么写进 Dockerfile 重新构建镜像要么通过挂载卷或环境变量注入。一两次docker exec进去调试没问题但调试完记得把改动固化到镜像或 compose 配置里否则下次发布时这些修改就凭空消失了。我碰到过一个极端案例同事在容器里手改了数据库配置容器跑了一星期没出问题后来一次正常重启配置回滚到镜像默认值数据库连不上了排查了整整半天才意识到容器内修改不持久。这个教训也算值了。写在后面我现在的部署流程基本已经稳定成一套模板本地开发用 Docker Desktop 起容器线上用 docker-compose 管理应用和依赖服务镜像打 tag 发布日志走容器的标准输出加 log rotation数据库和 Redis 的数据挂卷保存。这套流程跑了大半年几乎没再因为环境问题熬过夜。如果你正打算把 Node.js 项目容器化直接拿这篇里的 Dockerfile 和 compose 配置改一改就能起步。等你跑通第一版再回头看裸机部署那一堆破事估计也会和我一样感慨——这个跟头早该摔了。