【Docker】用Docker构建一个前后端分离的项目 (go+vue)

0x0 前言

最近给自己的一个后端项目AynaAPI做了一个前端。因为是用vue写的,而且使用了vue-router, 所以直接用go-gin渲染前端不太好用,而且也不太符合前后端分离的理念。

反正忽略上面的理由,不管怎么样,总之来说我就是想用用看docker,顺便学习一下docker要如何使用。

但是问题还是有的,但是感觉前言写这么多是不是不太好,啊呀也无所谓了,总之网络上没多少关于如何用docker构建前后端分离项目的教程,或许是我没找到,或者就是屁用没有的教程,总之反正都不是很好用。

总之在这篇文章里我要简略的介绍一下我都后端项目以及前端项目以及如何使用docker+docker-compose来构建项目

0x1 项目简介

项目主要分为两部分,前端,以及后端(这不是废话么,笑了)

总之,前端就是一个简单的SPA单页应用

  • 用了 vue + vue-router
  • ajax 请求后端获取数据

然后,后端就是单纯的api,不包含任何的前端渲染

  • go-gin 作为后端
  • 所有api都以 /api开头
  • 后端有一个上传文件的功能,上传的文件保存在/static下
  • 后端依赖redis

0x2 服务端(后端) Dockerfile

后端的结构如图

1
2
3
4
5
6
7
8
AynaAPI
--config
--...
--server
----main
------main.go
--go.mod
--Dockerfile

总之服务器在/server/main/main.go下,build的时候使用命令go build /server/main/main.go

我们把Dockerfile建立在项目的根目录下,然后写入以下的文件

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
# 使用 golang:alpine作为编译使用的容器, 并且将其命名为build
FROM golang:alpine as build

# 安装gcc等其他编译工具
RUN apk add build-base

# 如果在国内用goproxy
#ENV GOPROXY https://goproxy.cn,direct

# 设定一个工作目录
WORKDIR /go/src/github.com/aynakeya/AynaAPI

# 复制当前文件夹下所有文件到工作目录中
COPY . .

# 编译服务端
RUN go generate && go env && go build -o ayapi ./server/main

# ===========================================================
# 多阶段构建运行服务端

# 因为是在alpine里构建的, 所以同样使用alpine镜像作为运行的容器
FROM alpine:latest

# 维护者, 写不写无所谓的
LABEL MAINTAINER="aynakeya@aynakeya.com"

# 设定一个工作目录
WORKDIR /go/src/github.com/aynakeya/AynaAPI

# 从上一个容器build中复制所有的代码包含可执行文件到当前的工作目录
COPY --from=build /go/src/github.com/aynakeya/AynaAPI ./

# 暴露端口
EXPOSE 8080

# 启动服务端
ENTRYPOINT ./ayapi -c=conf/conf_docker.ini

大概就是这样,首先用golang:alpine编译整个服务端程序,然后把程序以及项目复制到alpine:latest里来运行。这些不难理解。

但是后端依赖redis,而且alpine这个镜像里没有,所以我们需要使用另外一个容器来运行redis,这个时候我们就需要对我们的配置文件做一点小小的修改。把原来的localhost也就是127.0.0.1改为177.7.0.12,至于为啥要改成这个,等等就知道了。

1
2
3
4
5
6
7
[Redis]
;Host = 127.0.0.1:6379
Host = 177.7.0.12:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200

完成了所有的设置之后我们就可以运行一下,试试看有没有任何错误。注意在构建镜像的时候这个**.**一定要加上。

如果一切顺利的话你就能在localhost:8080上看见你的后端程序了

1
2
3
4
5
cd AynaAPI
// 构建镜像, backend 为镜像名字
docker build -t backend:v0.1 .
// 运行
docker run -it -p "8080:8080" --rm backend:v0.1

可以看见已经运行成功了,但是redis是没有连上的。

2022-01-09_002735.jpg

0x3 前端 Dockerfile

前端的结构如图

1
2
3
4
5
6
7
AynaAPI-Frontend
--src
--index.html
--nginx_docker.conf
--package.json
--Dockerfile
...

同样先来看看Dockerfile怎么写

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
# 声明镜像来源为node:12.16.1,命名为build
FROM node:12.18.3 as build

# 声明工作目录
WORKDIR /aynaapi_fronend/

# 拷贝整个前端项目到当前工作目录
COPY . .

# 安装所有的依赖
RUN npm install

# 打包项目,生成的文件保存在./dist下
RUN npm run build

# ===========================================================
# 多阶段构建

#使用nginx
FROM nginx:alpine

LABEL MAINTAINER = "aynakeya@aynakeya.com"

# 暴露8000端口
EXPOSE 8000

# 复制打包好的文件到nginx的www目录下
COPY --from=build /aynaapi_fronend/dist /usr/share/nginx/html
# 把nginx的默认配置改成我们自己的
COPY --from=build /aynaapi_fronend/nginx_docker.conf /etc/nginx/conf.d/default.conf

总之打包文件那部分还是比较好理解的,主要说一下正式运行的部分。这里使用nginx来处理请求,同样使用了alpine的版本来减少最终镜像的大小。然后来看一看我们自己的nginx配置。

首先listen 8000不难理解,就纯粹开放一下8000端口。

接下来做了两个转发,第一是把访问/api的所有请求转发到177.7.0.11:8080,也就是后端的地址,至于为啥是177.7.0.11之后会说。这个rewrite 就是把 /api 重写成/api,相当于没变。这边暂时没用我只是写着,万一那天有用呢。第二个是把所有访问/static的也转发到后端。

然后把访问其他所有路径的请求都用都用打包好的前端处理。这里因为使用了vue-router,所以使用try_files把其他所有路径的请求也用index.html来处理。

也就是说把/ -> /usr/share/nginx/html/index.html

也同样把/xx/xx/* -> /usr/share/nginx/html/index.html

这样就用户直接输入url的时候也能访问到对应的router-view中

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
server {
listen 8000;

server_name _;

client_max_body_size 50M;
location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://177.7.0.11:8080;
rewrite "^/api/(.*)$" /api/$1 break;
}

location /static {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://177.7.0.11:8080;
rewrite "^/static/(.*)$" /static/$1 break;
}

location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html last;
}
}


完成了之后也可以尝试一下

1
2
3
4
5
cd AynaAPI-Frontend
// 构建镜像, backend 为镜像名字
docker build -t frontend:v0.1 .
// 运行
docker run -it -p "8000:8000" --rm frontend:v0.1

0x4 docker-compose来整合前后端

项目结构

1
2
3
-- AynaAPI
-- AynaAPI-Frontend
-- docker-compose.yaml

做完前后端的Dockerfile之后就可以把这两个整合到一起了,首先我们来回顾一下需要解决的问题

  • 后端依赖redis,需要一个redis容器,且要能访问这个redis容器(177.7.0.12)
  • 前端依赖后端,需要一个能访问到后端的地址(177.7.0.11)

那么我们就来写一下这个docker-compose.yaml吧。

可以看到我们首先定义了一个网络,命名为ayapi,且他的地址在177.7.0.0-177.7.0.255上。

然后我们有三个service,分别是frontend,backend,以及redis。

在每个services里,我们通过networks选项给每个容器分配了一个网络,以及对应的ip地址。比如backend就分配到了177.7.0.11,redis分配到了177.7.0.12。这就是为什么前面的ip地址要填这两个。

同时,frontend依赖backend,backend依赖redis,这样可以让容器按顺序(redis-backend-frontend)启动,以此来避免不必要的错误。

再来看看build,context指的是build的时候要传入的上下文,相当于docker build -t xxx .中的**.**。所以frontend就是./AynaAPI-Frontend,后端backend就是./AynaAPI

最后把frontend的8000通过ports暴露给外面,这样外面才可以访问

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
version: "3"


networks:
ayapi:
ipam:
driver: default
config:
- subnet: '177.7.0.0/16'

services:
frontend:
build:
context: ./AynaAPI-Frontend
dockerfile: ./Dockerfile
container_name: ayapi-frontend
restart: always
ports:
- '8000:8000'
depends_on:
- backend
command: [ 'nginx-debug', '-g', 'daemon off;' ]
networks:
ayapi:
ipv4_address: 177.7.0.10

backend:
build:
context: ./AynaAPI
dockerfile: ./Dockerfile
container_name: ayapi-backend
restart: always
depends_on:
- redis
networks:
ayapi:
ipv4_address: 177.7.0.11

redis:
image: redis:alpine
container_name: ayapi-redis
restart: always
networks:
ayapi:
ipv4_address: 177.7.0.12

完成docker-compose.yaml的编写后就可以尝试运行了

1
2
3
4
5
6
// 强制重写build
docker-compose up --build
// 直接运行
docker-compose up
// 后台运行
docker-compose up -d

运行结果

2022-01-09_011048.jpg
2022-01-09_011219.jpg

0x5 发布前端后端镜像,修改docker-compose

以上大概就是差不多基本完成了我的需求。但是有一个问题,就是如果要部署到服务器话,我们得把前端代码,后端的代码,以及docker-compose.yaml上传到服务器,然后用docker-compose运行。

或者写一个脚本,git clone两个代码然后运行docker-compose.

但是不管怎么样,这两种方法部署起来都比较麻烦,而且需要在服务器里进行构建,那么有没有别的方法呢?

当然有了,就是制作前端和后端的镜像,然后把制作好的镜像发布。

推送后端镜像

1
2
docker build -t aynakeya/aynaapi:v0.1 .
docker push aynakeya/aynaapi:v0.1

推送前端镜像

1
2
docker build -t aynakeya/aynaapi-fe:v0.1 .
docker push aynakeya/aynaapi-fe:v0.1

接着修改docker-compose.yaml,把build参数改为image,填上我们自己的镜像名字就行了。

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
version: "3"


networks:
ayapi:
ipam:
driver: default
config:
- subnet: '177.7.0.0/16'

services:
frontend:
image: aynakeya/aynaapi-fe:v0.1
container_name: ayapi-frontend
restart: always
ports:
- '8000:8000'
depends_on:
- backend
command: [ 'nginx-debug', '-g', 'daemon off;' ]
networks:
ayapi:
ipv4_address: 177.7.0.10

backend:
image: aynakeya/aynaapi:v0.1
container_name: ayapi-backend
restart: always
depends_on:
- redis
networks:
ayapi:
ipv4_address: 177.7.0.11

redis:
image: redis:alpine
container_name: ayapi-redis
restart: always
networks:
ayapi:
ipv4_address: 177.7.0.12

这样子的话,如果需要在服务器上部署,只需要上传docker-compose.yaml,然后用docker-compose up就可以运行这整一个前后端分离项目了

0x6 结尾

docker确实好用,虽然自己研究的时候踩了许多的坑,但是我这个时候应该写点什么来升华主题,但是我想不出写什么了,那就这样吧。摆了,啊对对对对对。