본문 바로가기

Software

Docker 사용법 및 다양한 옵션 설명

1 Introduction

본 포스트에서는 Docker의 사용법 및 다양한 옵션들에 대해 설명한다. Docker는 컨테이너라는 가상의 이미지를 생성하여 호스트 컴퓨터와 리소스는 공유하지만 별도의 독립적인 프로그램을 돌릴 수 있는 가상화 프로그램이다. 리눅스를 사용하면서 버전 문제로 인해 빌드에 실패해 본 경험이 있는 사용자들에게 Docker를 추천한다.

본 포스트에서 설명하는 모든 내용은 우분투 18.04 LTS 버전에서 정상적으로 작동하였으며 사용한 docker의 버전은 19.03.06 버전이다.

2 Environment setup

Ubuntu 18.04 LTS 환경에서 docker는 다음 명령어를 통해 간단하게 설치할 수 있다.

$ sudo apt install docker*

2.1 Use docker without sudo command

우분투에서 docker를 처음 설치하고 실행하면 매번 실행할 때마다 sudo 명령어를 입력해줘야 한다. 이를 sudo 명령어없이 편하게 사용하려면 현재 계정을 docker 그룹에 포함시키면 된다. 명령어는 아래와 같다.

# Add user to docker group.
$ sudo usermod -aG docker ${USER}

# Restart docker service.
$ sudo service docker restart

# 현재 계정을 로그아웃 또는 재부팅하고 재접속하면 적용된다

2.2 nvidia-docker 

nvidia-docker 는 호스트 컴퓨터에 Nvidia GPU가 장착되어 있을 경우 컨테이너에서 해당 GPU를 사용할 수 있도록 해주는 프로그램이다. 2019년 하반기에 업데이트된 docker 19.03.5 버전부터 자체적으로 nvidia-docker 명령어를 흡수하면서 --gpus 옵션으로 보다 간단하게 사용할 수 있게 되었다. --gpus 옵션을 사용하기 위해서는 nvidia-container-runtime 이라는 프로그램을 설치해야 한다.

$ curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey |  sudo apt-key add -

# distribution is ubuntu18.04
$ distribution=$(. /etc/os-release;echo $ID$VERSION_ID)

$ curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list

# Install nvidia-container-runtime
$ sudo apt update
$ sudo apt install nvidia-container-runtime

# Check it's installed successfully.
$ nvidia-container-runtime-hook --help

# Restart docker daemon.
$ sudo systemctl restart docker

정상적으로 nvidia-container-runtime 이 설치한 후 docker 서비스를 재시작하였다면 다음과 같이 --gpus all 을 통해 호스트 컴퓨터의 GPU를 사용할 수 있다.

# -e NVIDIA_VISIBLE_DEVICES=all 을 입력하면 non-CUDA 이미지에서도 gpu를 사용할 수 있다.
$ docker run \
  --gpus all \
  -it \
  --name {container name} \  # <= your container name here
  {image name}               # <= your docker image name here

만약 non-cuda 이미지에서 GPU를 사용하고 싶을 경우 아래와 같은 명령어를 사용하면 된다.

# -e NVIDIA_VISIBLE_DEVICES=all 을 입력하면 non-CUDA 이미지에서도 gpu를 사용할 수 있다.
$ docker run \
  --gpus all \
  -e NVIDIA_VISIBLE_DEVICES=all \
  -it \
  --name {container name} \  # <= your container name here
  {image name}               # <= your docker image name here

nvidia-docker 를 통해 정상적으로 컨테이너가 실행된 경우 다음의 명령어를 통해 GPU가 정상적으로 인식됐는지 확인할 수 있다.

$ nvidia-smi

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 4xx.xx.xx    Driver Version: 4xx.xx.xx    CUDA Version: xx.x     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce ..........  Off  | 00000000:01:00.0 Off |                  N/A |
| 26%   34C    P8     1W / 250W |    209MiB / 11018MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

2.3 Autocomplete terminal command using tab

(필자가 테스트해본 결과 Ubuntu 18.04 LTS에서 설치한 docker 19.03.06 버전은 별도의 과정없이 자동완성이 되는 것을 확인하였다. 만약 자동완성이 되지 않는 경우 아래 과정을 진행하면 된다)

만약 자신의 docker 내부 bash에서 자동완성이 되지 않는 경우 아래 절차대로 실행하면 자동완성이 설정된다. 하지만, 아래 코드를 수행해도 apt 관련 자동완성은 되지 않았다. apt 명령어 자동완성을 제외한 다른 자동완성들은 정상적으로 작동하였다.

$ apt install bash-completion

# download bash completion file into /etc/bash_completion.d/ 
$ curl https://raw.githubusercontent.com/docker/docker-ce/master/components/cli/contrib/completion/bash/docker -o /etc/bash_completion.d/docker.sh

# /etc/.bash.bashrc 파일을 열고 아래 코드를 주석 해제한다.
$ vim /etc/bash.bashrc

if ! shopt -oq posix; then
    if [ -f /usr/share/bash-completion/bash_completion ]; then
        . /usr/share/bash-completion/bash_completion
    elif [ -f /etc/bash_completion ]; then
        . /etc/bash_completion
    fi
fi

# ~/.bashrc 에도 비슷한 completion 코드가 있는데 해당 코드를 주석 해제한다.
if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
    . /etc/bash_completion
fi

# 변경사항을 적용한다. 
$ source /etc/bash.bashrc
$ source ~/.bashrc

2.4 Internet connection in container

(docker 19.03.06 버전에서는 –net=host 옵션만 사용해도 인터넷 사용이 가능하였다. 만약 인터넷이 정상적으로 동작하지 않는 경우 아래 과정을 진행하면 된다)

Docker 컨테이너는 NAT 기술을 사용하여 외부 네트워크로 통신한다. 실제 인터넷 선은 하나 밖에 제공되어 있지 않고 그 선을 통해 데이터가 나가야 하기 때문에 Linux의 iptables 명령을 사용해서 해당 브릿지 인터페이스에 MASQUERADE 기능을 부여하거나 SNAT 값을 줘야 한다.

# host 머신에서 아래 명령어를 실행한다. 아래 명령어를 통해 iptables에 NAT 정보를 입력한다
$ sudo iptables -t nat -A PREROUTING -d 0.0.0.0/0 -m addrtype --dst-type LOCAL -j DOCKER
$ sudo iptables -t nat -A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER
$ sudo iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -j MASQUERADE

# 명령어가 정상적으로 작동했는지 확인한다.
$ sudo iptables -t nat -L

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
DOCKER     all  --  anywhere            !127.0.0.0/8          ADDRTYPE match dst-type LOCAL
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
MASQUERADE  all  --  172.17.0.0/16        anywhere            
MASQUERADE  all  --  172.17.0.0/16        anywhere            

Chain DOCKER (4 references)
target     prot opt source               destination         
RETURN     all  --  anywhere             anywhere  

3 Useful command

docker에는 많은 명령어들이 존재하지만 해당 섹션에서는 필자가 자주 사용하는 docker 명령어들을 위주로 정리하였다. Control 키바인딩은 <C-{...}> 로 표시하였다.

명령어 설명에 앞서서 docker의 용어에 대해 간단히 설명해보면 이미지(=Image) 는 docker에서 일종의 실행프로그램(.exe) 로 볼 수 있다. 이미지는 Dockerfile이라는 파일을 사용하여 빌드할 수 있으며 빌드한 후에도 commit 명령어를 통해 변경사항을 업데이트할 수 있다. 이미지 를 실행하면 이를 컨테이너(Container) 라고 하는데 컨테이너는 실행프로그램(.exe) 이 실행된 프로세스(process) 로 생각하면 된다. 컨테이너 에서 수행하는 작업은 이미지 에 영향을 미치지 않지만 추후 commit을 통해 이미지에 변경사항을 적용할 수 있다.

3.1 command

3.1.1 ps

현재 실행 중인 컨테이너 목록을 검색한다. -a 옵션을 사용하면 종료된 컨테이너 목록까지 모든 목록을 출력한다

$ docker ps -a

3.1.2 images

호스트에 설치된 이미지 목록을 검색한다

3.1.3 rm

특정 컨테이너를 제거한다. 단, 현재 실행중인 컨테이너는 지울 수 없다

3.1.4 rmi

특정 이미지를 제거한다. 현재 실행중인 컨테이너가 의존하는 이미지는 지울 수 없다

3.1.5 attach

현재 실행 중인 컨테이너에 접속한다

3.1.6 run

이미지를 컨테이너로 실행한다. 일반적으로 run 명령어는 많은 옵션이 존재하기 때문에 터미널에서 실행하지 않고 쉘스크립트로 작성한 다음 실행한다. 아래는 필자가 사용하는 run 예시 파일이다.

#!/bin/sh
XSOCK=/tmp/.X11-unix
XAUTH=${HOME}/.Xauthority

xhost +local:docker

docker run \
       --net=host \
       --name scratch \
       -it \
       --env="XAUTHORITY=${XAUTH}" \
       --env="DISPLAY=unix${DISPLAY}" \
       --env="XDG_RUNTIME_DIR=/run/user/1000" \
       --privileged  \
       -v /run/user/1000:/run/user/1000 \
       -v /dev/bus/usb:/dev/bus/usb \
       -v ${XSOCK}:${XSOCK}:rw \
       -v ${XAUTH}:${XAUTH}:rw \
       -v ${HOME}/share_docker:/root/share_docker \
       -v /media/data/dataset:/root/dataset \
       --expose 22 \
       edward0im/1804:scratch
# -i: interactive, 표준입력(stdin)을 활성화하여 bash에 명령을 입력한다
# -t: pseudo-tty, bash를 사용하려면 해당 옵션을 사용해야 한다
# --net: 컨테이너의 네트워크 모드를 선택한다. host를 사용하면 컨테이너 안에서 호스트의 네트워크를 그대로 사용한다
# -v: 호스트의 디렉토리와 공유할 디렉토리를 입력한다 {host directory}:{docker directory}
# --name : 컨테이너의 이름을 설정한다

3.1.7 commit

현재 컨테이너의 변경사항을 이미지로 저장한다. git의 commit 명령어와 유사한 기능이다

# 작성자(-a)는 Author이고 메세지(-m)는 Initial commit이다
$ docker commit -a "Author" -m "Initial commit" {container name} {image name}

3.1.8 build

Dockerfile 파일을 이미지로 빌드한다

3.1.8.1 Dockerfile

Dockerfile 파일은 docker 이미지를 빌드할 수 있는 파일이다. Dockerfile의 문법에 맞춰서 작성하면 build 명령을 통해 원하는 이미지를 만들 수 있다. 아래는 필자가 사용하는 Dockerfile 파일 중 하나로써 Ubuntu 18.04와 ROS를 자동으로 설치해준다.

# this Dockerfile creates an Ubuntu 18.04 environment with ROS melodic.
FROM ubuntu:18.04
ENV DEBIAN_FRONTEND noninteractive

# install dependencies via apt
ENV DEBCONF_NOWARNINGS yes
RUN set -x && \
    apt update -y -qq && \
    apt upgrade -y -qq --no-install-recommends && \
    : "basic dependencies" && \
    apt install -y -qq build-essential pkg-config cmake git wget curl tar unzip && \
    : "g2o dependencies" && \
    apt install -y -qq libgoogle-glog-dev libatlas-base-dev libsuitesparse-dev && \
    : "OpenCV dependencies" && \
    apt install -y -qq libgtk-3-dev libjpeg-dev libpng++-dev libtiff-dev libopenexr-dev libwebp-dev ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavresample-dev && \
    : "Pangolin dependencies" && \
    apt install -y -qq libglew-dev && \
    : "other dependencies" && \
    apt install -y -qq libyaml-cpp-dev && \
    : "remove cache" && \
    apt autoremove -y -qq && rm -rf /var/lib/apt/lists/*

ARG CMAKE_INSTALL_PREFIX=/usr/local
ARG NUM_THREADS=7

ENV CPATH=${CMAKE_INSTALL_PREFIX}/include:${CPATH}
ENV C_INCLUDE_PATH=${CMAKE_INSTALL_PREFIX}/include:${C_INCLUDE_PATH}
ENV CPLUS_INCLUDE_PATH=${CMAKE_INSTALL_PREFIX}/include:${CPLUS_INCLUDE_PATH}
ENV LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib:${LIBRARY_PATH}
ENV LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib:${LD_LIBRARY_PATH}

ENV NVIDIA_VISIBLE_DEVICES ${NVIDIA_VISIBLE_DEVICES:-all}
ENV NVIDIA_DRIVER_CAPABILITIES ${NVIDIA_DRIVER_CAPABILITIES:+$NVIDIA_DRIVER_CAPABILITIES,}graphics

# Eigen
ARG EIGEN3_VERSION=3.3.7
WORKDIR /tmp
RUN set -x && \
    wget -q https://gitlab.com/libeigen/eigen/-/archive/${EIGEN3_VERSION}/eigen-${EIGEN3_VERSION}.tar.bz2 && \
    tar xf eigen-${EIGEN3_VERSION}.tar.bz2 && \
    rm -rf eigen-${EIGEN3_VERSION}.tar.bz2 && \
    cd eigen-${EIGEN3_VERSION} && \
    mkdir -p build && \
    cd build && \
    cmake \
        -DCMAKE_BUILD_TYPE=Release \
        -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX} \
        .. && \
    make -j${NUM_THREADS} && \
    make install && \
    cd /tmp && \
    rm -rf *
ENV Eigen3_DIR=${CMAKE_INSTALL_PREFIX}/share/eigen3/cmake

# ROS
# install packages
RUN apt update && apt install -q -y \
                              dirmngr \
                              gnupg2 \
    && rm -rf /var/lib/apt/lists/*

# setup keys
RUN apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654

# setup sources.list
RUN echo "deb http://packages.ros.org/ros/ubuntu bionic main" > /etc/apt/sources.list.d/ros1-latest.list

# install bootstrap tools
RUN apt update && apt install --no-install-recommends -y \
                              python-rosdep \
                              python-rosinstall \
                              python-vcstools \
    && rm -rf /var/lib/apt/lists/*

# setup environment
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8

# bootstrap rosdep
RUN rosdep init \
    && rosdep update

# install ros packages
ENV ROS_DISTRO melodic
RUN apt update && apt install -y \
                              ros-melodic-ros-core \
                              ros-melodic-desktop-full \
    && rm -rf /var/lib/apt/lists/*


# run opengl program (e.g., rviz) on docker 
RUN apt update && apt install -y --no-install-recommends \
                              pkg-config \
                              libxau-dev \
                              libxdmcp-dev \
                              libxcb1-dev \
                              libxext-dev \
                              libx11-dev && \
    rm -rf /var/lib/apt/lists/*

COPY --from=nvidia/opengl:1.0-glvnd-runtime-ubuntu18.04 \
     /usr/lib/x86_64-linux-gnu \
     /usr/lib/x86_64-linux-gnu

COPY --from=nvidia/opengl:1.0-glvnd-runtime-ubuntu18.04 \
     /usr/share/glvnd/egl_vendor.d/10_nvidia.json \
     /usr/share/glvnd/egl_vendor.d/10_nvidia.json

RUN echo '/usr/lib/x86_64-linux-gnu' >> /etc/ld.so.conf.d/glvnd.conf && \
    ldconfig && \
    echo '/usr/lib/x86_64-linux-gnu/libGL.so.1' >> /etc/ld.so.preload && \
    echo '/usr/lib/x86_64-linux-gnu/libEGL.so.1' >> /etc/ld.so.preload

# Finish-----------------
WORKDIR /root
ENTRYPOINT ["/bin/bash"]

위와 같이 Dockerfile을 작성하고 파일이 존재하는 폴더에서 다음 명령을 실행하면 빌드된다

# -t(--tag) 옵션으로 이미지 이름과 태그를 설정할 수 있다. 이미지 이름만 설정하면 태그는 latest로 설정된다
$ docker build -t build_test -f Dockerfile .

# 아래 명령어로 이미지 목록을 출력한다
$ docker images

# run 명령을 통해 이미지를 실행할 수 있다
$ docker run \
  --name test \
  sample:0.1

3.1.9 push, pull

git의 pull, push와 동일하게 원격 hub로부터 이미지를 가져오거나 저장한다

3.1.9.1 Docker hub

docker도 github처럼 저장소 사이트가 존재한다. 해당 사이트에 가입하고 Pull, Push 명령어를 통해 원격저장소와 데이터를 주고 받을 수 있다

# Docker hub 계정 로그인
$ docker login

# 로컬에 존재하는 이미지를 원격저장소에 Push한다
$ docker push {user ID}/{image name}:{tag}    # e.g., edward0im/1804:scratch

# 원격에 존재하는 이미지를 로컬저장소에 Pull한다.
$ docker pull {user ID}/{image name}:{tag}

3.1.10 <C-p> <C-q>

docker 컨테이너를 종료하지 않은 상태로 빠져나온다. 로 종료한 컨테이너는 백그라운드에서 계속 실행 상태에 있으며 다시 attach 명령을 통해 접속할 수 있다. 만약 컨테이너를 종료하고 싶으면 키를 누르면 된다.

4 References

  1. 도커 무작정 따라하기
  2. docker 컨테이너에서 GPU 사용 - ykarma1996 blog
  3. Docker 네트워크 구성과 설정 - neonkid blog
  4. 가장 빨리 만나는 Docker 20장, 28. run - pyrasis blog