Method Steps of Docker+DockerCompose Encapsulating web Application
- 2021-11-02 03:38:37
- OfStack
Back-end build api
Front-end build web
Gateway Construction gateway
Nginx Configuration
Dockerfile
Lua Realizes Gateway Authentication Based on Enterprise WeChat
Container orchestration using DockerCompose
This article shows you how to run the back end, front end, and gateway all using the Docker container, and finally use DockerCompose for container orchestration.
Technology stack
Front end
React Ant DesignBack end
Go IrisGateway
Nginx OpenResty Lua Enterprise WeChat
Back-end build api
Although we wrote EXPOSE 4182 here, which is only used for testing, we will not actually expose the back-end interface port in the production environment.
Instead, they are accessed to each other through the network between containers, and eventually forwarded using Nginx.
FROM golang:1.15.5
LABEL maintainer="K8sCat <k8scat@gmail.com>"
EXPOSE 4182
ENV GOPROXY=https://goproxy.cn,direct \
GO111MODULE=on
WORKDIR /go/src/github.com/k8scat/containerized-app/api
COPY . .
RUN go mod download && \
go build -o api main.go && \
chmod +x api
ENTRYPOINT [ "./api" ]
Front-end build web
What is worth mentioning here is that because the front end will definitely call the back end interface, and this interface address changes according to the deployment,
So here we use the ARG directive to set the interface address of the back end, so that we only need to pass in when building the image
--build-arg REACT_APP_BASE_URL=https://example.com/api
You can adjust the back-end interface address instead of changing the code.
Another point, some friends will definitely find that Entrypoint and CMD are used here at the same time, so that the front-end port can be adjusted when running, but in fact, we don't need to adjust it here, because Nginx is finally used for forwarding here.
FROM node:lts
LABEL maintainer="K8sCat <k8scat@gmail.com>"
WORKDIR /web
COPY . .
ARG REACT_APP_BASE_URL
RUN npm config set registry https://registry.npm.taobao.org && \
npm install && \
npm run build && \
npm install -g serve
ENTRYPOINT [ "serve", "-s", "build" ]
CMD [ "-l", "3214" ]
Gateway Construction gateway
Nginx Configuration
Here, we set the upstream of the back end and the front end respectively, and then set the location rule for forwarding.
Here are a few points that can be said once:
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 {
root html;
index index.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-centos
LABEL maintainer="K8sCat <k8scat@gmail.com>"
COPY gateway.conf /etc/nginx/conf.d/gateway.conf
COPY gateway.lua /usr/local/openresty/nginx/conf/gateway.lua
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
# Install lua-resty-http
RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-http
Lua Realizes Gateway Authentication Based on Enterprise WeChat
Some of the configuration parameters here are variables set by obtaining Nginx.
local json = require("cjson")
local http = require("resty.http")
local uri = ngx.var.uri
local uri_args = ngx.req.get_uri_args()
local scheme = ngx.var.scheme
local corp_id = ngx.var.corp_id
local agent_id = ngx.var.agent_id
local secret = ngx.var.secret
local callback_scheme = ngx.var.callback_scheme or scheme
local callback_host = ngx.var.callback_host
local callback_uri = ngx.var.callback_uri
local use_secure_cookie = ngx.var.use_secure_cookie == "true" or false
local callback_url = callback_scheme .. "://" .. callback_host .. callback_uri
local redirect_url = callback_scheme .. "://" .. callback_host .. ngx.var.request_uri
local 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 then
return nil, (err or "access token request failed: " .. (err or "unknown reason"))
end
if res.status ~= 200 then
return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/gettoken: " .. res.body
end
local data = json.decode(res.body)
if data["errcode"] ~= 0 then
return nil, data["errmsg"]
else
return data["access_token"]
end
end
local 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 then
return nil, "get profile request failed: " .. (err or "unknown reason")
end
if res.status ~= 200 then
return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"
end
local userinfo = json.decode(res.body)
if userinfo["errcode"] == 0 then
if userinfo["UserId"] then
res, 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 then
return nil, "get user request failed: " .. (err or "unknown reason")
end
if res.status ~= 200 then
return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/user/get"
end
local user = json.decode(res.body)
if user["errcode"] == 0 then
return user
else
return nil, user["errmsg"]
end
else
return nil, "UserId not exists"
end
else
return nil, userinfo["errmsg"]
end
end
local function is_authorized()
local headers = ngx.req.get_headers()
local expires = tonumber(ngx.var.cookie_OauthExpires) or 0
local user_id = ngx.unescape_uri(ngx.var.cookie_OauthUserID or "")
local token = ngx.var.cookie_OauthAccessToken or ""
if expires == 0 and headers["OauthExpires"] then
expires = tonumber(headers["OauthExpires"])
end
if user_id:len() == 0 and headers["OauthUserID"] then
user_id = headers["OauthUserID"]
end
if token:len() == 0 and headers["OauthAccessToken"] then
token = headers["OauthAccessToken"]
end
local expect_token = callback_host .. user_id .. expires
if token == expect_token and expires then
if expires > ngx.time() then
return true
else
return false
end
else
return false
end
end
local 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
}))
end
local function authorize()
if uri ~= callback_uri then
return redirect_to_auth()
end
local code = uri_args["code"]
if not code then
ngx.log(ngx.ERR, "not received code from https://open.work.weixin.qq.com/wwopen/sso/qrConnect")
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
local access_token, request_access_token_err = request_access_token(code)
if not access_token then
ngx.log(ngx.ERR, "got error during access token request: " .. request_access_token_err)
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
local user, request_user_err = request_user(access_token, code)
if not user then
ngx.log(ngx.ERR, "got error during profile request: " .. request_user_err)
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
ngx.log(ngx.ERR, "user id: " .. user["userid"])
local expires = ngx.time() + token_expires
local cookie_tail = "; version=1; path=/; Max-Age=" .. expires
if use_secure_cookie then
cookie_tail = cookie_tail .. "; secure"
end
local user_id = user["userid"]
local user_token = callback_host .. user_id .. expires
ngx.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"])
end
local function handle_logout()
if uri == logout_uri then
ngx.header["Set-Cookie"] = "OauthAccessToken==deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
--return ngx.redirect("/")
end
end
handle_logout()
if (not is_authorized()) then
authorize()
end
Container orchestration using DockerCompose
A few points need to be made here:
Setting the args of the front end can pass in the back-end interface address when the front end is built Setting the hostname of the gateway can set the hostname of the gateway container Setting the environment of the gateway can pass in the relevant configuration At last, only the gateway layer exposes the port
version: "3.8"
services:
api:
build: ./api
image: ca-api:latest
container_name: ca-api
web:
build:
context: ./web
args:
REACT_APP_BASE_URL: https://example.com/api
image: ca-web:latest
container_name: ca-web
gateway:
build: ./gateway
image: ca-gateway:latest
hostname: example.com
volumes:
- ./gateway/certs/fullchain.pem:/certs/cert.crt
- ./gateway/certs/privkey.pem:/certs/cert.key
ports:
- 80:80
- 443:443
environment:
- 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=true
container_name: ca-gateway
Open source code
GitHub https://github.com/k8scat/containerized-app
Gitee https://gitee.com/k8scat/containerized-app