基于 Node.js 项目 Docker部署笔记

因为这是一个笔记,不是教程,所以内容记录的比较散,不具备教程逻辑。我只记录我认为的重点。

首先基于我这个笔记的开源项目地址:express-demo

里面基于 express.js 框架搭建的一个标准的 node.js web 项目模板。也是为了复习 node.js 搭建学习使用的。

[!NOTE]

博客服务名 web 和 db 跟实际开源的名称不一致。意思一致

一、Docker基础知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# docker-compose.yml
version: "3.8"

services:
api:
build: .
container_name: blog-api
restart: always
ports:
- "3000:3000"
depends_on:
- mysql
env_file:
- .env
environment:
- NODE_ENV=production
- DB_HOST=mysql # 重要:在docker-compose网络中使用服务名作为主机名
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME}
- DB_PORT=3306
- DB_CONNECTION_LIMIT=${DB_CONNECTION_LIMIT}
- JWT_SECRET=${JWT_SECRET}

volumes:
- ./logs:/app/logs # 持久化日志目录
networks:
- blog-network

mysql:
image: mysql:8.0
container_name: blog-mysql
restart: always
environment:
- MYSQL_ROOT_PASSWORD==${DB_PASSWORD}
- MYSQL_DATABASE=${DB_NAME}
ports:
- "3306:3306" # 建议移除 MySQL 的 ports: "3306:3306",仅允许内网访问
volumes:
- mysql-data:/var/lib/mysql # 持久化数据
- ./mysql-init:/docker-entrypoint-initdb.d # 初始化SQL脚本(可选)
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
networks:
- blog-network

networks:
blog-network:
driver: bridge

volumes:
mysql-data:
driver: local

Docker 各组件关系图解

1
2
3
[源码] → Dockerfile → docker build → [镜像] → docker run → [容器]

└── docker-compose.yml → 编排多个容器

这里其实我想搞懂的就是docker-compose.yml启动流程,还有构建细节 build: .build: . 表示使用当前项目源码中的 Dockerfile 构建镜像。因为没有镜像就没有办法构建容器,而docker-compose 核心功能就是进行容器编排,因此假如说本地手动有现成的基于Dockerfile 构建的镜像,那其实docker-compose.yml 也可以直接使用,类似数据库服务中的image: mysql:8.0

  • 必要性:若没有 Dockerfile,Compose 无法自动生成镜像(除非直接使用现有镜像如 mysql:8.0)。

2. 手动构建镜像后的 Compose 使用

假设你已手动构建镜像:

1
2
# 手动构建镜像(假设镜像名为 my-api-image:v1)
docker build -t my-api-image:v1 .

修改 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
services:
api:
image: my-api-image:v1 # 直接引用已有镜像
# 删除 build: . 配置
networks:
- blog-network
mysql:
image: mysql:8.0
networks:
- blog-network
  • 结果docker-compose up -d 会直接使用 my-api-image:v1 启动容器,无需重新构建
  • 通信能力:只要 apimysql 在同一网络(blog-network),通信完全正常。

如何理解Dockerfile 和镜像的关系

Dockerfile 是一个文本文件,包含了一系列用于构建 Docker 镜像的指令(如 FROMRUNCOPY 等)。
镜像 是 Dockerfile 通过 docker build 命令生成的静态文件,它是一个分层的只读模板,用于创建容器。

核心关系:

  1. Dockerfile → 镜像
    • Dockerfile 是“菜谱”,镜像是由它“烹饪”出的“成品”。
    • 每条 Dockerfile 指令会在镜像中生成一个只读层(Layer),最终多层叠加形成完整的镜像。
  2. 镜像 → 容器
    • 镜像是静态模板,容器是镜像的动态运行实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[宿主机]

├── docker-compose.yml → 定义多服务协作规则
├── Dockerfile → 构建 web 项目 服务镜像
└── .env → 提供敏感配置
// 查看容器内实际环境变量
# docker-compose exec api env


[Docker 引擎]

├── 构建镜像(web 服务)
│ └── 根据 Dockerfile 生成包含代码和依赖的镜像

├── 拉取镜像(db 服务)
│ └── 直接使用 mysql:8.0 官方镜像

└── 创建并启动容器
├── api 容器(运行 node dist/index.js)
└── mysql 容器(运行 MySQL 数据库)

1. Docker启动项目命令区别

行为 docker-compose up -d --build docker-compose build + docker-compose up -d
镜像构建时机 每次运行都强制重新构建 build 时构建,up 直接使用现有镜像
是否自动启动服务 是(构建后立即启动) 否(需手动运行 up
缓存利用 总是重新构建,可能忽略缓存 可手动控制是否清理缓存(如 --no-cache
适用场景 开发环境(代码频繁变更) 生产环境(构建和部署分离)

关闭占用端口的进程:

1
2
3
# Linux/Mac
lsof -i :3000
kill <PID>

2. Dockerfile关键点

  • 构建依赖时要安装所有依赖而非仅生产依赖,因为项目是加入了 ts 类型,ts 只在开发环境使用
1
2
3
4
5
6
7
8
9
# ❌ 错误方式:导致tsc命令不可用
RUN pnpm install --prod

# ✅ 正确方式:安装所有依赖
RUN pnpm install

# 构建后再精简
RUN pnpm build
RUN pnpm prune --prod

3. 如何去理解Docker 挂载机制

Docker 挂载机制的核心:双向数据同步

核心概念:挂载是共享而非复制

  • 不是数据拷贝:挂载操作不会将文件复制到容器内,而是直接暴露宿主机的存储位置。
  • 没有主从之分:宿主机和容器是平等的存储访问者,任何一方的修改都会影响另一方。

Docker 的挂载分为两类,但数据同步逻辑相同:

  1. 卷挂载(Volume)

    • Docker 管理的存储位置(通常位于 /var/lib/docker/volumes/
    • 示例:MySQL 数据持久化
  2. 绑定挂载(Bind Mount)

    • 直接挂载宿主机的文件或目录
    • 示例:Nginx 配置或静态资源挂载

    无论是哪种挂载方式,Docker 都会通过 Linux 内核的 联合文件系统(UnionFS) 实现以下机制:

    • 挂载本质:将宿主机文件/目录的 物理存储地址 映射到容器内。
    • 数据同步:对挂载目录的读写操作,实际上是直接操作宿主机或卷的物理文件系统。
    • 实时性:修改会立即生效(无需重启容器),因为双方共享同一块磁盘区域。

二、Docker 网络与通信知识体系梳理

1.服务名称(Service Name)

docker-compose.yml 中定义的 services 名称(如 webdb)。

1
2
3
4
5
services:
web: # 服务名 = web
build: .
db: # 服务名 = db
image: mysql
  • 容器内可通过 http://web:3000mysql://db:3306 访问其他服务。

2.网络名称(Network Name)

docker-compose.yml 中自定义的网络(如 blog-network

1
2
3
4
5
6
7
8
9
10
networks:
backend-network:
driver: bridge
services:
web:
networks:
- blog-network
db:
networks:
- blog-network

3.服务名称与网络名称的区别

  1. 网络名称 (blog-network):
    • 是通信基础设施,相当于一个虚拟局域网
    • 连接到同一网络的容器可以相互通信
    • 是容器间通信的媒介
  2. 服务名称 (db):
    • 是容器在网络内的”主机名”(hostname)
    • 作为容器的网络标识符
    • 由Docker的内部DNS服务自动注册

连接过程解析:

当你设置DB_HOST=db时,这里的流程是:

  1. web容器需要连接数据库
  2. web查找主机名为mysql的服务
  3. Docker网络的DNS服务将db解析为MySQL容器的IP地址,因为 dbweb 属于同一blog-network网络下
  4. 连接建立

这就好比:

  • blog-network = 你家的WiFi网络
  • mysql = 连接到该WiFi的一台设备的主机名

4.容器间通信

1. 同一网络内的通信
  • 通过服务名访问:Docker 内置 DNS 自动解析服务名为容器 IP。

    1
    2
    3
    # 在 web 容器中访问 db 服务
    ping db
    curl http://db:3306
  • 通过容器名访问(不推荐):需显式指定 container_name

    1
    2
    3
    services:
    db:
    container_name: mysql-db
    1
    2
    # 在 web 容器中访问
    ping mysql-db
2. 不同网络间的通信
  • 方案 1:将容器加入多个网络。

    1
    2
    3
    4
    5
    services:
    web:
    networks:
    - frontend-network
    - backend-network
  • 方案 2:通过网关 IP 访问(需知道目标网络网关地址)。

三、Docker存储与数据管理

1. 卷命名规则

  • 格式项目名称_卷名称
  • 示例my-blog-api_mysql-data
  • 好处:避免不同项目间卷名冲突,自动组织和分类

2. 数据持久化

  • 数据库数据:mysql-data:/var/lib/mysql
  • 日志文件:./logs:/app/logs

四、进入容器-调试与日志管理

1. 容器管理命令

1
2
3
4
5
6
# 进入API(node.js 项目)容器
docker exec -it blog-api sh

# 进入MySQL容器并连接数据库
docker exec -it blog-mysql bash
mysql -uroot -p #再输入密码就进去了

2. 日志查看方法

1
2
3
4
5
6
7
8
9
# 查看所有服务日志
docker-compose logs

# 查看特定服务日志
docker-compose logs api
docker-compose logs mysql

# 实时跟踪日志
docker-compose logs -f api

3. 宿主机直接查看日志

因为在docker-compose.yml中做了挂载,所以可以在宿主中直接查看

1
cat ./logs/combined.log

五、常见问题与解决方案

1. 构建失败

  • 问题tsc: not found - TypeScript编译失败
  • 原因:Dockerfile中只安装了生产依赖
  • 解决方案:修改为安装所有依赖pnpm install

2. 数据库连接问题

  • 问题connect ECONNREFUSED 192.168.148.2:3306
  • 解决方案:
    • 检查MySQL容器是否正常启动
    • 确认环境变量配置正确
    • 添加MySQL健康检查,确保API服务在MySQL就绪后启动

3. 授权错误

  • 问题Access denied for user 'root'@'192.168.148.3'
  • 解决方案:
    • 检查.env文件中的数据库凭据
    • 修复MySQL环境变量中的额外等号:MYSQL_ROOT_PASSWORD==${DB_PASSWORD}

六、最佳实践总结

  1. 网络配置:使用自定义网络,通过服务名互相访问
  2. 数据持久化:使用命名卷保存重要数据
  3. 环境变量:使用.env文件集中管理敏感配置
  4. 构建优化:构建时安装全部依赖,运行时精简
  5. 日志管理:将日志挂载到宿主机便于查看和分析