###### tags: `tutorials` `docker` `ubuntu` `linux` `cuda` `GPU` `NVIDIA` `python` `deep learning` `AI`
# Dockerfile 建立教學
## Introduction
`Dockerfile` 其實就像是用很多 *linux* 指令的組合來構建想要的系統 `image`,一般我們都會用 [DockerHub](https://hub.docker.com/) 上現存的 `image` 在網上建構,最常用的官方 `image` 有 [ubuntu](https://hub.docker.com/_/ubuntu),還有近年因機器學習而竄起的 [nvidia cuda](https://hub.docker.com/r/nvidia/cuda)
## Example
:::warning
詳細指令內容請看[官方教學](https://docs.docker.com/engine/reference/builder/),接下來只會大概介紹 `FROM`、`ARG`、`WORKDIR`、`RUN`
:::
接下來用以下[我寫的 nvidia cuda Dockerfile](https://hub.docker.com/r/jimmy801/nvidia_cuda-base) 講解
:::warning
以下文件結尾的 `\` 是表示換行接續符
:::
```bash=
# you can use `--build-arg CUDA=<cuda_version>` to specify pyton version
# use `--build-arg CUDA=10.0-cudnn7-runtime-ubuntu18.04` to set cuda10 w/ cudnn7 as docker cuda
# you can find cuda versions on: https://hub.docker.com/r/nvidia/cuda/tags
# NOTE: default `CUDA` is `10.0-cudnn7-runtime-ubuntu18.04`
# you can use `--build-arg PYTHON_VERSION=<version_num>` to specify pyton version
# use `--build-arg PYTHON_VERSION=3.5` to set python3.5 as docker default python version
# NOTE: default `PYTHON_VERSION` is `3`
# you can use `--build-arg TZ=<timezone>` to specify timezone
# use `--build-arg TZ=Asia/Taipei` to set Asia/Taipei as docker default timezone
# you can find timezones on: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# NOTE: default `TZ` is `Asia/Taipei`
# NOTE: all arg set need to add prefix `--build-arg`
# that is to say, if you want to specify cuda version, python version, and timezone
# you have to write
# `--build-arg CUDA=<cuda_version> --build-arg PYTHON_VERSION=<version_num> --build-arg TZ=<timezone>`
# to do this
ARG CUDA=10.0-cudnn7-runtime-ubuntu18.04
FROM nvidia/cuda:$CUDA
ARG PYTHON_VERSION=3
ARG TZ=Asia/Taipei
WORKDIR home
# set timezone
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# install packages
RUN apt-get update && \
apt-get install -y \
cmake \
wget \
curl \
git \
vim \
software-properties-common \
libgl1-mesa-glx libsm6 libxrender1 libxext-dev
# register python dependency(ppa)
# NOTE: Register ppa may take more time
# More info: https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa
# Note: Python2.7 (all),
# Python 3.5 (16.04, xenial), Python 3.6 (18.04, bionic), Python 3.8 (20.04, focal)
# are not provided by deadsnakes as upstream ubuntu provides those packages
# (it means they don't need to register ppa)
RUN ubuntu=$(lsb_release -r | grep "Release:") && ubuntu=${ubuntu##*:} && \
if [ "$PYTHON_VERSION" != "2.7" ] && [ "$PYTHON_VERSION" != "3" ] && \
[ \( ${ubuntu} = "16.04" -a "$PYTHON_VERSION" != "3.5" \) -o \
\( ${ubuntu} = "18.04" -a "$PYTHON_VERSION" != "3.6" \) -o \
\( ${ubuntu} = "20.04" -a "$PYTHON_VERSION" != "3.8" \) ]; then \
add-apt-repository ppa:deadsnakes/ppa && \
apt-get update; \
fi
# install specific python version
RUN apt-get install -y \
python$PYTHON_VERSION \
python$PYTHON_VERSION-dev && \
# set default `python` to `python$PYTHON_VERSION`
ln -sf /usr/bin/python$PYTHON_VERSION /usr/bin/python
# install pip
RUN if [ $PYTHON_VERSION \> 3 ]; then \
apt-get install -y python3-distutils-extra && \
ln -sf /usr/bin/python$PYTHON_VERSION /usr/bin/python3; \
fi && \
curl -O https://bootstrap.pypa.io/get-pip.py && \
python get-pip.py && \
rm get-pip.py
# uncomment it to not use default color prompt,
# replace PS1 2nd occurence line (PS1 color setting line)
# RUN sed -i '0,/PS1.*/! {0,/PS1.*/ s/PS1.*/'"\ PS1=\'\$\{debian_chroot:\+\(\$debian_chroot\)\}\\\[\\\033\[01;32m\\\]\\\u\\\[\\\033\[00;37m\\\]@\\\[\\\033\[01;35m\\\]\\\h\\\[\\\033\[00m\\\]:\\\[\\\033\[01;34m\\\]\\\w\\\[\\\033\[00m\\\]# \'"'/}' ~/.bashrc
# set ~/.bashrc
RUN sed -i 's/^#force_color_prompt=yes/force_color_prompt=yes/' ~/.bashrc && \
# above enable color prompt of docker in terminal
# set alias
echo "alias nv='nvidia-smi'" >> ~/.bash_aliases && \
echo "alias wnv='watch -n 1 nvidia-smi'" >> ~/.bash_aliases && \
echo "alias wwnv='watch -n 0.1 nvidia-smi'" >> ~/.bash_aliases
```
---
### ARG
```bash=21 !
ARG CUDA=10.0-cudnn7-runtime-ubuntu18.04
```
```bash=23 !
ARG PYTHON_VERSION=3
```
```bash=24 !
ARG TZ=Asia/Taipei
```
`21` 行、`23` 行及 `24` 行都是 `ARG` 變數,這種變數是只會存在於 `Dockerfile` 中,並不會留到之後的 `image` 或是 `container` 中,並且可以在使用 `Dockerfile` 建立 `image` 時使用而外的 `--build-arg {ARG_param_name}={ARG_value}` 來改變 `ARG` 變數的值
:::info
也就是 `CUDA` 變數預設為 `10.0-cudnn7-runtime-ubuntu18.04`,而 `PYTHON_VERSION` 變數預設為 `3`,可以在建立`image` 時加上 `--build-arg CUDA=9.0-cudnn7-runtime-ubuntu16.04`、`--build-arg PYTHON_VERSION=3.8` 來將預設值改變
> 更多建立 `image` 的教學請看[這裡](https://hackmd.io/@NCUmsplab/ByZVjKwnD?view#Create-image)
:::
:::warning
`runtime` 版本是輕量版的 nvidia-cuda image,如果要更完整版請使用 `devel` 版本
> 例如 `10.0-cudnn7-`**`devel`**`-ubuntu18.04`
:::
---
### FROM
```bash=22 !
FROM nvidia/cuda:$CUDA
```
`docker` 基本上就像是疊積木,簡單來說 `FROM` 就是決定你要從哪個 `base image` 開始疊起,而這裡是使用 `nvidia/cuda` repository 下的 `$CUDA` tag 的 `image`,而這裡的 `$CUDA` 指的是上面 `ARG` 變數的 `CUDA` 值(預設為`10.0-cudnn7-runtime-ubuntu18.04`)
:::info
依照預設值展開,這行就等價於 `FROM nvidia/cuda:10.0-cudnn7-runtime-ubuntu18.04`
:::
---
### WORKDIR
```bash=26 !
WORKDIR home
```
`WORKDIR` 是拿來設定 `RUN`, `CMD`, `ENTRYPOINT`, `COPY` 和 `ADD` 指令的相對路徑,也是預設進入的路徑,若是不存在該路徑則會自動建立
---
### RUN
`RUN` 就是執行某個 `shell` 指令
#### `29 ~ 31` 行
```bash=29 !
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
```
- apt-get update: 更新套件
- apt-get install: 安裝套件
- `-y`: 安裝時預設 `yes`,不用詢問
- 這裡是設定時區的一些必要設置
#### `34 ~ 42` 行
```bash=34 !
RUN apt-get update && \
apt-get install -y \
cmake \
wget \
curl \
git \
vim \
software-properties-common \
libgl1-mesa-glx libsm6 libxrender1 libxext-dev
```
- apt-get update: 更新套件
- apt-get install: 安裝套件
- `-y`: 安裝時預設 `yes`,不用詢問
- cmake: cmake 編譯器套件
- wget: wget 下載指令套件
- curl: curl 下載指令套件
- git: git 套件
- vim: 文字編輯套件
- software-properties-common: 額外可添加套件
- 剩餘的是一些 opencv 所需的必要套件
#### `51 ~ 58` 行
```bash=51 !
RUN ubuntu=$(lsb_release -r | grep "Release:") && ubuntu=${ubuntu##*:} && \
if [ "$PYTHON_VERSION" != "2.7" ] && [ "$PYTHON_VERSION" != "3" ] && \
[ \( ${ubuntu} = "16.04" -a "$PYTHON_VERSION" != "3.5" \) -o \
\( ${ubuntu} = "18.04" -a "$PYTHON_VERSION" != "3.6" \) -o \
\( ${ubuntu} = "20.04" -a "$PYTHON_VERSION" != "3.8" \) ]; then \
add-apt-repository ppa:deadsnakes/ppa && \
apt-get update; \
fi
```
這段是比較有條件的指令
:::spoiler `39` 行
##### 確認 `ubuntu` 版本,因為 `CUDA` 參數是在 `FROM` 之前,所以在之後會無法得知該參數的數值,因而要另外確認
- `ubuntu=$(lsb_release -r | grep "Release:")`: 首先使用 `lsb_release -r` 列出系統版本資訊,接著使用 `grep` 找出 `Release:` 部分(也就是釋出版本)
- `ubuntu=${ubuntu##*:}`: 將剛剛那行只取 `:` 之後的部分(版本號部分)
> `lsb_release -r` 輸出為
> ```
> Release: 18.04
> ```
>
> 而使用 `grep "Release:"` 後得到的值為 `Release:18.04`
> 使用 `ubuntu=${ubuntu##*:}` 會將其值只保留最後的 `18.04`
:::
:::spoiler `52` 行
##### 限制如果 `$PYTHON_VERSION` 不是 `2.7` 和 `3` 才需要進條件(預設不需要進此條件)
:::
:::spoiler `53 ~ 55` 行
##### 限制如果沒有依照各版本所配置的 `python` 版本才需要進條件(預設不需要進此條件)
:::info
(`ubuntu16.04` 且不是 `python3.5`) 或 (`ubuntu18.04` 且不是 `python3.6`) 或 (`ubuntu20.04` 且不是 `python3.8`)
:::
:::spoiler `56 ~ 57` 行
##### 條件執行內容
- `55` 行表示如果沒有要使用系統預設的 `python` 版本,則需要添加第三方套件(所以需要安裝 `41` 行的 `software-properties-common` 套件)
- `57` 行則是在剛剛添加完第三方套件後要執行更新讓系統知道有新的套件可安裝
:::
#### `61 ~ 65` 行
```bash=61 !
RUN apt-get install -y \
python$PYTHON_VERSION \
python$PYTHON_VERSION-dev && \
# set default `python` to `python$PYTHON_VERSION`
ln -sf /usr/bin/python$PYTHON_VERSION /usr/bin/python
```
- `61 ~ 63` 行都是安裝設定的 `python` 版本
- `65` 行則是把預設的 `python` 指令軟連結設為剛剛安裝的指定 `pyhton` 版本
#### `79` 行
```bash=79 !
# RUN sed -i '0,/PS1.*/! {0,/PS1.*/ s/PS1.*/'"\ PS1=\'\$\{debian_chroot:\+\(\$debian_chroot\)\}\\\[\\\033\[01;32m\\\]\\\u\\\[\\\033\[00;37m\\\]@\\\[\\\033\[01;35m\\\]\\\h\\\[\\\033\[00m\\\]:\\\[\\\033\[01;34m\\\]\\\w\\\[\\\033\[00m\\\]# \'"'/}' ~/.bashrc
```
- 使用 `sed` 修改色彩控制的 `PS1` 參數
- 是修改 `PS1` 在 `~/.bashrc` 檔案出現的第 2 次
- 原始 `PS1`
~~~bash= !
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
~~~
- 效果為 <span style="background:black"><font color="#4e9a06">{user_name}@{host_name}</font><font color="white">:</font><font color="#32afff">{work_path}</font><font color="white">$</font> </span>
- 修改為
~~~bash= !
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u\[\033[00;37m\]@\[\033[01;35m\]\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\# '
~~~
- 效果為 <span style="background:black"><font color="#4e9a06">{user_name}</font><font color="white">@</font><font color="#ad7fa8">{host_name}</font><font color="white">:</font><font color="#32afff">{work_path}</font><font color="white">#</font> </span>
- `\u`: user name,使用者名稱
- `\h`: host name,電腦名稱
- `\w`: work path,當前工作路徑
:::info
簡單來說就是
1. 在 `@` 之前加入 `\[\033[01;37m\]`
2. 在 `\h` 之前加入 `\[\033[01;35m\]`
:::
- 如果有想要使用修改後的顏色就把 `79` 行取消註解
#### `68 ~ 74` 行
```bash=68 !
RUN if [ $PYTHON_VERSION \> 3 ]; then \
apt-get install -y python3-distutils-extra && \
ln -sf /usr/bin/python$PYTHON_VERSION /usr/bin/python3; \
fi && \
curl -O https://bootstrap.pypa.io/get-pip.py && \
python get-pip.py && \
rm get-pip.py
```
- `python3-distutils-extra`: 安裝 `distutils` 避免一些 `python` 安裝錯誤
- `ln -sf /usr/bin/python$PYTHON_VERSION /usr/bin/python3`: 將 `python3` 軟連結設定為指定的 `python3` 版本
- `72 ~ 74` 行: 安裝 `pip`
#### `82 ~ 87` 行
```bash=82
RUN sed -i 's/^#force_color_prompt=yes/force_color_prompt=yes/' ~/.bashrc && \
# above enable color prompt of docker in terminal
# set alias
echo "alias nv='nvidia-smi'" >> ~/.bash_aliases && \
echo "alias wnv='watch -n 1 nvidia-smi'" >> ~/.bash_aliases && \
echo "alias wwnv='watch -n 0.1 nvidia-smi'" >> ~/.bash_aliases
```
- `82` 行: 將 `~/.bashrc` 中的 `#force_color_prompt=yes` 取代為 `force_color_prompt=yes` 來打開 `container` 的 `bash` 提示色
- `85` 行: 設定新指令 `nv`,該指令等價於後面的 `nvidia-smi` 指令
- `86` 行: 設定新指令 `wnv`,該指令等價於後面的 `watch -n 1 nvidia-smi` 指令,也就是每 `1` 秒執行並刷新 `nv` 指令
- `87` 行: 設定新指令 `wwnv`,該指令等價於後面的 `watch -n 0.1 nvidia-smi` 指令,也就是每 `0.1` 秒執行並刷新 `nv` 指令
:::warning
`alias` 要寫在 `~/.bash_aliases` 檔案內
:::