Docker Compose 部署前后端分離應用


部署前后端分離應用

容器化 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 應用增添啟動延遲重試的功能,但顯然這對生產環境來說並不合適。

其實,我們可以來分析一下幾個要求:

  1. MySQL 必須在 API 應用啟動前,完成初始化或遷移。
  2. 不應為此給 API 應用引入額外的邏輯。
  3. 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 服務器。

值得注意的也就以下三點:

  1. 掛載 Web 根目錄和配置文件
  2. 開啟反向代理
  3. 開啟偽靜態

掛載目錄和文件已經是老生常談了,這里就不再贅述,在 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;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM