部署前后端分離應用
容器化 Abp 應用
關於 Abp 應用的容器化,其實和普通的 ASP.NET Core 應用差不多,大家可以參考我此前的文章。
唯一需要注意的是:因為 Abp 解決方案中有多個項目,在 publish 過程中需要手動指定啟動項目,例如:
# 其余內容請參考上述文章
# 修改 RUN dotnet publish -c Release -o /app 為以下內容
RUN dotnet publish ./src/YourProjectName.Web.Host/YourProjectName.Web.Host.csproj -c Release -o /app
使用 sql 文件應用遷移
在使用 EF Core 進行 Code First 開發的時候,我們往往都習慣在 VS 的控制台中使用Update-Database
完成遷移。但在實際部署過程中,考慮開發環境可能無法直接與部署所用主機相連,我們可以通過導出 sql 文件的形式來完成遷移。
在解決方案的根目錄下打開命令行:
dotnet ef migrations script -p .\src\YourProjectName.EntityFrameworkCore\ -o .\init.sql
然后,將該 init.sql 文件掛載到 mysql 的鏡像的初始化目錄當中:
version: "3"
services:
mysql:
# ...
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
這樣,就會在 mysql 容器啟動時,自動完成遷移。
使用外部網絡橋接數據庫
在我們的 API 應用中,會在啟動階段檢測數據庫中是否正確配置了基礎信息,這就帶來一個棘手的問題:當所用數據庫比如說 MySQL,也是通過同一個 Docker Compose 部署的時候,會有啟動延遲,從而使得數據庫還未來得及應用遷移(Apply Migrations),即便在配置文件中配置了depends_on
也無法避免。
depends_on
只影響容器啟動的順序,而此處 MySQL 的啟動延遲是在容器啟動后發生的。
在此前,我一直是通過腳本或者額外的代碼來為 API 應用增添啟動延遲重試的功能,但顯然這對生產環境來說並不合適。
其實,我們可以來分析一下幾個要求:
- MySQL 必須在 API 應用啟動前,完成初始化或遷移。
- 不應為此給 API 應用引入額外的邏輯。
- API 應用的更新不應該影響數據庫。
所以,最終還是決定將數據庫獨立出來,單獨用一個 Docker Compose 來進行部署,可這也帶來一個新的問題:API 應用無法確定數據庫所在的子網 IP,或者說兩者甚至不在同一子網中。
那么,自然得想辦法將數據庫重新加入 API 應用所在的子網,通過查找資料,Docker Compose 已經為我們提供了這一功能,即使用外部網絡。
這里又可以多說一句,在我們使用 Docker Compose 部署的時候,其默認會為我們創建一個子網,並將我們的定義的服務添加到該子網中。而這個網絡是直接由 Docker Compose 管理的,在
up
時創建,在down
時銷毀,因此不符合我們需要將多個 Docker Compose 定義的服務加入同一子網的要求。
手動創建一個虛擬子網:
docker network create xxx
在配置文件中定義該網絡,並將服務分別添加到該網絡:
# db.yml
version: "3"
services:
mysql:
# ...
networks:
- xxx
networks:
# 定義該網絡為外部網絡
xxx:
external: true
# 同理
# docker-compose.yml
version: "3"
services:
api:
# ...
networks:
- xxx
networks:
xxx:
external: true
然后,在部署的時候,我們只需要在第一次部署時,先等等 MySQL 已經完成初始化,再啟動 API 應用即可。且對此后的 CI/CD 過程,我們也只需要關注 API 應用鏡像的更新(需要修改數據庫表結構除外)。
使用 Nginx 部署 SPA 應用
我們的前端都是以 SPA 應用來構建的,也就是說只需發布靜態文件即可。這里我們就使用 Nginx 作為 Web 服務器。
值得注意的也就以下三點:
- 掛載 Web 根目錄和配置文件
- 開啟反向代理
- 開啟偽靜態
掛載目錄和文件已經是老生常談了,這里就不再贅述,在 docker-compose.yml 中配置以下兩行即可:
version: "3"
services:
web:
# ...
volumes:
- ./html:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
接下來是開啟反向代理,這里我們所請求的 API 都是以/api
為前綴的相對路徑,因此只需要在 nginx.conf 中配置:
server {
# ...
location /api {
proxy_pass http://api;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
}
}
可能細心的人有發現這里將請求代理給了 api 這個域名,其實也就是我們所定義的 api 服務。那為了讓這種寫法有效,自然別忘了將 web 服務和 api 服務添加到同一子網,可以是之前的外部子網,或者再新建一個內部子網也行。
配置到這里,其實已經是可以用了,但在你刷新頁面的時候不時會出現 404 錯誤。這是因為 SPA 應用的路由並不對應真正文件的路徑,我們需要將對應的請求指向我們真正的文件,也就是偽靜態。
例如,你的應用發布在 Web 根目錄下,其主頁面名為 index.html,可以如下配置:
server {
# ...
location / {
try_files $uri $uri/ /index.html;
}
}
結語
最終,你所有的配置文件應該如下:
docker-compose.yml
version: "3"
services:
api:
container_name: xxx_api
image: xxx:api
ports:
- "21021:80"
volumes:
# 這里映射日志目錄
- ./App_Data:/app/App_Data
environment:
- ConnectionStrings:Default=server=mysql;userid=root;pwd=xxx;port=3306;database=xxx;Charset=utf-8;
# 跨域控制
- App:ServerRootAddress=http://xxx.xxx:21021
- App:ClientRootAddress=http://xxx.xxx:8000
- App:CorsOrigins=http://xxx.xxx:8000,http://xxx.xxx:21021
networks:
- xxx
web:
container_name: xxx_web
image: nginx
ports:
- "8000:80"
volumes:
- ./html:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- xxx
networks:
xxx:
external: true
db.yml
version: "3"
services:
mysql:
container_name: xxx_mysql
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=xxx
- MYSQL_DATABASE=xxx
volumes:
- ./mysql:/var/lib/mysql
- ./charset.cnf:/etc/mysql/conf.d/charset.cnf
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- xxx
networks:
xxx:
external: true
nginx.conf
server {
listen 80;
# gzip config
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://api;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
}
}