Docker+DockerCompose封装web应用的方法步骤

目录

  • 技术栈
  • 后端构建 api
  • 前端构建 web
  • 网关构建 gateway
    • Nginx 配置
    • Dockerfile
    • Lua 实现基于企业微信的网关认证
  • 使用 DockerCompose 进行容器编排
    这篇文章会介绍如何将后端、前端和网关通通使用 Docker 容器进行运行,并最终使用 DockerCompose 进行容器编排。

    技术栈
    前端
    • React
    • Ant Design
    后端
    • Go
    • Iris
    网关
    • Nginx
    • OpenResty
    • Lua
    • 企业微信

    后端构建 api
    这里虽然我们写了 EXPOSE 4182,这个只用在测试的时候,生产环境实际上我们不会将后端接口端口进行暴露,
    而是通过容器间的网络进行互相访问,以及最终会使用 Nginx 进行转发。

    FROM golang:1.15.5LABEL maintainer="K8sCat "EXPOSE 4182ENV GOPROXY=https://goproxy.cn,direct \GO111MODULE=onWORKDIR /go/src/github.com/k8scat/containerized-app/apiCOPY . .RUN go mod download && \go build -o api main.go && \chmod +x apiENTRYPOINT [ "./api" ]


    前端构建 web
    这里值得一提的是,因为前端肯定会去调用后端接口,而且这个接口地址是根据部署而改变,
    所以这里我们使用了 ARG 指令进行设置后端的接口地址,这样我们只需要在构建镜像的时候传入 --build-arg REACT_APP_BASE_URL=https://example.com/api 就可以调整后端接口地址了,而不是去改动代码。

    【Docker+DockerCompose封装web应用的方法步骤】还有一点,有朋友肯定会发现这里同时使用到了 Entrypoint 和 CMD,这是为了可以在运行的时候调整前端的端口,但实际上我们这里没必要去调整,因为这里最终也是用 Nginx 进行转发。

    FROM node:ltsLABEL maintainer="K8sCat "WORKDIR /webCOPY . .ARG REACT_APP_BASE_URLRUN npm config set registry https://registry.npm.taobao.org && \npm install && \npm run build && \npm install -g serveENTRYPOINT [ "serve", "-s", "build" ]CMD [ "-l", "3214" ]


    网关构建 gateway

    Nginx 配置

    这里我们就分别设置了后端和前端的上游,然后设置 location 规则进行转发。
    这里有几个点可以说一下:
    • 通过 set_by_lua 获取容器的环境变量,最终在运行的时候通过设置 environment 设置这些环境变量,更加灵活
    • server_name 使用到了 $hostname,运行时需要设置容器的 hostname
    • ssl_certificate 和 ssl_certificate_key 不能使用变量设置
    • 加载 gateway.lua 脚本实现企业微信的网关认证
    upstream web {server ca-web:3214; }upstream api { server ca-api:4182; }server { set_by_lua $corp_id 'return os.getenv("CORP_ID")'; set_by_lua $agent_id 'return os.getenv("AGENT_ID")'; set_by_lua $secret 'return os.getenv("SECRET")'; set_by_lua $callback_host 'return os.getenv("CALLBACK_HOST")'; set_by_lua $callback_schema 'return os.getenv("CALLBACK_SCHEMA")'; set_by_lua $callback_uri 'return os.getenv("CALLBACK_URI")'; set_by_lua $logout_uri 'return os.getenv("LOGOUT_URI")'; set_by_lua $token_expires 'return os.getenv("TOKEN_EXPIRES")'; set_by_lua $use_secure_cookie 'return os.getenv("USE_SECURE_COOKIE")'; listen 443 ssl http2; server_name $hostname; resolver 8.8.8.8; ssl_certificate /certs/cert.crt; ssl_certificate_key /certs/cert.key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers AESGCM:HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; lua_ssl_verify_depth 2; lua_ssl_trusted_certificate /etc/pki/tls/certs/ca-bundle.crt; if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") {set $year $1; set $month $2; set $day $3; } access_log logs/access_$year$month$day.log main; error_log logs/error.log; access_by_lua_file "/usr/local/openresty/nginx/conf/gateway.lua"; location ^~ /gateway {roothtml; indexindex.html index.htm; } location ^~ /api {proxy_pass http://api; proxy_read_timeout 3600; proxy_http_version 1.1; proxy_set_header X_FORWARDED_PROTO https; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header Connection ""; } location ^~ / {proxy_pass http://web; proxy_read_timeout 3600; proxy_http_version 1.1; proxy_set_header X_FORWARDED_PROTO https; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header Connection ""; } error_page 500 502 503 504 /50x.html; location = /50x.html {root html; }}server { listen 80; server_name $hostname; location / {rewrite ^/(.*) https://$server_name/$1 redirect; }}


    Dockerfile

    FROM openresty/openresty:1.19.3.1-centosLABEL maintainer="K8sCat "COPY gateway.conf /etc/nginx/conf.d/gateway.confCOPY gateway.lua /usr/local/openresty/nginx/conf/gateway.luaCOPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf# Install lua-resty-httpRUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-http


    Lua 实现基于企业微信的网关认证

    这里面的一些配置参数都是通过获取 Nginx 设置的变量。

    local json = require("cjson")local http = require("resty.http")local uri = ngx.var.urilocal uri_args = ngx.req.get_uri_args()local scheme = ngx.var.schemelocal corp_id = ngx.var.corp_idlocal agent_id = ngx.var.agent_idlocal secret = ngx.var.secretlocal callback_scheme = ngx.var.callback_scheme or schemelocal callback_host = ngx.var.callback_hostlocal callback_uri = ngx.var.callback_urilocal use_secure_cookie = ngx.var.use_secure_cookie == "true" or falselocal callback_url = callback_scheme .. "://" .. callback_host .. callback_urilocal redirect_url = callback_scheme .. "://" .. callback_host .. ngx.var.request_urilocal logout_uri = ngx.var.logout_uri or "/logout"local token_expires = ngx.var.token_expires or "7200"token_expires = tonumber(token_expires)local function request_access_token(code)local request = http.new()request:set_timeout(7000)local res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/gettoken", {method = "GET",query = {corpid = corp_id,corpsecret = secret,},ssl_verify = true,})if not res thenreturn nil, (err or "access token request failed: " .. (err or "unknown reason"))endif res.status ~= 200 thenreturn nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/gettoken: " .. res.bodyendlocal data = https://www.it610.com/article/json.decode(res.body)if data["errcode"] ~= 0 thenreturn nil, data["errmsg"]elsereturn data["access_token"]endendlocal function request_user(access_token, code)local request = http.new()request:set_timeout(7000)local res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo", {method = "GET",query = {access_token = access_token,code = code,},ssl_verify = true,})if not res thenreturn nil, "get profile request failed: " .. (err or "unknown reason")endif res.status ~= 200 thenreturn nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"endlocal userinfo = json.decode(res.body)if userinfo["errcode"] == 0 thenif userinfo["UserId"] thenres, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/user/get", {method = "GET",query = {access_token = access_token,userid = userinfo["UserId"],},ssl_verify = true,})if not res thenreturn nil, "get user request failed: " .. (err or "unknown reason")endif res.status ~= 200 thenreturn nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/user/get"endlocal user = json.decode(res.body)if user["errcode"] == 0 thenreturn userelsereturn nil, user["errmsg"]endelsereturn nil, "UserId not exists"endelsereturn nil, userinfo["errmsg"]endendlocal function is_authorized()local headers = ngx.req.get_headers()local expires = tonumber(ngx.var.cookie_OauthExpires) or 0local user_id = ngx.unescape_uri(ngx.var.cookie_OauthUserID or "")local token = ngx.var.cookie_OauthAccessToken or ""if expires == 0 and headers["OauthExpires"] thenexpires = tonumber(headers["OauthExpires"])endif user_id:len() == 0 and headers["OauthUserID"] thenuser_id = headers["OauthUserID"]endif token:len() == 0 and headers["OauthAccessToken"] thentoken = headers["OauthAccessToken"]endlocal expect_token = callback_host .. user_id .. expiresif token == expect_token and expires thenif expires > ngx.time() thenreturn trueelsereturn falseendelsereturn falseendendlocal function redirect_to_auth()return ngx.redirect("https://open.work.weixin.qq.com/wwopen/sso/qrConnect?" .. ngx.encode_args({appid = corp_id,agentid = agent_id,redirect_uri = callback_url,state = redirect_url}))endlocal function authorize()if uri ~= callback_uri thenreturn redirect_to_auth()endlocal code = uri_args["code"]if not code thenngx.log(ngx.ERR, "not received code from https://open.work.weixin.qq.com/wwopen/sso/qrConnect")return ngx.exit(ngx.HTTP_FORBIDDEN)endlocal access_token, request_access_token_err = request_access_token(code)if not access_token thenngx.log(ngx.ERR, "got error during access token request: " .. request_access_token_err)return ngx.exit(ngx.HTTP_FORBIDDEN)endlocal user, request_user_err = request_user(access_token, code)if not user thenngx.log(ngx.ERR, "got error during profile request: " .. request_user_err)return ngx.exit(ngx.HTTP_FORBIDDEN)endngx.log(ngx.ERR, "user id: " .. user["userid"])local expires = ngx.time() + token_expireslocal cookie_tail = "; version=1; path=/; Max-Age=" .. expiresif use_secure_cookie thencookie_tail = cookie_tail .. "; secure"endlocal user_id = user["userid"]local user_token = callback_host .. user_id .. expiresngx.header["Set-Cookie"] = {"OauthUserID=" .. ngx.escape_uri(user_id) .. cookie_tail,"OauthAccessToken=" .. ngx.escape_uri(user_token) .. cookie_tail,"OauthExpires=" .. expires .. cookie_tail,}return ngx.redirect(uri_args["state"])endlocal function handle_logout()if uri == logout_uri thenngx.header["Set-Cookie"] = "OauthAccessToken==deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"--return ngx.redirect("/")endendhandle_logout()if (not is_authorized()) thenauthorize()end


    使用 DockerCompose 进行容器编排
    这里需要讲几个点:
    • 设置前端的 args 可以在前端构建时传入后端接口地址
    • 设置网关的 hostname 可以设置网关容器的 hostname
    • 设置网关的 environment 可以传入相关配置
    • 最终运行时只有网关层进行暴露端口
    version: "3.8"services:api:build: ./apiimage: ca-api:latestcontainer_name: ca-apiweb:build:context: ./webargs:REACT_APP_BASE_URL: https://example.com/apiimage: ca-web:latestcontainer_name: ca-webgateway:build: ./gatewayimage: ca-gateway:latesthostname: example.comvolumes:- ./gateway/certs/fullchain.pem:/certs/cert.crt- ./gateway/certs/privkey.pem:/certs/cert.keyports:- 80:80- 443:443environment:- CORP_ID=- AGENT_ID=- SECRET=- CALLBACK_HOST=example.com- CALLBACK_SCHEMA=https- CALLBACK_URI=/gateway/oauth_wechat- LOGOUT_URI=/gateway/oauth_logout- TOKEN_EXPIRES=7200- USE_SECURE_COOKIE=truecontainer_name: ca-gateway

    开源代码
    GitHub https://github.com/k8scat/containerized-app
    Gitee https://gitee.com/k8scat/containerized-app
    到此这篇关于Docker+DockerCompose封装web应用的文章就介绍到这了,更多相关Docker+DockerCompose封装web应用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

      推荐阅读