Gin Blog III

Kesa...大约 11 分钟golangdockermysqlnginx

在之前的步骤中,我们完成了基本的文章增删改查功能,接下来就将其部署到 Docker 上

之前已经在 nginx 上部署了一个 vuepress 项目,这次就在另一个 nginx 上部署本项目

前端项目通过反向代理实现在同一域名下不同路径访问不同的应用:

  • localhost/: 为 vuepress blog
  • localhost/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.jspublicPath 修改为环境变量

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/staticgcr.io/distroless/base 参见 Documentation for gcr.io/distroless/base and gcr.io/distroless/staticopen in new window, 相比于 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 --from=build-env /app/dist /app

ENV NODE_OPTIONS=--openssl-legacy-provider 用于解决 Error:0308010C, 详情参见Error message "error:0308010C:digital envelope routines::unsupported"open in new window

构建镜像

$ 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 --from=build-env /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 Composeopen in new window

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: 仅表示依赖关系,但容器并不会等待依赖容器完全启动后再启动

6.2 .env file

编写.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

  1. nginxopen in new window nginx docs
  2. docker nginxopen in new window docker hub
  3. configopen in new window vue-cli docs
  4. history modeopen in new window vue-router docs
  5. HTTP Load Balancingopen in new window nginx docs
  6. Documentation for gcr.io/distroless/base and gcr.io/distroless/staticopen in new window
  7. docker mysqlopen in new window docker hub
  8. Use multi-stage buildsopen in new window docker docs
  9. Error message "error:0308010C:digital envelope routines::unsupported"open in new window stackoverflow
  10. Docker Composeopen in new window Docker docs
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2