Docker 이미지 및 컨테이너 정리하기(1)

게으른 개발자·2025년 3월 1일

현재 재직중인 회사는 고객사에 솔루션 형태로 서비스를 제공하고있다. 해당 솔루션은 OS 환경에 매우 의존적이라 고객사의 OS 환경을 미리 전달받아 내부에서 런타임 환경을 구성하고 테스트를 진행한 뒤 고객사에 배포를 하고 있다. 그렇기에 Docker를 매우 자주 사용하는데 나를 제외한 다양한 개발자들이 다같이 테스트를 하다보니 Docker Image와 Container가 정리되지 않은 상태로 매우 많이 생성되어 있어 호스트 PC의 리소스도 불필요하게 많이 사용하고 있어서 이번에 정리해보고자 한다.

기존의 문제점

  1. 사용하는 사람이 많아 네이밍을 각자가 알아서 생성하고 관리하여 불필요한 리소스를 많이 사용
  2. 런타임용과 빌드용이 구분되어 있지만 각자의 용도에 맞게 최적화가 안되어있어 Docker Image의 용량이 매우 크다.
  3. Dockerfile이 재사용을 고려하지 않고 필요할 때마다 구성해서 사용

전제 조건

  1. 되도록 Podman을 사용하고 안된다면 Docker를 사용할 경우 rootless 모드를 사용
    • nvidia-docker를 필수적으로 설치!
    • ubuntu의 경우 podman을 지원하는 버전이 낮아 nvidia docker를 설치해도 nvidia 옵션을 사용할 수 없다! (하.... 이거때문에 하루동안 다른 일을 못했다.... 진작에 좀 알려주시죠 팀장님....ㅜㅜ)
  2. 솔루션의 빌드용 Docker 이미지고객사 테스트용 Docker 이미지를 분리
    • 테스트용일 경우 OS, GPU 사용 여부를 고려
  3. 이미지 생성에 필요한 Dockerfile을 재사용이 가능하도록 생성하여 Dockerfile이 재사용이 가능한 OS들을 묶어서 별도로 관리
    • 재사용이 가능한 OS들 이외에 재사용이 가능한 Dockerfile을 찾는다면 해당 OS도 같이 추가하여 같이 관리
  4. Docker Image 또는 Container 생성 시, 네이밍 규칙을 정해서 생성한다.
  5. Docker Image 생성시 필요한 파일들을 미리 COPY하는 것이 아닌 마운트를 걸어 관리
    • Build용 이미지 생성

빌드용 이미지 생성시 고려사항

  1. Java 버전
  2. GPU 사용 여부
    • Cuda(11.6) Toolkit
  3. Cmake
  4. C++

테스트용 이미지 생성시 고려사항

  1. Java 버전
  2. GPU 사용 여부
    • Cuda(11.6) Toolkit

테스트용 Dockerfile(예제)

  • 가장 많이 사용하는 테스트용 Dockerfile을 먼저 정리하였다.
  • 자사에서는 RHEL 계열의 OS를 권장하고 있어 RHEL 계열의 OS를 기준으로 예제를 작성한다.

변경 전

FROM centos:8.4.2105

RUN /usr/bin/sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-Linux-*
RUN /usr/bin/sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-Linux-*

# install centos package
RUN yum install -y {packages}
				   
# install CUDA toolkit
...

# user arguments
ARG username
ARG userid
ARG groupname
ARG groupid

# create user with specified uid & gid
RUN groupadd -g $groupid $groupname
RUN useradd -u $userid -g $groupname -m $username -s /bin/bash
RUN usermod -aG wheel $username
RUN echo "$username ALL=NOPASSWD: ALL" > /etc/sudoers.d/$username

# create directories
RUN mkdir /app
RUN chown $username:$groupname -R /app

WORKDIR /app

# add environments
...

USER $username:$groupname
  • 문제점
    1. centos 8.4 이미지 자체가 너무 무겁다.
    2. GPU 사용여부와 상관없이 CUDA Toolkit을 설치

변경 후

ARG IMAGE_TAG
FROM rockylinux/rockylinux:8.${IMAGE_TAG}-minimal

ADD ./amazon-corretto-8-x64-linux-jdk.tar.gz /usr/lib/jvm/

RUN microdnf install -y glibc-langpack-ko \
 && microdnf clean all

ENV LANG ko_KR.utf8
ENV LC_ALL ko_KR.utf8

# java 환경 설정
ENV JAVA_HOME /usr/lib/jvm/amazon-corretto-8.432.06.1-linux-x64
ENV PATH $PATH:${JAVA_HOME}/bin

ARG WORKDIR=/app
ENV WORKDIR=${WORKDIR}

RUN mkdir -p ${WORKDIR}
WORKDIR ${WORKDIR}

#ARG DEPLOY_DIR=deploy
#COPY ${DEPLOY_DIR}/. ${WORKDIR}/

ARG USE_GPU=false
RUN if [[ "${USE_GPU,,}" == "true" ]]; then \
    ...
fi
  • 수정 내용
    1. centOS 이미지보다 가벼운 RHEL 계열의 rockylinux의 minimal 버전을 사용하도록 수정
    2. GPU 사용 여부를 체크하여 Cuda Toolkit 설치

Docker Image 빌드 쉘 스크립트 작성

flow chart

  • 중간 중간에 세세한 기능은 빠졌지만 대략적으로 아래와 같다.

쉘 스크립트

#!/usr/bin/env bash

function check_var() {
    if [ -z "$1" ]; then
        echo "Not allowed empty input"
        exit 1
    fi
}

CUR_DIR="$( cd "$( dirname "$0" )" && pwd -P )"

DIR_LIST=($(find "$CUR_DIR" -maxdepth 1 -mindepth 1 -type d -exec basename {} \;))

MESSAGE="Please select build docker type: "
select TYPE_DIR in "${DIR_LIST[@]}"
do
    echo "Selected docker type: $TYPE_DIR"
    break
done

DIR_LIST=($(find "$CUR_DIR/$TYPE_DIR" -maxdepth 1 -mindepth 1 -type d -exec basename {} \;))

MESSAGE="Please select build docker image: "
select DOCKER_DIR in "${DIR_LIST[@]}"
do
    echo "Selected docker: $DOCKER_DIR"
    break
done

if [ -z "$DOCKER_DIR" ]; then
    echo "Invalid docker image, please retry it"
    exit 1
fi

CONFIG_YML_FILE="$TYPE_DIR/config.yml"

# YAML 파일에서 선택한 키($DOCKER_DIR)가 있는지 확인하고 값 가져오기
VALUES=$(awk -v key="$DOCKER_DIR" '
    $0 ~ "^" key ":" { flag=1; next }
    /^[[:space:]]*-/ && flag { print $2 }
    /^[^[:space:]]/ && flag { exit }
' "$CONFIG_YML_FILE")

if [ -n "$VALUES" ]; then
    echo "Current support your selected image detail(lower) tag"
    echo "$VALUES" | tr '\n' ' '
    echo

    read -p "Enter the tag of docker image TAG : " IMAGE_TAG

    TAG_EXIST=false
    if echo "$VALUES" | grep -wq "$IMAGE_TAG"; then
        echo "The tag '$IMAGE_TAG' is valid."
        TAG_EXIST=true
    else
        echo "Not supported tag '$IMAGE_TAG'. validating tag."
    fi
fi

FULL_IMAGE_NAME="${DOCKER_DIR//_/:}"
if [ -n "$IMAGE_TAG" ]; then
    FULL_IMAGE_NAME="$FULL_IMAGE_NAME.$IMAGE_TAG"
fi

echo "Your build docker image name: $FULL_IMAGE_NAME"

# 도커 허브에 이미지가 존재하는지 검증
if ! docker manifest inspect "$FULL_IMAGE_NAME" >/dev/null 2>&1; then
    echo "❌ ERROR: Docker Hub에 존재하지 않는 이미지 태그입니다: $FULL_IMAGE_NAME"
    exit 1
fi

while true; do
    read -p "Do you use it for personal purposes? (y, n)(q: quit task): " use_personal
    case $use_personal in
        [yY])
            FOR_PERSONAL=true
            break
        ;;
        [nN])
            FOR_PERSONAL=false
            break
        ;;
        [qQ])
            echo "Canceled docker image creation operation."
            exit 1
        ;;
        *)
            echo "Invalid input, try again"
        ;;
    esac
done

if $FOR_PERSONAL ; then
    USER_NAME=$(id -un)
    USER_ID=$(id -u)
    GROUP_NAME=$(id -gn)
    GROUP_ID=$(id -g)
else
    read -p "Enter user name: " USER_NAME
    check_var $USER_NAME
    read -p "Enter user id: " USER_ID
    check_var $USER_ID
    read -p "Enter group name: " GROUP_NAME
    check_var $GROUP_NAME
    read -p "Enter group id: " GROUP_ID
    check_var $GROUP_ID
fi

read -p "Enter the tag of docker image name : " IMAGE_NAME

# 생성하려는 이미지 명이 기존에 존재하는지 체크
if docker images --format "{{.Repository}}:{{.Tag}}" | grep -wq "$IMAGE_NAME"; then
    echo "The Docker image name you entered already exists: $IMAGE_NAME"
    echo "Please try again with another image name"
    exit 1
fi

# 이미지 빌드
docker build -t $IMAGE_NAME \
--build-arg USER_NAME=$USER_NAME \
--build-arg USER_ID=$USER_ID \
--build-arg GROUP_NAME=$GROUP_NAME \
--build-arg GROUP_ID=$GROUP_ID \
--build-arg IMAGE_TAG=$IMAGE_TAG \
$TYPE_DIR/$DOCKER_DIR

# 이미지가 정상적으로 생성되었을 경우
# 생성한 이미지의 태그가 기존에 존재하지 않을 경우 추가
if [ $? -eq 0 ]; then
    if [ "$TAG_EXIST" = false ]; then
        echo "Process completed successfully. Adding new tag: $IMAGE_TAG"
        
        # VALUES에 추가
        VALUES="${VALUES} $IMAGE_TAG"

        # config.yml 파일 업데이트
        TMP_FILE=$(mktemp)
        awk -v key="$DOCKER_DIR" -v new_value="$IMAGE_TAG" '
            BEGIN {
                flag = 0
            }
            {
                if ($0 ~ "^" key ":") {
                    flag = 1
                    print $0
                    next
                }

                if (flag == 1 && /^[[:space:]]*-/) {
                    print $0
                } else if (flag == 1 && /^[^[:space:]]/) {
                    print "  - " new_value
                    flag = 0
                    print $0
                } else {
                    print $0
                }
            }
        ' "$CONFIG_YML_FILE" > "$TMP_FILE"

        mv "$TMP_FILE" "$CONFIG_YML_FILE"
        echo "Updated config.yml with new tag: $IMAGE_TAG"
    fi
fi
  • 아직 보완할 부분이 많이 있지만 동료분들과 필요한 부분들을 우선 정리하고 해당 내용만 반영하여 코드를 작성해보았다.
  • 수정된 내용이 생긴다면 해당 페이지에 계속해서 수정하여 반영을 할 것이다.

정리

  • 여러 함수들을 이용해서 쉘 스크립트를 작성해본 것은 이번이 처음이다. 그래서 다소 함수들이 최적화가 안된 부분들이 많이 보이는 것 같다. 이번에 팀장님께 검사를 받고서.... 한번 제대로 털린 뒤 다시 수정해보자!
  • 현재 사용하는 방식은 docker image를 생성하고 그걸 container를 만드는 방식인데 테스트를 할 때, 여러개의 서로 다른 솔루션을 연결하여 테스트가 필요한 경우가 대부분이라.... 하나의 컨테이너를 생성하여 포트를 여러개 포워딩하여 서로 다른 솔루션을 실행하고 있다. 하지만 각 솔루션마다 필요한 OS 패키지들이 달라서 이것도 솔루션 별로 docker image를 최적화하여 docker compose를 이용해 테스트 환경을 구성해보도록 해봐야겠다.
profile
6년차 백엔드 엔지니어로 일하고 있습니다~!

0개의 댓글