公司一直有一個維護的案子,該維護案的開發環境都一直放在雲端上,例如 SQL Server、後端 API 等等…。 但是該維護案又不會常常使用到開發環境,但是沒有開發環境偶爾要維護又很麻煩,所以為了節省成本,就決定把該開發環境容器化,並且把放在雲端的服務關掉。

該維護案是前後端分離,前端是 Angular 後端是 ASP.NET Core APISQL Server,所以要容器化的是 後端 的部分,只要將後端容器化後,前端開發就不用煩惱後端環境怎麼建立。

所以今天要介紹如何用 docker-compose 建立 ASP.NET Core API 與 SQL Server 服務,並且該 API 會連 SQL Server 當作資料來源。 讓前端的開發工程師在開發的時候,不用為了後端的環境而煩惱。

環境說明

這次的 demo 我建立了一個 ASP.NET Core 的專案,且該專案用 EF Core 對資料庫做操作,以及建立了一個含有 northwnd 資料庫的 SQL Server 映像檔。

ASP.NET Core 專案說明

我有將專案放到 Github 上供參考 ASP.NET Core API 專案

該專案就是一個非常簡單的 API ,只有一個 Controller ,一個 API ,該 API 就是讀取資料庫,撈出一筆資料後丟出來。 如下方程式碼

ValueController
  • cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly NORTHWNDContext _db;

public ValuesController(NORTHWNDContext db)
{
this._db = db;
}

// GET api/values
[HttpGet]
public ActionResult<Customers> Get()
{
var customer = _db.Customers.First();
return customer;
}
}

而該專案的資料夾,請參考下圖,會專注在 docker-compose.yml 上。 其中 Dockerfile 是用來建置後端 API ,可以參考這篇 [Docker] 多階段建置映像檔 (multi-stage build),以 ASP.NET Core 為例

backend projectbackend project

範例資料庫說明

我有將範例的資料庫推上我的 docker hub - docker/mileslin/northwnd ,直接 docker run -e "ACCEPT_EULA=Y" -p 1433:1433 -d mileslin/northwnd 就可以把 northwnd 資料庫執行起來。 (但是我們今天不是要單獨執行,所以可以先不用執行此指令。)

資料庫登入資訊

  • port: 1433
  • 帳號: sa
  • 密碼: 2wsx#EDC

若要了解如何從無到有建立 SQL Server 映像檔,可以參考 [Docker] SQL Server Container 快速入門

看懂 docker-compose.yml

環境說明結束了,現在就是來使用 docker-compose 建立服務啦。 如果還不知道什麼是 docker-compose? 可以先看一下官方說明。

Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration. To learn more about all the features of Compose, see the list of features.

簡單來說 docker-compose 就是用來定義一次執行多個容器的工具。

所以我們現在要做的就是,要寫一個 docker-compose.yml 檔案,該檔案會定義我要建立的 docker 服務,並且使用 docker-compose 來啟動服務。 那接下來我就來帶大家看這份 docker-compose.yml 檔案吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 使用 Compose file format V3.2 版本
version: '3.2'
# 定義 services 區塊,以這裡的例子,我定義了 `mydb` 與 `myapi` 這兩個 services。
services:
# 定義一個名為 mydb 的 service
mydb:
# 此 service 會使用此 image 執行。(這是我已經上傳好,含有 northwnd 資料庫的 SQL Server)
image: mileslin/northwnd
# 對外 mapping 1433 port。 如果沒有想要從外部連資料庫,則此設定可以不用。
ports:
- "1433:1433"
# 將此 service 加入一個自訂義的網路,名為 demonetwork。
networks:
- demonetwork
# 設定此 service 的環境變數,要使用此 SQL Server image ,要設定此環境變數,代表同意 End-User Licensing Agreement.
environment:
ACCEPT_EULA: "Y"
# 定義一個名為 myapi 的服務
myapi:
# 此 service 要用建置的方式產生 image ,並且設定 Dockerfile 的路徑位置
build:
context: ./
dockerfile: Dockerfile
# 對外 mapping 8000 port。所以當網站起來的時候,可以用 http://localhost:8000/api/values 取得資料。
ports:
- "8000:80"
# 設定此 service 的環境變數,覆寫資料庫的連線字串。 可以注意的是,我的 SQL Server 的位址是 mydb 這個服務的名稱。
environment:
ConnectionStrings__northwnd_db: "Server=mydb;Database=NORTHWND;User ID=sa;Password=2wsx#EDC;Trusted_Connection=True;Integrated Security=False;"
# 將此 service 加入一個自訂義的網路,名為 demonetwork。
networks:
- demonetwork
# 當 mydb service 起來後,再執行此 service 的設定
depends_on:
- mydb

# 建立一個名為 demonetwork 的自訂義網路,且採用預設的 bridge 類別的網路
networks:
demonetwork:
driver: bridge

depends_on 設定說明
雖然上面有設定 myapi depends_on mydb ,但是請注意,就算 mydb 的 service 先啟動,不代表資料庫服務已經起來,還是需要等一段時間,資料庫的實體才會起來,在這段時間可能會造成 myapi 無法連線的問題。 如果要處理這個問題,需要使用 wait-for-it

Docker 預設的 DNS

這裡要額外補充說明的是,為什麼 SQL Server 的位址是 mydb 這個服務的名稱?

只要是 docker services 在同一個自訂義網路的情況下,docker 的 DNS 會從 service 的名稱解析到該 service 的位址。 所以以上方的範例,我將 mydb 與 myapi 都加入一個自訂義網路,名為 demonetwork ,所以我的 myapi 服務在設定資料庫連線字串的時候,可以使用 mydb 當作資料庫的位址,docker 的 DNS 就會幫我導到該資料庫去。 參考: Embedded DNS server in user-defined networks

請注意,如果 service 不是建立在自訂義的網路,而是預設的 bridge 網路,則 docker 的 DNS 服務需要額外做設定。
參考: Configure container DNS

使用 docker-compose 指令啟動服務與移除服務

現在萬事都準備好了,只剩下把服務啟動, 執行 docker-compose -f docker-compose.yml up,就會看到下圖的情況,docker-compose 會把在 yml 裡面定義好的服務都啟動。

因為 docker-compose.yml 是 docker 預設會找到檔案,所以 -f docker-compose.yml 不一定要設定,也可以使用此指令 docker-compose up 效果是一樣的。

docker-compose updocker-compose up

服務起來後,我們來看一下 http://localhost:8000/api/values 是不是真的能夠連到資料

resultresult

上圖可以知道,我們已經成功連到資料,且該資料是從資料庫來的!!

那接下來,要移除服務也很簡單,只要輸入 docker-compose down 就可以把所以服務移除。

docker-compose downdocker-compose down

如果啟動服務要背景執行的話,可以加入 -d 參數,如: docker-compose -f docker-compose.yml up -d 這樣 service 就會在背景執行了

其他 docker-compose 指令

docker-compose 也可以對單一 service 做操作,例如,我要停止 myapi service,我就可以使用 docker-compose stop myapi,重新啟動 myapi service,就可以使用 docker-compose start myapi

請參考下圖,我使用 docker-compose ps 來觀察停止服務與啟動服務的狀態

restart servicerestart service

重新建置 myapi 的方式

如果 myapi 的程式碼有調整,想要更新的話,是沒辦法直接 docker-compose down + docker-compose up 就更新的,因為有 cache 的問題,所以如果重新建置的話,要用其他方式,我這邊介紹兩種做法。

方法一
把 myapi 的 image 刪除,再重新 docker-compose up
請參考下方指令

  1. docker-compose images 找到要刪除的 image id
  2. docker-compose stop myapi, docker-compose rm myapi 停止與刪除 myapi service
  3. docker rmi 46ccc 刪除 myapi 的 image ,
  4. docker-compose up 重新啟動服務
    rebuild service
    • bash
    1
    2
    3
    4
    5
    docker-compose images
    docker-compose stop myapi
    docker-compose rm myapi
    docker rmi 46ccc
    docker-compose up

執行成功後,會看到下圖的樣子

rebuild-1rebuild-1

方法二
重新建置該 service ,這方法是參考 How to restart a single container with docker-compose 的說明。

只要執行下面 4 行指令就可以直接重 build 了。

rebuild service
  • bash
1
2
3
4
docker-compose stop myapi
docker-compose build myapi
docker-compose create myapi
docker-compose start myapi

小結

如果讀者要直接執行的話,可以到我的專案範例 docker-compose-demo 下載下來後,直接 docker-compose up 就可以把服務執行起來了。 剩下的只要看懂 yml 在寫什麼,就可以很輕易運用到自己的專案上了。

延伸閱讀

[Overview of Docker Compose]
[Get started with Docker Compose]
[Overview of docker-compose CLI]