Gin Blog III
在之前的步骤中,我们完成了基本的文章增删改查功能,接下来就将其部署到 Docker 上
之前已经在 nginx 上部署了一个 vuepress 项目,这次就在另一个 nginx 上部署本项目
前端项目通过反向代理实现在同一域名下不同路径访问不同的应用:
localhost/
: 为 vuepress bloglocalhost/app/
: 为本应用
后端项目通过创建一个负载均衡 nginx 来转发前端的请求
1. Fontend deploy
1.1 配置目录
创建资源文件夹,这里 conf 文件夹不用手动创建,后面从容器中复制一份出来
MyDockerApps
└── blog-app
├── app
│ ├── conf
│ ├── data
│ └── logs
└── blog
├── conf
├── data
└── logs
blog-app
: 应用根路径app
: 本应用blog
: vuepress blog 应用conf
: nginx 配置data
: 资源路径logs
: nginx 日志
启动 nginx 并复制配置文件
$ docker run -d --rm --name test-nginx nginx
$ docker container cp test-nginx:/etc/nginx/ ./app
$ docker container cp test-nginx:/etc/nginx/ ./blog
$ mv ./app/nginx ./app/conf
$ mv ./blog/nginx ./blog/conf
--rm
: stop 容器时,容器自动删除docker container cp container:dir destDir
: 将容器内的文件复制到指定目录
1.2 配置反向代理
修改./blog/conf/conf.d/default.conf
, 建议将 default
改为自定义的域名 servername
server {
listen 80;
listen [::]:80;
server_name localhost;
access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /app/ {
proxy_pass http://app/;
}
error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
location /app/
: URI 匹配,proxy_pass
将/app/
的请求代理到指定地址 需要注意的是http://app/
和http://app
http://app/
: 将会把 URI 去除前缀后追加至代理路径 例如:htpp://localhost:/app/article
将被代理至http://app-nginx/article
http://app
: 将完整匹配的 URI 追加至代理路径 例如:htpp://localhost:/app/article
将被代理至http://app-nginx/app/article
./blog/conf/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
upstream app {
server app-nginx:80;
}
}
upstream
: 配置上游服务app
: 名称server
: 指定地址,app-nginx
为部署本应用的容器名,在容器加入同一网络之后,其将作为容器的域名
./app/conf/conf.d/default.conf
server {
listen 80;
listen [::]:80;
server_name localhost;
access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
1.4 Docker Network
两个 nginx 容器需要使用 docker network 互联,创建 network
$ docker network create -d bridge blog-app-net
-d
: 网络模式,bridge
为桥接模式
1.3 简单测试
现在简单测试下反向代理的效果,为了方便可以编写启动容器的脚本
start-app.sh
#!/usr/bin/env sh
# app-nginx
docker run -d \
--name app-nginx \
-v $HOME/MyDockerApps/blog-app/app/conf:/etc/nginx:ro \
-v $HOME/MyDockerApps/blog-app/app/logs:/var/log/nginx \
-v $HOME/MyDockerApps/blog-app/app/data/dist:/usr/share/nginx/html \
-v /etc/localtime:/etc/localtime:ro \
--network blog-app-net \
--restart always \
nginx
# blog-nginx
docker run -d \
--name blog-nginx \
-p 18080:80 \
-v $HOME/MyDockerApps/blog-app/blog/conf:/etc/nginx:ro \
-v $HOME/MyDockerApps/blog-app/blog/logs:/var/log/nginx \
-v $HOME/MyDockerApps/blog-app/blog/data/dist:/usr/share/nginx/html \
-v /etc/localtime:/etc/localtime:ro \
--network blog-app-net \
--restart always \
nginx
-v /etc/localtime:/etc/localtime:ro
: 同步宿主机时区
recreate-app.sh
#!/usr/bin/env sh
docker rm -f blog-nginx app-nginx
./start-app.sh
restart-app.sh
#!/usr/bin/env sh
docker restart app-nginx blog-nginx
写入测试用的页面:
$ mkdir ./blog/data/dist ./app/data/dist
$ echo 'HELLO BLOG' >> ./blog/data/dist/index.html
$ echo 'HELLO APP' >> ./app/data/dist/index.html
启动容器,并测试
$ ./start-app.sh
$ curl 'http://localhost:18080'
HELLO BLOG
$ curl 'http://localhost:18080/app/'
HELLO APP
1.4 Deploy
测试反向代理成功后就可以部署前端应用了,vuepress 的项目可以使用上述的 ./blog/data/dist/index.html
替代
1.4.1 配置Public Path
Vue CLI 默认认为应用部署在域名根路径,若需要部署在非根目录则需要修改 publicPath
为对应的路径, 以及 router 和 axios 的baseurl
我们将不同的变量放在环境配置中,这样就可以根据环境使用不同的配置了,比如:dev
环境使用 publicPath: '/'
即可而部署是可以改为 ‘/app/’
修改.env.production
,添加 PUBLIC_PATH
并修改 server 参数
# public path
PUBLIC_PATH = '/app/'
将vue.config.js
的 publicPath
修改为环境变量
const pulicPath = process.env.PUBLIC_PATH
module.export {
publicPath: pulicPath,
// ...
}
const baseURL = process.env.PUBLIC_PATH
const createRouter = () =>
new Router({
base: baseURL,
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const baseURL = process.env.PUBLIC_PATH + process.env.VUE_APP_BASE_API
// create an axios instance
const service = axios.create({
baseURL: baseURL, // url = base url + request url
withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
1.4.2 部署脚本
在前端项目根目录创建deploy/deploy.sh
#!/usr/bin/env sh
set -e
yellowFront='\e[33m'
redFront='\e[31m'
greenFront='\e[32m'
restoe='\e[0m'
echo -e $yellowFront'Start Deploy'$restoe
yarn run build:prod
distPath='dist'
appPath=$HOME/MyDockerApps/blog-app/app/data
echo -e $greenFront' Deploying resources ...'$restoe
if [[ -d $appPath/dist ]]
then
echo -e $redFront' Deleting exists resources ...'$restoe
rm -rf $appPath/dist
fi
cp -r $distPath $appPath
echo -e $greenFront' Restart' `docker restart app-nginx`$restoe
echo -e $yellowFront'Deploy Done'$restoe
简单流程如下:
- 构建项目生成
dist
目录 - 删除原有的静态资源(若存在),将
dist
目录复制到资源目录 - 重启容器
在package.json
中添加指令
"deploy": "sh ./deploy/deploy.sh"
执行部署脚本
$ yarn run deploy
浏览器访问http://localhost:18080/app/
可以成功进入 Dashboard 页面
现阶段项目的构建都是在本地环境的,但是对于每个人来说本地环境不尽相同,不能够保证构建结果一致
更好的做法是采用多阶段构建(mulit-stage build)在使用 docker 镜像进行编译构建以保证环境的一致性
2. Load Balancer
本节将添加一个 nginx 作为负载均衡服务器并采用轮询策略将请求转发至三个后台服务
和上面类似,将配置文件挂载到本地
start-load-banlancer.sh
#!/usr/bin/env sh
# load-balancer-nginx
docker run -d \
-p 19090:80 \
--name load-balancer-nginx \
-v $HOME/MyDockerApps/blog-app/load-balancer/logs:/var/log/nginx \
-v $HOME/MyDockerApps/blog-app/load-balancer/conf:/etc/nginx \
-v /etc/localtime:/etc/localtime \
--restart always \
--network blog-app-net \
nginx
修改配置文件./load-balancer/conf/conf.d/localhost.conf
server {
listen 19090;
listen [::]:19090;
server_name localhost;
access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 404 /404.html;
}
配置负载均衡./load-balancer/conf/nginx.conf
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
upstream go-server {
server blog-app-server-01:19091;
server blog-app-server-02:19092;
server blog-app-server-03:19093;
}
}
upstream go-server
: 设置了三个服务,默认是权重为 1 的加权轮询策略
至此,负载均衡服务就完成了,接下来部署后台服务
3. Mysql Deploy
类似的,创建资源目录
blog-app
└── mysql
├── conf
└── data
conf
: mysql 配置data
: 存储 mysql 数据
start-mysql.sh
#!/usr/bin/env sh
# blog-app-mysql
docker run -d \
--name blog-app-mysql \
-v $HOME/MyDockerApps/blog-app/mysql/conf:/etc/mysql/conf.d \
-v $HOME/MyDockerApps/blog-app/mysql/data:/var/lib/mysql \
-v /etc/localtime:/etc/localtime:ro \
-e MYSQL_ROOT_PASSWORD=pass \
--network blog-app-net \
--restart always \
mysql
-v $HOME/MyDockerApps/blog-app/mysql/conf:/etc/mysql/conf.d
: 挂载自定义配置-v $HOME/MyDockerApps/blog-app/mysql/data:/var/lib/mysql
: 挂载数据库文件-e MYSQL_ROOT_PASSWORD=pass
: 设置 ROOT 用户密码
3.1 初始化数据库
启动容器后,执行 sql 脚本
$ docker exec -i blog-app-mysql sh -c 'mysql -uroot -p${MYSQL_ROOT_PASSWORD}' < blog.sql
sh -c
:-c
后面的字符串讲作为 shell 的命令执行
可以直接进入 容器访问 mysql
$ docker exec -it blog-app-mysql bash
mysql -u root -p
也可以启动一个同一 docker network 的临时 mysql 实例作为 client 进行访问
$ docker run -it --rm --network blog-app-net mysql mysql -hblog-app-mysql -uroot -p
4. Backend Deploy
4.1 本地编译
部署之前,先在本地编译好可执行文件
$ CGO_ENABLED=0 GOOS=linux go build -o ./bin/blog-app-server main.go
CGO_ENABLED=0
: 禁用 CGO 动态链接,采用静态链接库,因为运行环境在 scratch 镜像中GOOS
: 声明程序构建环境的目标操作系统
4.2 Dockerfile
创建 Dockerfile,这里使用 scratch
镜像,这样构建的镜像会非常小
FROM scratch
WORKDIR /app/config
COPY ./bin/blog-app-server /app
WORKDIR /app
ENV APP_ENV=docker
EXPOSE 9090
ENTRYPOINT ["/app/blog-app-server"]
注意 scratch 中没有 shell ,无法直接使用 mkdir 创建目录
可以使用 WORKDIR 来创建目录, 或者在多阶段构建中从其他容器中复制过来
需要注意的是在 WORKDIR /app/config
之后再次使用 WORKDIR /app
将工作目录切换到了 /app
若切换的话,启动容器时应用会到/app/config/config
下寻找配置文件,和预期不符
构建镜像并查看
$ docker build -t blog-app-server:local
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
blog-app-server local 93827d1581c0 7 seconds ago 18MB
golang alpine 6f9d081b1170 10 days ago 315MB
golang latest d939cc1fb139 5 weeks ago 941MB
这里可以对比下 golang 官方和 goalng:alpine 的大小,使用 scratch
镜像非常小
因为这些官方镜像包含了各种编译环境、库和工具等所以大小会比较大
也可以使用官方教程中使用的 gcr.io/distroless/static
或 gcr.io/distroless/base
参见 Documentation for gcr.io/distroless/base
and gcr.io/distroless/static
, 相比于 scratch 有着更好的安全性
4.3 资源目录
blog-app
└── server
└── conf
创建配置文件 ./conf/config.docker.yaml
server:
port: 9090
readTimeout: 10s
readHeaderTimeout: 10ms
writeTimeout: 10s
mysql:
host: blog-app-mysql
port: 3306
username: root
password: pass
database: gin_blog
charset: utf8
gorm:
tablePrefix: blog_
maxIdleConns: 10
maxOpenConns: 100
logLevel: error
host: blog-app-mysql
: 容器和 mysql 容器在同一网络下,容器名将作为域名logLevel: warn
: 生产环境可以提高 log level
4.3 启动 Container
#!/usr/bin/env sh
serverName="blog-app-server0"
port=19090
for (( i=1; i<4; i++ ))
do
let port++
docker run -d \
--name $serverName$i \
-p $port:9090 \
-v $HOME/MyDockerApps/blog-app/server/conf:/app/config \
-v /etc/localtime:/etc/localtime \
-e APP_ENV=docker \
--network blog-app-net \
--restart always \
blog-app-server:v1
done
上述脚本将启动三个后端容器
5. Muli-stage builds
在上面的例子中,我们都是采用本地编译/构建然后将资源部署至本地挂载的目录中。但是这样有一个弊端,不能保证不同的环境编译或构建的结果,因为每个人的编译环境不尽相同不能保证其编译结果;若是个人项目的话只在单个设备上运行问题不大,若是需要部署到不同的环境中则需要保证结果的一致性。
多阶段构建 (Multi-stage build) 不仅可以解决上述问题,还可以有效的降低镜像的大小 (将构建结果直接复制到极小的镜像中运行,不必带入构建过程中的需要的工具,库等)
下面就来将前端和后端项目采用 Multi-stage 来构建镜像
5.1 Front end
创建 .dockerignore
**/dist
**/node_modules
yarn.lock
忽略掉不需要的文件和目录
创建Dockerfile
#
# Build
#
FROM node:17 AS build-env
COPY ./ /app
WORKDIR /app
ENV NODE_OPTIONS=--openssl-legacy-provider
RUN yarn config set registry https://registry.npm.taobao.org \
&& yarn config set proxy http://127.0.0.1:7890 \
&& yarn config set https-proxy http://127.0.0.1:7890 \
&& yarn install \
&& yarn run build:prod
#
# Deploy
#
FROM nginx:alpine
WORKDIR /app
COPY /app/dist /app
ENV NODE_OPTIONS=--openssl-legacy-provider
用于解决 Error:0308010C
, 详情参见Error message "error:0308010C:digital envelope routines::unsupported"
构建镜像
$ docker build --network host -t app-blog:v1 .
5.2 Backend
创建 .dockerignore
bin/
config/
blog.sql
README.md
Dockerfile
#
# Build
#
FROM golang:alpine AS build-env
# Set proxy
ENV GOPROXY https://goproxy.cn,direct
COPY . /app
WORKDIR /app
RUN go mod download \
&& CGO_ENABLED=0 GOOS=linux go build -o /blog-app-server
#
# Deploy
#
FROM gcr.io/distroless/static:latest
WORKDIR /app/config
COPY /blog-app-server /app
WORKDIR /app
EXPOSE 9090
ENTRYPOINT ["/app/blog-app-server"]
构建镜像
$ docker build --network host -t blog-app-server:v1 .
5.3 编写启动脚本
在上面的步骤中我们都是一个个启动容器的,比较繁琐,现在我们优化一下启动脚本
在 blog-app
目录下创建 scripts
,并将之前的启动脚本放在这里并编写新的脚本一次启动所有容器
start-apps.sh
#!/usr/bin/env sh
# run all container
./app.sh
./mysql.sh
./server.sh
./load-balancer.sh
这样就能一次性启动所有容器了
6. Docker Compose
之前的步骤中我们使用了脚本来一次启动所有容器,但是如果需要重新启动,重新构建,配置网络等步骤需要编写更加复杂的脚本。当需要启动的容器数量越多,管理就越复杂
此时就需要用到 Docker Compose 来管理多个容器所构成的应用,具体介绍可以参见Docker Compose
6.1 Compose File
在项目资源目录创建/blog-app/docker-compose.yaml
, 不建议将其放在项目根目录,因为 Docker Compose 是用于编排和管理多个容器,不应和单个项目糅合在一起
version: "3.9"
services:
blog-nginx:
image: nginx:alpine
ports:
- "18080:80"
volumes:
- type: bind
source: "${BLOG_NGINX_PATH}/conf"
target: /etc/nginx
read_only: true
- type: bind
source: "${BLOG_NGINX_PATH}/logs"
target: /var/log/nginx
- type: bind
source: "${BLOG_NGINX_PATH}/data/dist"
target: /usr/share/nginx/html
- type: bind
source: /etc/localtime
target: /etc/localtime
read_only: true
restart: always
depends_on:
- app-nginx
app-nginx:
image: nginx:alpine
volumes:
- type: bind
source: "${APP_NGINX_PATH}/conf"
target: /etc/nginx
read_only: true
- type: bind
source: "${APP_NGINX_PATH}/logs"
target: /var/log/nginx
- type: bind
source: "${APP_NGINX_PATH}/data/dist"
target: /usr/share/nginx/html
- type: bind
source: /etc/localtime
target: /etc/localtime
read_only: true
restart: always
depends_on:
- load-balancer-nginx
load-balancer-nginx:
image: nginx:alpine
ports:
- "19090:80"
volumes:
- type: bind
source: "${LOAD_BALANCER_NGINX_PATH}/conf"
target: /etc/nginx
read_only: true
- type: bind
source: "${LOAD_BALANCER_NGINX_PATH}/logs"
target: /var/log/nginx
- type: bind
source: /etc/localtime
target: /etc/localtime
read_only: true
restart: always
depends_on:
- blog-app-server
server:
build: "${BACKEND_PATH}/"
ports:
- "9090"
volumes:
- type: bind
source: "${SERVER_PATH}/config"
target: /app/config
- type: bind
source: /etc/localtime
target: /etc/localtime
read_only: true
restart: always
depends_on:
- msyql-db
mysql-db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
volumes:
- type: bind
source: "${MYSQL_PATH}/conf"
target: /etc/mysql/conf.d
read_only: true
- type: bind
source: "${MYSQL_PATH}/data"
target: /var/lib/mysql
- type: bind
source: /etc/localtime
target: /etc/localtime
read_only: true
- type: bind
source: "${MYSQL_PATH}/init"
target: /docker-entrypoint-initdb.d
restart: always
depends_on
: 仅表示依赖关系,但容器并不会等待依赖容器完全启动后再启动
.env
file
6.2 编写.env
文件设置上面出现的环境变量
BLOG_NGINX_PATH=sample path
APP_NGINX_PATH=sample path
LOAD_BALANCER_NGINX_PATH=sample path
SERVER_PATH=sample path
MYSQL_ROOT_PASSWORD=root pass
MYSQL_PATH=sample path
6.3 启动服务
$ docker compose -p ba up -d --scale server=3
-p
: 指定 project 名称,默认为当前目录的名称(例如 blog-app),启动的容器名格式为project-service-No.
, 例如ba-server-1
-d
: detached, 服务在后台运行--scale service=num
: 指定启动服务容器的数量,这里启动了三个后台服务
至此完整的服务部署就完成了,在浏览器中访问 http://localhost:18080/app/
即可访问
Reference
- nginx nginx docs
- docker nginx docker hub
- config vue-cli docs
- history mode vue-router docs
- HTTP Load Balancing nginx docs
- Documentation for
gcr.io/distroless/base
andgcr.io/distroless/static
- docker mysql docker hub
- Use multi-stage builds docker docs
- Error message "error:0308010C:digital envelope routines::unsupported" stackoverflow
- Docker Compose Docker docs