---
# System prepended metadata

title: Docker
tags: [Built Environment]

---


<center>
<image width=75% src=https://hackmd.io/_uploads/BJj4FrLDyg.png>
</center>

**Docker允許所有團隊成員在同一環境中建置和測試。** 有這麼好康 ??

在本機環境中處理多個 c++ 專案時，系統上會安裝各種程式庫像是 OpenCV, Open3D 等等。
接下來會遇到多個環境問題:
1. 開始新專案時,
    如果新專案的依賴項可以使用多個版本
    - 可能會有API衝突
    - 可能使用效能比較差的版本, **沒有人知道**.
2. 回顧舊專案時,
    - 從建立於 OpenCV3 的專案中休息一下... 再處理 OpenCV4 的專案... 如果再次回到這個 OpenCV3 專案怎麼辦？
        如果使用 ```find_package(OpenCV REQUIRED)```，沒有人知道會取得哪個版本的函式庫。
3. 團隊合作時,
    當某成員將新程式推上至 git, 建置於另一成員的電腦卻崩潰...
<br>
    
解決上述問題是非常惱人的,
1. 需要反覆確認函式庫版本是否相同, 開始進行一系列的清理... ~~砍掉linux重灌比較快~~
2. 需要確認每個成員都使用相同版本的函式庫...
 

<br>

使用 Docker 會變得很輕鬆，
1. 先建構多個函式庫 (i.g. OpenCV) 的基礎映像
    - 使用 Docker 在上預先安裝 clear Ubuntu 20.04，並建置多個函式庫(i.g. )
    - 將結果打包成映像，避免每次重複建構
2. 再去建構專案程式碼

## 基礎概念
1. Docker 是一種 Container 
    Container（容器） 是一種輕量級、可攜帶、獨立的軟體打包與運行技術。它將應用程式及其所有的相依環境（如程式庫、設定檔、依賴檔案）打包在一起，確保應用程式在不同的環境中都能一致地運作。
2. Virtual machine v.s. Docker
    與虛擬機(Virtual Machine)不同，Container 不需要內建完整的作業系統，它們直接共享主機（Host）的作業系統核心（Kernel），因此啟動速度更快、資源占用更少。
    
    <center>
    <image width=85% src=https://hackmd.io/_uploads/SkImRNvvye.png>
    </center>
    

## 安裝 Docker
1. 安裝方式
    :::spoiler 極速安裝(不推薦)
    Docker版本較舊，建議參考官方文件進行安裝。
    ```shell=
    sudo apt-get install docker.io
    ```
    :::

    :::spoiler 官方安裝懶人包, 建議使用[官方安裝方式](https://docs.docker.com/engine/install/ubuntu/)
    1. 移除 docker 舊版本
        ```shell=
        for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
        ```
    2. 安裝 docker by apt
        - 設定 docker 基本配備
            ```shell=
            # Add Docker's official GPG key:
            sudo apt-get update
            sudo apt-get install ca-certificates curl
            sudo install -m 0755 -d /etc/apt/keyrings
            sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
            sudo chmod a+r /etc/apt/keyrings/docker.asc

            # Add the repository to Apt sources:
            echo \
              "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
              $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
              sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
            sudo apt-get update
            ```
        - 安裝 docker 套件
            ```shell=
            sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
            ```
        - 驗證 docker
            ```shell=
            sudo docker run hello-world
            ```
            <image width=65% src=https://hackmd.io/_uploads/r1xcYMDjwJe.png>
    :::

2. 檢查 Docker 服務有無正常啟動
    ```shell=
    service docker status
    ```
    正常啟動會顯示 ```Active: active (running)```

3. 將自己使用者帳號加入 Docker 群組
    擺脫每次執行 docker 皆需要 ```sudo```.
    ```shell=
    sudo usermod -aG docker <username>
    ```
    重新登入 : 通常用戶組更改需要重新登入才會生效.
    ```shell=
    newgrp docker
    ```
## Docker : image v.s. container
- 映像檔 image
    image 是一種靜態模板, 包含應用程式及其所有依賴. 其內容不可被變更.
- 容器 container
    - container 是 image 的運行實例. 
    - container 建立時會疊加一個可寫層在 image 之上。任何檔案的新增、修改或刪除都發生在這個可寫層。一旦 container 停止或被刪除, 其可寫層也會損毀.
      通常會採用共享資料夾方式, 來避免資料損毀.
    - 可採用多個 container 共享一個 image

                
## 建立乾淨 Ubuntu 映像檔於 Docker
```shel=
# 建立乾淨 Ubuntu, 並在後台執行該容器
sudo docker run -dit ubuntu:latest
sudo docker run -dit ubuntu:20.04


# 查看正在執行的容器
sudo docker ps

# 列印所有映像檔
sudo docker images
```
<image width=65% src=https://hackmd.io/_uploads/HkWBDPivJx.png>


## 創建 Dockerfile 用於自動化建置
每次建立容器都得執行指令，會過於繁雜。我們可以透過 Dockerfile 來達成自動化創建環境作業(```touch Dockerfile```)。 

- 撰寫 Dockerfile
    該專案需要 opencv, c++, cmake.
    若 opencv 需要指定特定版本, 可以參考[opencv官網編譯流程](https://docs.opencv.org/4.5.3/d7/d9f/tutorial_linux_install.html#tutorial_linux_install_quick_build_contrib). opencv的cmake需要特別注意是否使用windows.
    ```shell=
    # Use Ubuntu 20.04 as the base image
    FROM ubuntu:20.04

    MAINTAINER <your name>
    LABEL maintainer="<your gmail>"

    # Avoid interactive prompts during installation
    ARG DEBIAN_FRONTEND=noninteractive

    # Install C++ development tools and dependencies
    RUN apt-get update && apt-get install -y \
        build-essential \
        cmake \
        git \
        wget \
        libopencv-dev \
        pkg-config \
        libeigen3-dev \
        libboost-all-dev \
        libopencv-core-dev \
        libopencv-highgui-dev \
        libopencv-imgproc-dev \
        libopencv-videoio-dev \
        libopencv-imgcodecs-dev \
        libopencv-calib3d-dev \
        libopencv-objdetect-dev \
        libopencv-features2d-dev \
        libopencv-contrib-dev \
        && apt-get clean
    
    # Intall GUI dependency
    RUN apt-get install -y \
	    x11-apps \
	    libgtk2.0-dev \
	    libcanberra-gtk-module \
	    libcanberra-gtk3-module

    # Create a working directory
    WORKDIR /app

    # Copy local code to the container's working directory
    COPY . /app

    # Create and build the CMake project
    RUN mkdir -p build && \
        cd build && \
        cmake .. && \
        make

    # Set the default command to run when the container starts
    CMD ["bash"]
    ```
 
- 建置專案之映像檔
    執行建置指令在包含```Dockerfile```的當前目錄, ```-t```是tag的意思.
    ```shell=
    docker build -t <your-image-name> . 
    # 查詢是否成功建置出 image
    docker images
    ```
    ```shell=
    # 自定義 Dockerfile 的名稱需要額外加入 -f
    docker build -t <your-image-name> -f <your-Dockerfile-name> .
    ```
    
    - 建立 arm64 於 x86 的 cross-compile 環境
    ```shell=
    docker buildx create --use
    docker buildx build \
      --platform linux/arm64 \
      -f dockerFile_ros1_rpy5 \
      -t kalibr-rpy5 \
      --load .
    ```

- 執行乘載專案映像檔之容器
    ```-i```是交互介面的意思, ```-t```開啟terminal.
    ```shell=
    docker run -it <your-image-name>
    ```

## 刪除 Docker 容器/映像檔
假如我們想刪除```<none>```標籤的映像檔 (dangling images), 通常是重複建置相同 image 名稱造成的。
<image width=65% src=https://hackmd.io/_uploads/SkiEzIQYke.png>

- 假使刪除**所有** ```<none>```標籤的映像檔
    ```shell=
    docker images -f "dangling=true" -q | xargs -r docker rmi
    ```
    如果容器停止但還沒刪除就會出現 : ```Error response from daemon: conflict: unable to delete 0e81ff9da00c (must be forced) - image is being used by stopped container 5ba8210f9b58```
    
    運行以下命令用來刪除已經停止的容器
    ```shell=
    docker container prune -f
    ```
    

## 如何取出資料從 Docker 容器
- 使用 docker cp 指令
    1. 查詢容器 ID 或名稱
        ```shell=
        docker ps
        ```
        會看到以下輸出：
        ```shell=
        CONTAINER ID   IMAGE           COMMAND   CREATED         STATUS         PORTS 　　NAMES
        a1b2c3d4e5f6   <your_image_name>  "bash"    10 minutes ago  Up 10 minutes        amazing_brown
        ```
        - container 名稱就會是```amazing_brown```.
        - image 名稱就是```<your_image_name>```.
    2. 從容器複製資料至本機端當前目錄
        ```shell=
        docker cp amazing_brown:/app/output_image.jpg ./output_image.jpg
        ```
- 使用共享資料夾
    啟動容器時掛載目錄 : 假設本機的目錄為 ```/home/images```, 在容器內，將影像儲存到 ```/app/output```.
    ```shell=
    docker run -it -v /home/images:/app/output <your_image_name>
    ```

## 如何於 Docker 容器中開啟 GUI 功能
1. 需要於主機端與容器端皆安裝 x11, RTX
    ```shell=
    apt-get install -y \
        x11-apps \
        xauth \
        libgtk2.0-dev \
        libcanberra-gtk-module \
        libcanberra-gtk3-module
    ```
    驗證安裝完成透過 ```xclock```
2. 主機端需確認 x11 相關權限
    - ```.Xauthority```
    - ```/tmp/.X11-unix```
3. 執行指令
    ```shell=
    docker run -it \
        -e DISPLAY=$DISPLAY \
        -e XAUTHORITY={Xauthority Path} \
        -v {Xauthority Path}:{Xauthority Path} \
        -v /tmp/.X11-unix:/tmp/.X11-unix \
        --net=host \
        {imageID_or_repo:Tag}
    
    xclock
    ```
    - ```{Xauthority Path}``` 可以藉由 ```ps aux | grep Xorg``` 獲得相關位置.
    - ```-e``` 設定 container 環境變數指令
        - DISPLAY=$DISPLAY: 將 container 內部的 DISPLAY 參數使用的顯示 port 設成和外部使用者同一個
        - XAUTHORITY={Xauthority Path}: 將 container 內部 X server 授權文件的 XAUTHORITY 參數設成和外部使用者同一個顯示授權文件
    - ```-v``` 外部授權相關文件掛載進 container
        - 將 ```{Xauthority Path}``` 掛載進 container 的 ```{Xauthority Path}```
        - 將 ```/tmp/.X11-unix``` 掛載進 container 的 ```/tmp/.X11-unix```
    
## 離開 Docker 
1. 退出容器, 但是讓容器於後台執行
    使用快捷键 ```Ctrl + P + Q```
2. 停止並退出容器
    同時這也會損毀可寫層
    ```shell=
    exit
    ```




