﻿# 使用 Dockerfile 建置容器映象(image)時應多注意「換行符號」帶來的影響

今天使用 Visual Studio Code 的 [Remote Development](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) 將一個 .NET 6 與 SQL Server 開發環境全部放到 Docker 容器中。雖然[照著文件](https://code.visualstudio.com/docs/remote/containers)操作都很順利，但在專案加入版控之後，問題就出現了。這篇文章我想點出現今容器化、跨平台的時代中，**換行符號**其實不得不面對他，否則遇到問題會無所適從。

### 體驗在容器中開發的感覺

1.  先安裝好 [Docker Desktop](https://www.docker.com/products/docker-desktop) 與 [Remote Development](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) 擴充套件
    
2.  先建立基本 ASP.NET Core 6 專案並用 [Visual Studio Code](https://code.visualstudio.com/) 開啟
    
    ```bash
    dotnet new web -n devcontainer-demo1
    code devcontainer-demo1
    ```
    
3.  按下 `F1` > `Remote-Containers: Add Development Container Configuration Files`
    
4.  先選擇一個 Dev Container 範本: `C# (.NET) and MS SQL`
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/63c67695252e431ee4fc-183288811-bb3220cc-c966-4a07-bc4f-6db11812d670.png)
    
5.  選擇 .NET 版本: `6.0-focal`
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f13babad5c0d979fc373-183288824-d62fce33-2a8b-4c43-9f0a-de39b4605478.png)
    
6.  選擇 Node.js 版本: `none`
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/75f9e9cc59d74aad446f-183288831-ba17fa09-223a-43c1-a27d-7d6caee51df1.png)
    
7.  選擇要在容器中使用的工具: 這裡我們先不選，可直接按下 `OK` 完成設定
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/8168961ff0be4d6a2852-183288845-790212ae-8c6c-44f4-87cc-35788af609a5.png)
    
    這樣就設定完成了！ 👍
    
8.  接著我們進行 Git 版控
    
    為了要進行版控，我先加入一個 .NET 專用的 `.gitignore` 檔案：
    
    ```bash
    dotnet new gitignore
    ```
    
    下圖是最終版本，基本上每個檔案你都可以點開來看：
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f66e0e5df13ddc25ef1f-183289657-0efbcdc5-54b1-4c9c-86a9-9314409a750d.png)
    
    建立版控
    
    ```bash
    git init
    git add .
    git commit -m "Initial commit"
    ```
    
9.  執行 `F1` > `Remote-Containers: Reopen in Container` 即可自動啟動
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/868564f19994cfbd8ddf-183290012-053c0100-9a01-46ed-a3d6-72bc80e53dc4.png)
    
    這個動作的背後，會全自動建置所有需要的容器映象(image)，然後自動啟動，自動初始化容器環境 (安裝必要的開發工具)，開啟必要的網路連線埠轉向(Port Forwarding)，然後就可以很順利的讓你在 Visual Studio Code 連接到 Container 容器中進行開發、測試、執行。
    

### 體驗在版控之後發生的問題

由於專案中的 `.devcontainer/Dockerfile` 檔案會負責建置 Dev Container 開發容器中的所有環境，其中包含了 [SQL 相關工具](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup-tools?view=sql-server-ver16&WT.mc_id=DT-MVP-4015686) 如 `sqlcmd`, `bcp`, `sqlpackage` 等等。

這個 `.devcontainer/Dockerfile` 內容如下：

```dockerfile
#!/bin/bash
# [Choice] .NET version: 6.0-focal, 3.1-focal
ARG VARIANT="6.0-focal"
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT}

# [Choice] Node.js version: none, lts/*, 18, 16, 14
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

# Install SQL Tools: SQLPackage and sqlcmd
COPY mssql/installSQLtools.sh installSQLtools.sh
RUN bash ./installSQLtools.sh \
     && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
#     && apt-get -y install --no-install-recommends <your-package-list-here>

# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
```

你可以發現到他有將 `mssql/installSQLtools.sh` 複製到容器中執行，重點是微軟所提供的 Dev Container 開發用容器是以 `Ubuntu 20.04.4 LTS` 為主要作業系統，而在執行這段命令時，會要求所有的 Shell Script 必須以 `LF` 為主要斷行符號，因此 Windows 常見的 `CRLF` 斷行符號是無法使用的，建置容器映像的過程會發生錯誤！

接著我們就來看一下這個發生錯誤的過程：

1.  容器中並不會包含專案原始碼
    
    事實上，微軟的 Dev Container 會用 volumn mapping 的方式將原始碼掛載到容器中使用，因此你只有「專案執行環境」在容器中而已！
    
2.  判斷 `~/.gitconfig` 設定
    
    我們一開始在建立容器映像的時候，他預設會將 Windows 本機的 `~/.gitconfig` 全域設定檔，自動複製到容器環境下的 `~/.gitconfig`，因此基本上在 `Ubuntu 20.04.4 LTS` 底下的 Git 設定是跟 Windows 完全相同的。
    
    但是 Windows 底下有個 `core.autocrlf=true` 的預設值，這意味著當所有檔案被**取出**(Checkout)的時候，所有**文字檔案**都會自動以 `CRLF` 來當成換行字元，但這個問題就大了！
    
3.  重新取出原始碼
    
    由於 `.devcontainer/mssql/installSQLtools.sh` 檔案剛開始是以 `LF` 來保存的，只要重新取出原始碼，他就會自動轉成 `CRLF` 換行字元，所以我們可以預期 `.devcontainer/Dockerfile` 在進行建置時會出現錯誤！
    
    你可以試著將 `.devcontainer` 整個移除，然後用 `git reset --hard` 重新取出檔案。
    
4.  執行 `F1` > `Remote-Containers: Reopen Folder Locally` 重新回到 Windows 開啟此專案
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/c516ef628a80699cbc10-183290951-924fd4ee-92f8-4e8a-817e-e88e4d820ece.png)
    
5.  執行 `F1` > `Rebuild Without Cache and Reopen in Container` 重建 Dev Container 的容器映像
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/06062c78f7ddc50cdb32-183291052-5851a9c2-88ba-4fd3-95a2-916290a03e54.png)
    
    此時你就可以看到錯誤訊息了：
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/2a44d2a477b9fc2ad044-183291117-fd3c0c64-d41c-4498-8d52-faff8f8d834c.png)
    
    如果從 **Terminal** (終端機) 來看，你可以看到相對詳細的錯誤訊息：
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/e4eaeb006caa5a5ba218-183291162-8a9a4fe1-61c4-46ca-858d-2e301e68e976.png)
    
    發生問題的當下，第一時間並不是很好理解，但你現在應該很清楚是換行符號造成的錯誤！
    

### 正確的解決方案

這個問題最標準的解決方法如下：

1.  執行 `F1` > `Remote-Containers: Reopen Folder Locally` 重新回到 Windows 開啟此專案
    
2.  在 `.devcontainer/mssql/` 目錄下建立一個 `.gitattributes` 檔案，並加入以下內容：
    
    ```
    *.sh    text eol=lf
    ```
    
    如此一來，就可以確保該目錄下所有的 `*.sh` 檔案，在 Git 取出檔案時一定會使用 `LF` 當成換行字元，
    
3.  刪除 `.devcontainer/mssql/*.sh` 檔案，並使用 `git checkout` 或 `git reset --hard` 重新取出檔案
    
    ```bash
    # 一定要先刪除檔案
    rm .devcontainer/mssql/*.sh
    
    # 刪除後執行取出檔案作業
    git checkout -- .devcontainer/mssql/*.sh
    ```
    
4.  執行 `F1` > `Rebuild Without Cache and Reopen in Container` 重建 Dev Container 的容器映像
    
    這次應該又可以順利建立容器了！ 👍
    

### 總結

這個問題其實不太會在 macOS 或 Linux 發生，只有 Windows 用戶會遇到這個狀況，這不經讓我懷疑，是不是微軟的工程師沒有人在用 Windows 了啊？ 😅

我特別為了這個問題也到 [microsoft/vscode-remote-release](https://github.com/microsoft/vscode-remote-release/issues/7030) 開了一個 [Issue #7030](https://github.com/microsoft/vscode-remote-release/issues/7030) 追蹤此議題，我認為這個檔案只要從微軟的範本中加入，就不會對 Windows 用戶帶來困擾了！

不過，我還是覺得只要你跟我一樣是愛用 Windows 的開發人員，都應該理解 `CRLF` 與 `LF` 的差異，這樣日後在容器化的過程中，也會少踩一些地雷。

### 相關連結

-   [Git - gitattributes Documentation](https://git-scm.com/docs/gitattributes)
-   [CRLF - MDN Web Docs Glossary: Definitions of Web-related terms | MDN](https://developer.mozilla.org/en-US/docs/Glossary/CRLF)
-   [Create a development container](https://code.visualstudio.com/docs/remote/create-dev-container)
-   [Developing inside a Container using Visual Studio Code Remote Development](https://code.visualstudio.com/docs/remote/containers)
-   [.devcontainer\\mssql\\installSQLtools.sh fail to run when building images on Windows (Git issue) · Issue #7030 · microsoft/vscode-remote-release](https://github.com/microsoft/vscode-remote-release/issues/7030)
