近期考虑了去学习如何部署自己的网站项目。根据网上的资料,决定先使用Docker+Nginx的组合来部署到本地上,之后再考虑部署到云端。

项目结构

我的项目前端是React,后端是Express、使用了Socket.io来实现实时通信、使用了MongoDB来存储数据。

Docker容器的话需要为每个服务创建一个容器,所以我需要创建三个容器:前端、后端、数据库。同时还要创建一个Nginx容器来作为反向代理。

Nginx是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP代理服务器。

那么反向代理是什么意思呢?通常情况下我们如果访问一个网站,浏览器会直接向服务器发送请求,服务器再返回数据给浏览器。而反向代理是指,浏览器发送请求给Nginx,Nginx再将请求转发给服务器,服务器返回数据给Nginx,Nginx再返回数据给浏览器。

这么做的目的是为了隐藏服务器的真实IP地址,提高安全性。因为用户只能向Nginx发送请求,而不能直接向服务器发送请求。

Dockerfile

Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化。

为什么要使用Docker呢?因为Docker可以让开发者摆脱“在我的机器上可以运行”的问题。

应用能够在任何地方运行,而不用担心环境问题。这样就可以避免因为环境问题导致的bug,也可以避免因为环境问题导致的部署问题。

Docker的两个重要概念为镜像和容器。

镜像是一个只读的模板,可以想象为一个菜谱、详细列出了如何制作一道菜的步骤。就像你无法在菜谱上做菜一样,你也无法在镜像上做任何操作。

容器是镜像的一个实例,可以想象为一道菜。Docker(厨师)会根据镜像(菜谱)制作出容器(菜),并且可以对容器进行操作。

Dockerfile则是编写菜谱的过程。它是一个文本文件,包含了一条条的指令,每一条指令构建一层,从而构建出一个完整的镜像。

每个服务都需要一个Dockerfile来构建镜像。我的项目结构中暂且只有前端和后端,所以我需要创建两个Dockerfile、存放在这两个目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# client/Dockerfile

# 使用node:20.11.0-alpine作为基础镜像
# alpine代表着这是一个轻量级的镜像、体积更小
FROM node:20.11.0-alpine

# 设置工作目录
# 工作目录是容器中的一个目录,用来存放项目文件
WORKDIR /app

# 复制package.json到工作目录
COPY package.json .

# 安装依赖
RUN npm install

# 复制所有文件到工作目录
COPY . .

# 启动项目
CMD ["npm", "start"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM node:20.11.0-alpine

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

# EXPOSE指令通知了Docker、容器在运行时监听的端口
# 这个指令并不会让容器的端口映射到宿主机的端口,如果需要映射,还需要在运行容器时使用-p参数
EXPOSE 4000

CMD ["node", "./bin/www"]

宿主机是指安装了Docker的机器,也就是我们的电脑。

Docker Compose

Docker Compose是一个用来定义和运行多容器Docker应用的工具。通过一个单独的docker-compose.yml配置文件来配置应用的服务,然后使用docker-compose up命令来从配置文件中构建、启动、管理整个应用。

因为我需要创建多个容器,所以我需要一个docker-compose.yml文件来更好地管理这些容器。

在整个项目的根目录下创建一个docker-compose.yml文件:

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
# docker-compose.yml

version: '3'

# 定义服务
services:
# 定义nginx服务
nginx:
image: nginx:alpine
ports: # 将容器的80端口映射到宿主机的80端口
- "80:80"
depends_on: # 依赖于client和server服务
- client
- server
volumes: # 将宿主机的nginx.conf文件映射到容器的/etc/nginx/conf.d/default.conf文件
# 这里的nginx.conf文件之后会提到
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks: # 将nginx服务加入到app-network网络中
- app-network

client:
build: ./client # 使用client目录下的Dockerfile构建镜像
ports:
- "3000:3000"
networks:
- app-network

server:
build: ./server
ports:
- "4000:4000"
networks:
- app-network

# 定义mongodb服务
mongodb:
image: mongo
ports: # 将容器的27017端口映射到宿主机的28017端口,之后会提到为什么端口号不一样
- "28017:27017"
volumes: # 将mongodb_data卷挂载到/data/db目录
- mongodb_data:/data/db
networks:
- app-network

volumes: # 定义mongodb_data卷
mongodb_data:

networks:
app-network: # 定义app-network网络
driver: bridge

卷是一种数据持久化和数据共享的机制。它可以将宿主机的目录挂载到容器中,这样容器中的数据就可以持久化到宿主机上了。 即使容器被删除,宿主机上的数据也不会丢失。

网络定义了容器之间如何相互通信。每个网络都代表了一个独立的虚拟网络,容器可以连接到这个网络上,从而实现容器之间的通信。bridge 类型会给容器分配一个IP地址,这样容器之间就可以通过IP地址相互通信。不同 bridge 类型的网络是隔离的,即使是同一台宿主机上的容器也不能相互通信。

配置Nginx

Nginx的配置文件是nginx.conf,这个文件需要放在docker-compose.yml文件所在的目录下。

configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80; # 告诉Nginx监听80端口

location / { # 当访问根路径时
proxy_pass http://client:3000; # 将请求转发到client服务的3000端口
}

location /api/ { # 当访问/api路径时
proxy_pass http://server:4000; # 将请求转发到server服务的4000端口
}

location /socket.io/ { # 当访问/socket.io路径时
proxy_pass http://server:4000; # 将请求转发到server服务的4000端口
proxy_http_version 1.1; # 使用HTTP/1.1协议
proxy_set_header Upgrade $http_upgrade; # 设置请求头,和WebSocket有关
proxy_set_header Connection 'upgrade';
}
}

MongoDB

Docker容器中的MongoDB服务不同于平常的MongoDB服务。通常服务端连接MongoDB的地址是localhost:27017,但是在Docker容器中,要使用mongodb:27017

由于我的宿主机上已经有一个MongoDB服务在运行,所以我将容器的27017端口映射到了宿主机的28017端口。这样便可以避免端口冲突、使用宿主机上的MongoDB Compass直接连接localhost:28017来管理容器中的MongoDB服务。

构建和运行

首先要构建整个应用:

1
docker-compose build

如果是想要构建单个服务,可以使用docker-compose build 服务名

服务名就是docker-compose.yml文件中定义的服务名。

然后运行整个应用:

1
docker-compose up

如果想要停止应用,使用docker-compose down

docker-compose build 一样,如果是想要运行/停止单个服务,可以使用docker-compose up/down 服务名

运行后就可以在浏览器中访问localhost来查看应用了。

但是以上步骤只是在本地运行,并且我也没有使用开发环境。部署到云端、使用生产环境还需要更多的实践,之后再说。