---
tags: Docker, Docker Compose, DevOps,Visual Studio
---
<!-- Dark Theme for HackMD -->
<!-- markdownlint-disable MD041 -->
{%hackmd BJrTq20hE %}
# 使用 ***SSL*** 與 ***Domain Name*** 在Windows本機開發後端應用
撰寫者 : Cymon Dez
撰寫時間 : 2020-08-20
修改時間 :
* 2020-09-02 追加相似開源專案參考資料
* 2020-09-07 修正範例內容
* 2020-09-08 錯字修正
* 2021-05-08 修正 docker network名稱 `local-test` => `local_test`
* 2021-09-06
1. 修正 資料夾名稱 `traefik` => `traefik_data`
2. 修正 mkcert 指令
3. 修改 traefik 使用版本 `2.2` => `2.5`
* 2025-09-08 修正開源專案 `stonehenge`的連結
## 前言
此篇感謝公司前輩**Rex**的技術分享與指導
## 動機
1. 本機上需要使用`SSL` + `Domain Name`進行測試時
2. 80,443 port 的複用,減少port衝突
3. 減少修改 `hosts`檔案的麻煩
4. 使用 `VS2019`整合`docker-compose` debug環境
### 建置環境
1. Win10 1909
2. Docker Desktop
### 使用工具/應用
1. Docker 19.03 與 Docker-Compose 1.26.2
> 使用 容器化的方式建置乾淨的開發環境
2. Traefik 2.2
> 動態的反向代理器,須注意 1.7 與 2.x 的設定方式不相容,此篇相關設定均以 2.2以上的版本為主
3. Dnsmasq
> 作為本地DNS,用來引導domain name,以及取代修改hosts檔案
4. mkcert
> 產生本機測試用途之可信任的SSL檔
5. Visual Studio 2019
> 本篇舉例用的IDE,也可以使用其他有整合docker開發環境的IDE
## 建構流程
* 為求本機環境乾淨,本文所用到之服務一律使用Docker架設
### 創建測試用的docker network
```shell
docker network create local_test
```
### 創建docker-compose 專案目錄以及資料結構
```shell
mkdir local-dev
cd local-dev
# 榮果有安裝VS Code,可直接使用VSCode進行
# code .
```
### 設定 .env 檔案內容
在`local-dev`目錄下建立 `.env`檔
`.env`檔內容如下
```shell
# Develop env. settings
TESTING_DOMAIN=local-test.sh
# TESTING_NETWORK需與創建的 docker network一致
TESTING_NETWORK=local_test
TESTING_HTTP_PORT=80
TESTING_HTTPS_PORT=443
# Traefik settings
TRAEFIK_VERSION=v2.2
TRAEFIK_DASHBOARD_PORT=8080
## log level following : DEBUG, PANIC, FATAL, ERROR, WARN, and INFO.
TRAEFIK_LOG_LEVEL=info
# Dnsmasq settings
DNSMASQ_VERSION=latest
## dnsmasq ui 的登入帳密設定,如果嫌麻煩可以空白
DNSMASQ_LOGIN_ACCOUNT=admin
DNSMASQ_LOGIN_PASSWORD=admin
# Portainer settings
PORTAINER_VERSION=1.24.1-alpine
```
### 建立 docker-compose.yml
docker-compose.yml 內容如下
```yaml
version: '3.7'
### ====== Network ====== ###
networks:
default:
name: "${TESTING_NETWORK}"
external: true
### ====== Services ====== ###
services:
#### ====== Traefik ====== ####
traefik:
# The official v2.0 Traefik docker image
image: traefik:${TRAEFIK_VERSION}
container_name: traefik
restart: always
# Enables the web UI and tells Traefik to listen to docker
command:
## api
- --api.debug=true
- --api.dashboard=true
- --api.insecure=true
- --serversTransport.insecureSkipVerify=true
## logs
- --log.level=${TRAEFIK_LOG_LEVEL}
## entry points
- --entryPoints.http.address=:80
- --entryPoints.https.address=:443
## provider
### docker provider
- --providers.docker=true
- --providers.docker.endpoint=unix:///var/run/docker.sock
- --providers.docker.exposedByDefault=false
- --providers.docker.defaultRule=Host(`{{ trimPrefix "/" .Name }}.${TESTING_DOMAIN}`)
- --providers.docker.network=${TESTING_NETWORK}
### file provider
- --providers.file.watch=true
- --providers.file.directory=/etc/traefik/dynamic/
ports:
# The HTTP port
- "${TESTING_HTTP_PORT}:80"
# The HTTPS port
- "${TESTING_HTTPS_PORT}:443"
# Traefik DashBoard
- "${TRAEFIK_DASHBOARD_PORT}:8080"
volumes:
# So that Traefik can listen to the Docker events
- '//var/run/docker.sock:/var/run/docker.sock'
- './traefik-data/acme.json:/acme.json:rw'
- './traefik-data/dynamic-configs/:/etc/traefik/dynamic/:rw'
- './traefik-data/certs/:/ssl/:ro'
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.${TESTING_DOMAIN}`)"
- "traefik.http.routers.traefik.entrypoints=https"
- "traefik.http.routers.traefik.tls=true"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
- "traefik.http.routers.traefik.service=api@internal"
# catchall router
- "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
- "traefik.http.routers.http-catchall.entrypoints=http"
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
# middleware redirect
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
# Dynamic Configuration
#### ====== Portainer ====== ####
portainer:
image: portainer/portainer:1.24.1-alpine
container_name: "portainer"
restart: unless-stopped
command: |-
--no-auth -H unix:///var/run/docker.sock
--logo "${LOGO_URL}"
volumes:
- //var/run/docker.sock:/var/run/docker.sock
ports:
- "${PORTAINER_UI_PORT}:9000"
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.${TESTING_DOMAIN}`)"
- "traefik.http.routers.portainer.entrypoints=https"
- "traefik.http.routers.portainer.tls=true"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
#### ====== Dnsmasq ====== ####
dns:
image: jpillora/dnsmasq
hostname: dns.${TESTING_DOMAIN}
container_name: dns-local
restart: always
cap_add:
- NET_ADMIN
ports:
- '53:53/udp'
- "${DNSMASQ_UI_PORT}:8080"
command:
- "--address=/.${TESTING_DOMAIN}/127.0.0.1"
environment:
- HTTP_USER=${DNSMASQ_LOGIN_ACCOUNT}
- HTTP_PASS=${DNSMASQ_LOGIN_PASSWORD}
volumes:
- './etc/dnsmasq.conf:/etc/dnsmasq.conf'
- './etc/resolv.conf:/etc/resolv.conf'
labels:
- "traefik.enable=true"
- "traefik.http.routers.dnsmasq.rule=Host(`dns-ui.${TESTING_DOMAIN}`)"
- "traefik.http.routers.dnsmasq.entrypoints=https"
- "traefik.http.routers.dnsmasq.tls=true"
- "traefik.http.services.dnsmasq.loadbalancer.server.port=8080"
#### ====== whoami ====== #### (testing web host)
whoami:
image: containous/whoami
container_name: whoami
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.${TESTING_DOMAIN}`)"
- "traefik.http.routers.whoami.entrypoints=https"
- "traefik.http.routers.whoami.tls=true"
```
### 設定 traefik 動態配置
traefik.dynamic.yaml 內容如下:
```yaml
tls:
certificates:
- certFile: /ssl/local-test.sh.crt
keyFile: /ssl/local-test.sh.key
```
### 使用 mkcert產生 SSL
雖然traefik本身支援自動申請Let's Encrypt的SSL,但用於local的環境下請不要使用Let's Encrypt,詳細緣由請參考[Let's Encrypt官方說明](https://letsencrypt.org/zh-tw/docs/certificates-for-localhost/)
安裝mkcert,會使用套件管理工具 `chocolatey`,如果尚未安裝過,請先在`管理員權`下powershell下執行下面命令:
```shell
# PowerShell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
```
開始安裝mkcert
```shell
# PowerSell
choco install mkcert -y
```
建立SSL
```shell
# 當前目錄於 local-dev
mkdir -p traefik-data/certs
cd traefik-data/certs
# 將此目錄註冊至系統的可信任SSL位置,如果之浩此目錄移至其他位置需要重新註冊
mkcert -install
# 建立SSL ,使用 -cert-file 與 -key-file 將齣齣檔rename, domain 必須跟 .env 檔案中的 TESTING_DOMAIN設定一致
mkcert -cert-file local-test.sh.crt -key-file local-test.sh.key "*.local-test.sh"
```
須注意,由 mkcert 創建的 *.local-test.sh 僅能支援 1 level domain
例如:
`app.local-test.sh` (OK)
`app.demo.local-test.sh` (ERROR)
### 設定 dnsmasq 配置
建立目錄 etc
```shell
mkdir etc
cd etc
```
在 etc目錄下建立 `dnsmasq.conf` 與 `resolv.conf`
> dnsmasq.conf : 作為 dnsmasq ip跟domain對應等相關設定
> resolv.conf : 作為 dnsmasq 外部DNS位置的設定,內容可以隨喜好增減
dnsmasq.conf 內容如下:
```shell
#dnsmasq config, for a complete example, see:
# http://oss.segetech.com/intra/srv/dnsmasq.conf
#log all dns queries
log-queries
#don't use hosts nameservers
strict-order
resolv-file=/etc/resolv.conf
# #explicitly define host-ip mappings
# since using command line "--address=/.${TESTING_DOMAIN}/127.0.0.1", so you can ignore this
# address=/local-test.sh/127.0.0.1
```
resolv.conf 內容如下:
```shell
# Cloudflare
nameserver 1.1.1.1
# Google
nameserver 8.8.8.8
# HiNet
nameserver 168.95.1.1
```
### 設定 Windows DNS配置(有線、無線都需要各自設定)
以下是Windows 10 繁體中文環境所做的設定,每台電腦的網路介面名稱可能不盡相同
Windows上的網路介面名稱請使用下列指令查詢。
```shell
netsh interface ip show config
```
注意!以下command皆需要管理員權限。
設定 `Wi-Fi` 的DNS,將 127.0.0.1 加入`Wi-Fi`介面的DNS清單,並將順序設為第一個
```shell
netsh interface ip add dnsservers "Wi-Fi" 127.0.0.1 index=1
```
設定 `乙太網路` 的DNS,將 127.0.0.1 加入`乙太網路`介面的DNS清單,並將順序設為第一個
```shell
netsh interface ip add dnsservers "乙太網路" 127.0.0.1 index=1
```
### 啟動 docker-compose並測試
完整的資料目錄架構如下:
* 黑色是資料夾
* 藍色是檔案
* 綠色是註解
```plantuml
@startuml
salt
{
{T
+ local-dev <color:green> #專案所在的資瞭夾
++ etc
+++ <color:blue> dnsmasq.conf <color:green>#dnsmasq的設定檔
+++ <color:blue> resolv.conf <color:green>#為dnsmasq設定外部的DNS位置
++ traefik-data
+++ certs <color:green>#存放SSL檔案的資料夾
++++ <color:blue> local-test.sh.crt <color:green>#由mkcert建立
++++ <color:blue> local-test.sh.key <color:green>#由mkcert建立
+++ dynamic-configs
++++ <color:blue> traefik.dynamic.yml <color:green>#由traefik的動態設定檔,SSL的位置必須在此設定
++ <color:blue> .env
++ <color:blue> docker-compose.yml
}
}
@enduml
```
啟動並測試
```shell
# 進入 local-dev目錄
# 啟動 docker compose services
docker-compose up -d
# 檢查是否有啟動錯誤的服務
docker-compose logs
# 測試網路設定
curl https://whoami.local-test.sh
```
如果上面指令皆順利執行,就可以開啟瀏覽器進入下列頁面:
* [Traefik 管理介面](https://traefik.local-test.sh)
* [Dnsmasq 設定介面](https://dns-ui.local-test.sh)
* [Portainer 管理介面](https://portainer.local-test.sh)
如果遇到domain name問題,請改用localhost:port的方式進入個服務的管理介面做調整(個服務的port皆使用本篇預設為主)
* [Traefik 管理介面 - localhost:8080](http://localhost:8080)
* [Dnsmasq 設定介面- localhost:8888](http://localhost:8888)
* [Portainer 管理介面- localhost:9000](http://localhost:9000)
---
### 與 `Visual Studio 2019` Debug環境整合
本篇文章撰寫時的VS2019版本:VS2019 16.7.2
#### 建立 ASP .NET core 3 專案
#### 加入Docker-Compose支援,並將起始專案設為docker-compose
1. 選擇 [ 新增 > 容器協調器支援]。 [ Docker 支援選項 ]
2. 對話框出現後,選擇 [ Docker Compose]。
詳細內容請參考[微軟官方教學](https://docs.microsoft.com/zh-tw/visualstudio/containers/tutorial-multicontainer?view=vs-2019)
#### 修改 `docker-compose.override.yml`
修改後的結果如下:
```yml
version: '3.4'
services:
webapplicationdockercomposedemo:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
# 設定label
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.local-test.sh`)"
- "traefik.http.routers.app.entrypoints=https"
# should be enable tls, then just can using https for traefik
- traefik.http.routers.app.tls=true
- "traefik.http.services.app.loadbalancer.server.scheme=https"
# you should be set 443 port ,if not ,it will connect 80 port, then will be error
- "traefik.http.services.app.loadbalancer.server.port=443"
- "traefik.docker.network=local_test"
# 設定 network
networks:
- local_test
#必須設定設定
networks:
local_test:
external: true
```
#### 修改 docker-compose.dcproj
該檔案位於dotnet專案目錄底下
修改後的內容如下:
```xml
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
<PropertyGroup Label="Globals">
<ProjectVersion>2.1</ProjectVersion>
<DockerTargetOS>Linux</DockerTargetOS>
<ProjectGuid>4711cffe-8e4c-4077-8578-e91d5b9398f4</ProjectGuid>
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
<DockerServiceName>webapplicationdockercomposedemo</DockerServiceName>
</PropertyGroup>
<PropertyGroup>
<!-- 將DockerServiceUrl內容修改成與docker-compose.yml中設定的host相同 -->
<DockerServiceUrl>https://app.local-test.sh</DockerServiceUrl>
</PropertyGroup>
<ItemGroup>
<None Include="docker-compose.override.yml">
<DependentUpon>docker-compose.yml</DependentUpon>
</None>
<None Include="docker-compose.yml" />
<None Include=".dockerignore" />
</ItemGroup>
</Project>
```
#### F5 啟動debug
瀏覽器順利進入到demo畫面及代表設定成功
## 結語
此篇文章主要以網路架構的方式,配合docker建置開發環境。
此方式雖然方便,但由於架設了不少服務,會比較吃系統資源,如果環境條件不許可,
可以使用 `快速指令修改hosts檔案` + `mkcert來解決開發需求`
## 後記
目前GitHub上已有類似概念的開源專案[stonehenge](https://github.com/druidfi/stonehenge)可供參考(2020-09-02追加)
> stonehenge旨在解決網路開發人員的基本問題:如何盡可能輕鬆地進行本地環境開發。
> stonehenge為您提供多個項目的共用開發環境。它將處理專案的路由和本地域,以及開箱即用的這些域的 SSL 證書。
特色:
1. 整合了 traefik, portainer, mailhog, ssh-agent 等服務
2. 目前(2020-09-02)只能在 Linux, MacOS, WSL2環境上使用