From 5466f444c7864932d9dbe5506adbe154f7e00eeb Mon Sep 17 00:00:00 2001 From: aapdo Date: Fri, 1 May 2026 21:11:52 +0900 Subject: [PATCH 1/5] feat: add opt-in noVNC GUI support --- Dockerfile | 14 +++++++++ README.md | 47 +++++++++++++++++++++++++--- entrypoint.sh | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9b09888..53ea72a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,10 @@ # 변경한 사람: 이소은 # tensorflow version 2.13.0-gpu -> 2.18.0-gpu +##### decs:260501##### +# 변경한 사람: 임준영 +# Xfce, TigerVNC, noVNC 기반 GUI 접속 지원 추가 + ########################### version history end ########################## # TensorFlow 2.18.0 GPU 공식 이미지 사용 (CUDA 12.3, cuDNN 8.9 포함, Ubuntu 22.04 기반) @@ -30,6 +34,13 @@ vim \ wget \ curl \ ssh \ +dbus-x11 \ +xfce4 \ +xfce4-terminal \ +tigervnc-standalone-server \ +tigervnc-common \ +novnc \ +websockify \ software-properties-common # motd install @@ -78,6 +89,9 @@ RUN mkdir /jupyter_config \ # entrypoint.sh 복사 COPY entrypoint.sh / +# noVNC 접속 포트. VNC 서버(5901)는 localhost에만 바인딩합니다. +EXPOSE 6080 + # SSHD 서버를 실행하고, entrypoint 파일을 start/restart 시 마다 실행, dev/null에 entrypoint 로그를 저장 RUN chmod +x /entrypoint.sh ENTRYPOINT ["bash", "/entrypoint.sh"] diff --git a/README.md b/README.md index e189b75..95b1ad5 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,18 @@ | 이미지 태그 (Image Tag) | TensorFlow 버전 | CUDA / cuDNN | 베이스 OS (Base OS) | 주요 변경사항 및 설명 | | :--- | :--- | :--- | :--- | :--- | -| `dguailab/decs:latest`
`dguailab/decs:260201` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(최신)** 의도되지 않은 MOTD 출력 방지 버그 수정 | -| `dguailab/decs:251023` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(이전 안정 버전)** Jupyter Notebook 버전 변경으로 인한 오류 해결 버전 | -| `dguailab/decs:251002` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | TensorFlow 2.18.0 업그레이드, 최신 GPU 환경 지원 | -| `dguailab/decs:250926` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | **(이전 안정 버전)** TensorFlow 2.13.0 기반의 안정화 버전 | +| `dguailab/decs:latest`
`dguailab/decs:260501` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(최신)** Xfce + TigerVNC + noVNC 기반 브라우저 GUI 접속 지원 추가 | +| `dguailab/decs:260427` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 기존 사용자 컨테이너 변경 시 `~/.bashrc`의 conda initialize 블록 갱신, 경고 주석 영문화 및 인덴트 수정 | +| `dguailab/decs:260403` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 계정/권한 검증 강화, config-server 주입 기반 sudo 권한 축소, Jupyter 및 컨테이너 유지 프로세스 비-root 실행, gosu 추가 | +| `dguailab/decs:260201` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 의도되지 않은 MOTD 출력 방지 버그 수정 | +| `dguailab/decs:251023` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(이전 안정 버전)** Jupyter Notebook 설정 파일 생성/경로 오류 해결, 랜덤 토큰 저장 방식 반영 | +| `dguailab/decs:251002` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | TensorFlow 2.18.0 업그레이드, 최신 GPU 환경 지원, Docker Hub 자동 빌드/푸시 워크플로 도입 | +| `dguailab/decs:250926` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | **(이전 안정 버전)** 사용자 홈 디렉토리 소유권 설정 순서 수정, 이슈/PR 템플릿 추가 | +| `dguailab/decs:250428` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | Jupyter Lab 설정 파일을 사용자 홈의 `.jupyter` 경로에서 생성하고 사용하도록 수정 | +| `dguailab/decs:250416` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | Jupyter Notebook 설정 파일 생성 로직 수정, 10자리 랜덤 토큰 생성 및 파일 저장 추가 | +| `dguailab/decs:250323` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | `useradd` 옵션 보완, MOTD 공지 추가, TensorFlow ASCII 출력 제거 | +| `dguailab/decs:250309` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | ldconfig permission denied 오류 방지, MOTD/Slack 공지 반영, Conda 최신화 반영 | +| `dguailab/decs:250218` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | Anaconda3 2024.10-1 설치로 Conda 24.9.2 업그레이드 | ## ⚙️ 사용 방법 필요한 버전의 이미지를 Docker Hub에서 pull 받아 사용합니다. @@ -28,6 +36,37 @@ docker pull dguailab/decs:251002 docker pull dguailab/decs:250926 ``` +### GUI 접속(noVNC) +`260501` 버전부터 브라우저 기반 GUI 접속을 지원합니다. GUI 접속은 기본 비활성화 상태이며, 컨테이너 생성 시 `ENABLE_VNC=true` 환경변수를 전달한 경우에만 시작됩니다. + +- 컨테이너 내부 noVNC 포트: `6080` +- 컨테이너 내부 VNC 포트: `localhost:5901` 전용 +- 접속 URL: `http://서버주소:외부포트/vnc.html` +- VNC 비밀번호 저장 위치: `/home/$USER_ID/decs_jupyter_lab/vnc_password.txt` + +직접 `docker run`을 사용할 때는 다음처럼 환경변수와 포트를 함께 지정합니다. + +``` +-e ENABLE_VNC=true -p 외부포트:6080 +``` + +운영 스크립트(`~/uid`)에서는 `--enable_vnc true`를 주면 컨테이너 내부 `6080` 포트를 자동으로 추가하고, Docker 실행 환경변수에 `ENABLE_VNC=true`를 함께 전달합니다. FARM2 예시는 외부 포트 범위 `9100~9199` 중 하나가 `6080`에 연결되는 방식입니다. + +``` +--enable_vnc true +``` + +관련 환경변수: + +| 환경변수 | 기본값 | 설명 | +| :--- | :--- | :--- | +| `ENABLE_VNC` | `false` | `true`로 설정한 경우에만 VNC/noVNC를 시작합니다. | +| `VNC_PASSWORD` | 랜덤 8자리 | 지정하지 않으면 자동 생성 후 `vnc_password.txt`에 저장합니다. | +| `VNC_RESOLUTION` | `1920x1080` | VNC 화면 해상도입니다. | +| `VNC_DEPTH` | `24` | VNC 색상 깊이입니다. | +| `VNC_DISPLAY` | `1` | VNC display 번호입니다. 기본 VNC 포트는 `5901`입니다. | +| `NOVNC_PORT` | `6080` | noVNC가 컨테이너 내부에서 listen할 포트입니다. | + # ⚒️ 빌드 자동화 이 Docker 이미지는 GitHub Actions를 통해 자동으로 빌드 및 배포됩니다. diff --git a/entrypoint.sh b/entrypoint.sh index b03bcea..cf3d799 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,89 @@ #!/bin/bash +start_novnc() { + case "${ENABLE_VNC:-false}" in + true|TRUE|1|yes|YES|on|ON) ;; + *) + echo "VNC/noVNC disabled. Set ENABLE_VNC=true to enable it." + return 0 + ;; + esac + + local user_home="/home/$USER_ID" + local vnc_dir="$user_home/.vnc" + local vnc_display="${VNC_DISPLAY:-1}" + local vnc_resolution="${VNC_RESOLUTION:-1920x1080}" + local vnc_depth="${VNC_DEPTH:-24}" + local novnc_port="${NOVNC_PORT:-6080}" + local vnc_password_file="$user_home/decs_jupyter_lab/vnc_password.txt" + local vnc_password + + vnc_display="${vnc_display#:}" + if ! [[ "$vnc_display" =~ ^[0-9]+$ && "$novnc_port" =~ ^[0-9]+$ && "$vnc_depth" =~ ^[0-9]+$ ]]; then + echo "Invalid VNC configuration. Check VNC_DISPLAY, NOVNC_PORT, and VNC_DEPTH." + return 1 + fi + local vnc_port=$((5900 + vnc_display)) + + if ! command -v vncserver >/dev/null 2>&1 || ! command -v websockify >/dev/null 2>&1; then + echo "VNC/noVNC packages are not installed. Skipping GUI startup." + return 0 + fi + + mkdir -p "$vnc_dir" "$user_home/decs_jupyter_lab" /tmp/.X11-unix /tmp/.ICE-unix + chown root:root /tmp/.X11-unix /tmp/.ICE-unix + chmod 1777 /tmp/.X11-unix /tmp/.ICE-unix + + if [[ -n "${VNC_PASSWORD:-}" ]]; then + vnc_password="$VNC_PASSWORD" + elif [[ -s "$vnc_password_file" ]]; then + vnc_password=$(tr -d '\r\n' < "$vnc_password_file" | head -c 8) + else + vnc_password=$(tr -dc A-Za-z0-9 "$vnc_password_file" + chmod 600 "$vnc_password_file" + + printf "%s\n" "$vnc_password" | vncpasswd -f > "$vnc_dir/passwd" + chmod 600 "$vnc_dir/passwd" + + cat > "$vnc_dir/xstartup" <<'EOF' +#!/bin/sh +unset SESSION_MANAGER +unset DBUS_SESSION_BUS_ADDRESS +export XDG_SESSION_TYPE=x11 +export XKL_XMODMAP_DISABLE=1 +xrdb "$HOME/.Xresources" 2>/dev/null || true +exec dbus-launch --exit-with-session startxfce4 +EOF + chmod +x "$vnc_dir/xstartup" + chown -R "$USER_ID:$USER_GROUP" "$vnc_dir" "$vnc_password_file" + + sudo -u "$USER_ID" env HOME="$user_home" USER="$USER_ID" \ + vncserver -kill ":$vnc_display" >/tmp/vnc-kill.log 2>&1 || true + + echo "trying TigerVNC on localhost:$vnc_port..." + if ! sudo -u "$USER_ID" env HOME="$user_home" USER="$USER_ID" \ + vncserver -localhost yes ":$vnc_display" -geometry "$vnc_resolution" -depth "$vnc_depth" >/tmp/vncserver.log 2>&1; then + echo "TigerVNC startup failed. See /tmp/vncserver.log." + cat /tmp/vncserver.log + return 1 + fi + echo "TigerVNC listening on localhost:$vnc_port" + + pkill -f "websockify.*$novnc_port" >/dev/null 2>&1 || true + echo "trying noVNC on 0.0.0.0:$novnc_port..." + nohup websockify --web=/usr/share/novnc "0.0.0.0:$novnc_port" "localhost:$vnc_port" >/tmp/novnc.log 2>&1 & + echo "noVNC listening on port $novnc_port. VNC password saved to $vnc_password_file" +} + sudo apt update sudo apt install -y auditd @@ -129,6 +213,9 @@ echo "trying jupyter lab..." nohup /opt/anaconda3/bin/jupyter lab --NotebookApp.token=$TOKEN --config=/home/$USER_ID/.jupyter/jupyter_notebook_config.py >/dev/null 2>&1 & echo "jupyter lab listening!" +# noVNC 기동. 외부에서는 컨테이너의 6080 포트를 매핑해서 접속합니다. +start_novnc || echo "VNC/noVNC startup failed." + # ldconfig permission 오류 방지 # bash.bashrc에서 ldconfig 명령어 삭제 후 명령어 실행 및 결과 출력 sed -i '/ldconfig/d' /etc/bash.bashrc From d841a3b403e7677afb61699e99ec6a6523188e6f Mon Sep 17 00:00:00 2001 From: aapdo Date: Wed, 6 May 2026 22:26:26 +0900 Subject: [PATCH 2/5] feat: allow user password override --- README.md | 3 ++- entrypoint.sh | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 95b1ad5..efa6c08 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ | 이미지 태그 (Image Tag) | TensorFlow 버전 | CUDA / cuDNN | 베이스 OS (Base OS) | 주요 변경사항 및 설명 | | :--- | :--- | :--- | :--- | :--- | -| `dguailab/decs:latest`
`dguailab/decs:260501` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(최신)** Xfce + TigerVNC + noVNC 기반 브라우저 GUI 접속 지원 추가 | +| `dguailab/decs:latest`
`dguailab/decs:260506` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(최신)** 컨테이너 생성 시 `USER_PW` 환경변수로 사용자 초기 비밀번호를 주입할 수 있도록 하고, 값이 없으면 기존 기본 비밀번호를 사용하도록 fallback 추가 | +| `dguailab/decs:260501` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | Xfce + TigerVNC + noVNC 기반 브라우저 GUI 접속 지원 추가 | | `dguailab/decs:260427` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 기존 사용자 컨테이너 변경 시 `~/.bashrc`의 conda initialize 블록 갱신, 경고 주석 영문화 및 인덴트 수정 | | `dguailab/decs:260403` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 계정/권한 검증 강화, config-server 주입 기반 sudo 권한 축소, Jupyter 및 컨테이너 유지 프로세스 비-root 실행, gosu 추가 | | `dguailab/decs:260201` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 의도되지 않은 MOTD 출력 방지 버그 수정 | diff --git a/entrypoint.sh b/entrypoint.sh index cf3d799..009ac05 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,7 @@ #!/bin/bash +USER_PW="${USER_PW:-ailab2260}" + start_novnc() { case "${ENABLE_VNC:-false}" in true|TRUE|1|yes|YES|on|ON) ;; From 0e96b1c2ca0fac8bef5383870f72d9612d159f47 Mon Sep 17 00:00:00 2001 From: aapdo Date: Wed, 6 May 2026 22:27:52 +0900 Subject: [PATCH 3/5] fix: move VNC password file to home --- README.md | 2 +- entrypoint.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index efa6c08..3825491 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ docker pull dguailab/decs:250926 - 컨테이너 내부 noVNC 포트: `6080` - 컨테이너 내부 VNC 포트: `localhost:5901` 전용 - 접속 URL: `http://서버주소:외부포트/vnc.html` -- VNC 비밀번호 저장 위치: `/home/$USER_ID/decs_jupyter_lab/vnc_password.txt` +- VNC 비밀번호 저장 위치: `/home/$USER_ID/vnc_password.txt` 직접 `docker run`을 사용할 때는 다음처럼 환경변수와 포트를 함께 지정합니다. diff --git a/entrypoint.sh b/entrypoint.sh index 009ac05..29dd59e 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -17,7 +17,7 @@ start_novnc() { local vnc_resolution="${VNC_RESOLUTION:-1920x1080}" local vnc_depth="${VNC_DEPTH:-24}" local novnc_port="${NOVNC_PORT:-6080}" - local vnc_password_file="$user_home/decs_jupyter_lab/vnc_password.txt" + local vnc_password_file="$user_home/vnc_password.txt" local vnc_password vnc_display="${vnc_display#:}" From 2f0f591d98eb0911df560c681cd1272a8598c628 Mon Sep 17 00:00:00 2001 From: aapdo Date: Wed, 6 May 2026 22:51:04 +0900 Subject: [PATCH 4/5] fix: open noVNC from root URL --- README.md | 6 +++--- entrypoint.sh | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3825491..e018ed8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ | 이미지 태그 (Image Tag) | TensorFlow 버전 | CUDA / cuDNN | 베이스 OS (Base OS) | 주요 변경사항 및 설명 | | :--- | :--- | :--- | :--- | :--- | -| `dguailab/decs:latest`
`dguailab/decs:260506` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(최신)** 컨테이너 생성 시 `USER_PW` 환경변수로 사용자 초기 비밀번호를 주입할 수 있도록 하고, 값이 없으면 기존 기본 비밀번호를 사용하도록 fallback 추가 | +| `dguailab/decs:latest`
`dguailab/decs:260506` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(최신)** `USER_PW` 환경변수로 사용자 초기 비밀번호 주입 지원, VNC 비밀번호를 `/home/$USER_ID/vnc_password.txt`에 저장, noVNC 루트 URL(`http://서버주소:VNC포트`) 접속 지원 | | `dguailab/decs:260501` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | Xfce + TigerVNC + noVNC 기반 브라우저 GUI 접속 지원 추가 | | `dguailab/decs:260427` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 기존 사용자 컨테이너 변경 시 `~/.bashrc`의 conda initialize 블록 갱신, 경고 주석 영문화 및 인덴트 수정 | | `dguailab/decs:260403` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 계정/권한 검증 강화, config-server 주입 기반 sudo 권한 축소, Jupyter 및 컨테이너 유지 프로세스 비-root 실행, gosu 추가 | @@ -42,7 +42,7 @@ docker pull dguailab/decs:250926 - 컨테이너 내부 noVNC 포트: `6080` - 컨테이너 내부 VNC 포트: `localhost:5901` 전용 -- 접속 URL: `http://서버주소:외부포트/vnc.html` +- 접속 URL: `http://서버주소:VNC포트` 또는 `http://서버주소:VNC포트/vnc.html` - VNC 비밀번호 저장 위치: `/home/$USER_ID/vnc_password.txt` 직접 `docker run`을 사용할 때는 다음처럼 환경변수와 포트를 함께 지정합니다. @@ -51,7 +51,7 @@ docker pull dguailab/decs:250926 -e ENABLE_VNC=true -p 외부포트:6080 ``` -운영 스크립트(`~/uid`)에서는 `--enable_vnc true`를 주면 컨테이너 내부 `6080` 포트를 자동으로 추가하고, Docker 실행 환경변수에 `ENABLE_VNC=true`를 함께 전달합니다. FARM2 예시는 외부 포트 범위 `9100~9199` 중 하나가 `6080`에 연결되는 방식입니다. +운영 스크립트(`~/uid`)에서는 `--enable_vnc true`를 주면 컨테이너 내부 `6080` 포트를 VNC 포트로 별도 할당하고, Docker 실행 환경변수에 `ENABLE_VNC=true`를 함께 전달합니다. 이 포트는 사용자가 요청한 추가 포트가 아니라 SSH, Jupyter와 같은 기본 접속 포트로 안내됩니다. ``` --enable_vnc true diff --git a/entrypoint.sh b/entrypoint.sh index 29dd59e..a060492 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -80,6 +80,10 @@ EOF fi echo "TigerVNC listening on localhost:$vnc_port" + if [[ -d /usr/share/novnc && -f /usr/share/novnc/vnc.html ]]; then + ln -sf /usr/share/novnc/vnc.html /usr/share/novnc/index.html + fi + pkill -f "websockify.*$novnc_port" >/dev/null 2>&1 || true echo "trying noVNC on 0.0.0.0:$novnc_port..." nohup websockify --web=/usr/share/novnc "0.0.0.0:$novnc_port" "localhost:$vnc_port" >/tmp/novnc.log 2>&1 & From a45066a8109683edc3f1b4e3e7311e6d8c003bb3 Mon Sep 17 00:00:00 2001 From: aapdo Date: Fri, 15 May 2026 11:57:04 +0900 Subject: [PATCH 5/5] Add CUDA image variants and smoke tests --- .dockerignore | 6 + .github/workflows/docker-publish.yml | 75 +++++++++-- .gitignore | 2 + Dockerfile | 189 +++++++++++++++------------ README.md | 181 ++++++++++++++++--------- entrypoint.sh | 78 +++++++++-- image-variants.json | 70 ++++++++++ scripts/build_variants.py | 80 ++++++++++++ scripts/test_image_variants.py | 91 +++++++++++++ scripts/test_uid_create_container.py | 95 ++++++++++++++ scripts/variant_matrix.py | 78 +++++++++++ tests/ansible/decs_image_build.yml | 53 ++++++++ tests/ansible/decs_image_smoke.yml | 102 +++++++++++++++ 13 files changed, 927 insertions(+), 173 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 image-variants.json create mode 100755 scripts/build_variants.py create mode 100755 scripts/test_image_variants.py create mode 100755 scripts/test_uid_create_container.py create mode 100755 scripts/variant_matrix.py create mode 100644 tests/ansible/decs_image_build.yml create mode 100644 tests/ansible/decs_image_smoke.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2ebb488 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.github +README.md +image-variants.json +scripts +tests diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4739e02..80124bb 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -3,12 +3,59 @@ name: CI/CD - Docker Image from DECS Branch on: pull_request: types: [closed] - branches: [ "develop" ] + branches: [ "main" ] + workflow_dispatch: + inputs: + date_tag: + description: "YYMMDD release tag. Defaults to image-variants.json." + required: false + type: string jobs: + prepare: + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.generate_tag.outputs.TAG_NAME }} + matrix: ${{ steps.generate_matrix.outputs.matrix }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Generate release tag + id: generate_tag + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ inputs.date_tag }}" ]]; then + TAG_VERSION="${{ inputs.date_tag }}" + elif [[ -n "${{ github.event.pull_request.head.ref }}" ]]; then + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + TAG_VERSION="${BRANCH_NAME#decs}" + else + TAG_VERSION="$(python3 -c 'import json; print(json.load(open("image-variants.json", encoding="utf-8"))["default_date_tag"])')" + fi + + if ! [[ "$TAG_VERSION" =~ ^[0-9]{6}$ ]]; then + echo "Release tag must be YYMMDD, got: $TAG_VERSION" >&2 + exit 1 + fi + + echo "TAG_NAME=$TAG_VERSION" >> "$GITHUB_OUTPUT" + + - name: Generate build matrix + id: generate_matrix + run: | + python3 scripts/variant_matrix.py \ + --date-tag "${{ steps.generate_tag.outputs.TAG_NAME }}" \ + --repository dguailab/decs \ + --github-output + build-and-push: - if: github.event.pull_request.merged == true + needs: prepare runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }} steps: - name: Checkout repository @@ -20,19 +67,19 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Generate tag from branch name - id: generate_tag - run: | - BRANCH_NAME=${{ github.event.pull_request.head.ref }} - TAG_VERSION=${BRANCH_NAME#decs} - echo "Extracted tag version: $TAG_VERSION" - echo "TAG_NAME=$TAG_VERSION" >> $GITHUB_OUTPUT - - - name: Build and push Docker image + - name: Build and push ${{ matrix.id }} uses: docker/build-push-action@v5 with: context: . + file: ./Dockerfile push: true - tags: | - dguailab/decs:${{ steps.generate_tag.outputs.TAG_NAME }} - dguailab/decs:latest + build-args: | + BASE_IMAGE=${{ matrix.base_image }} + DECS_IMAGE_VARIANT=${{ matrix.id }} + CUDA_VERSION=${{ matrix.cuda_version }} + TENSORFLOW_VERSION=${{ matrix.tensorflow_version }} + TENSORFLOW_PACKAGE=${{ matrix.tensorflow_package }} + PYTHON_VERSION=${{ matrix.python_version }} + UBUNTU_VERSION=${{ matrix.ubuntu_version }} + MIN_NVIDIA_DRIVER=${{ matrix.min_nvidia_driver }} + tags: ${{ matrix.docker_tags }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/Dockerfile b/Dockerfile index 53ea72a..05b95ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,97 +1,114 @@ -# 이 파일은 decs:251002 이미지의 Dockerfile 입니다. -############################# version history ############################# - -##### decs:251002##### -# 변경한 사람: 이소은 -# tensorflow version 2.13.0-gpu -> 2.18.0-gpu - -##### decs:260501##### -# 변경한 사람: 임준영 -# Xfce, TigerVNC, noVNC 기반 GUI 접속 지원 추가 - -########################### version history end ########################## - -# TensorFlow 2.18.0 GPU 공식 이미지 사용 (CUDA 12.3, cuDNN 8.9 포함, Ubuntu 22.04 기반) -# https://hub.docker.com/layers/tensorflow/tensorflow/2.18.0-gpu/images/sha256-b076938b81335b8098a58a9e701ea183a652f146419f8601550c000f576e3cc4 -FROM tensorflow/tensorflow:2.18.0-gpu - -# 설치 시 geographic area 를 물어보지 않도록 설정(apt install 시 interrupted 됨) -ENV DEBIAN_FRONTEND noninteractive - -ENV SUDOER_ID svmanager -ENV SUDOER_PW decs2260 -ENV SUDOER_DIR /$SUDOER_ID -ENV SSHD_CONFIG_PATH /etc/ssh/sshd_config +# DECS CUDA/TensorFlow image template. +# Build variants are defined in image-variants.json and passed as build args. + +ARG BASE_IMAGE=nvidia/cuda:12.5.1-cudnn-devel-ubuntu22.04 +FROM ${BASE_IMAGE} + +ARG BASE_IMAGE +ARG DECS_IMAGE_VARIANT=cuda12.5-tf2.20-ubuntu22.04 +ARG CUDA_VERSION=12.5 +ARG TENSORFLOW_VERSION=2.20.0 +ARG TENSORFLOW_PACKAGE=tensorflow==2.20.0 +ARG PYTHON_VERSION=3.10 +ARG UBUNTU_VERSION=22.04 +ARG MIN_NVIDIA_DRIVER=555.42.06 +ARG MINIFORGE_VERSION=25.3.1-0 + +LABEL org.opencontainers.image.base.name="${BASE_IMAGE}" \ + ai.dgu.decs.variant="${DECS_IMAGE_VARIANT}" \ + ai.dgu.decs.cuda="${CUDA_VERSION}" \ + ai.dgu.decs.tensorflow="${TENSORFLOW_VERSION}" \ + ai.dgu.decs.python="${PYTHON_VERSION}" \ + ai.dgu.decs.ubuntu="${UBUNTU_VERSION}" \ + ai.dgu.decs.min_nvidia_driver="${MIN_NVIDIA_DRIVER}" + +ENV DEBIAN_FRONTEND=noninteractive \ + CONDA_DIR=/opt/conda \ + DECS_IMAGE_VARIANT="${DECS_IMAGE_VARIANT}" \ + DECS_CUDA_VERSION="${CUDA_VERSION}" \ + DECS_TENSORFLOW_VERSION="${TENSORFLOW_VERSION}" \ + DECS_PYTHON_VERSION="${PYTHON_VERSION}" \ + DECS_MIN_NVIDIA_DRIVER="${MIN_NVIDIA_DRIVER}" \ + SUDOER_ID=svmanager \ + SUDOER_PW=decs2260 \ + SUDOER_DIR=/svmanager \ + SSHD_CONFIG_PATH=/etc/ssh/sshd_config + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN apt-get clean \ -&& apt-get -y update \ -&& apt install -y \ -sudo \ -net-tools \ -fcitx-hangul \ -fonts-nanum* \ -vim \ -wget \ -curl \ -ssh \ -dbus-x11 \ -xfce4 \ -xfce4-terminal \ -tigervnc-standalone-server \ -tigervnc-common \ -novnc \ -websockify \ -software-properties-common - -# motd install -RUN apt-get update && apt-get install -y update-motd - - -# 관리자 계정의 home directory 로 쓸 폴더 추가(home은 nfs이므로, 다른 곳에 생성) -RUN mkdir "$SUDOER_DIR" -# 관리자 계정을 추가, home directory 를 위에서 생성한 폴더로 설정 -RUN useradd -s /bin/bash -d /$SUDOER_ID -G sudo $SUDOER_ID \ - && echo "$SUDOER_ID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers -# skel 을 복사 (로그인 시 tf-docker 로 뜨지 않게 하는 목적) -RUN cp -R /etc/skel/. "$SUDOER_DIR" - -RUN echo $SUDOER_ID:$SUDOER_PW | chpasswd - - -# decs dir 을 생성 -RUN mkdir /home/decs + && apt-get update \ + && apt-get install -y --no-install-recommends \ + auditd \ + ca-certificates \ + curl \ + dbus-x11 \ + fcitx-hangul \ + fonts-nanum \ + fonts-nanum-coding \ + fonts-nanum-extra \ + gnupg \ + net-tools \ + novnc \ + openssh-server \ + software-properties-common \ + sudo \ + tigervnc-common \ + tigervnc-standalone-server \ + update-motd \ + vim \ + websockify \ + wget \ + xfce4 \ + xfce4-terminal \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p "$SUDOER_DIR" /home/decs /run/sshd \ + && useradd -s /bin/bash -d "$SUDOER_DIR" -G sudo "$SUDOER_ID" \ + && echo "$SUDOER_ID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \ + && cp -R /etc/skel/. "$SUDOER_DIR" \ + && echo "$SUDOER_ID:$SUDOER_PW" | chpasswd RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ - && apt-get update && apt-get install -y google-chrome-stable - -RUN printf "LANG=\"ko_KR.UTF-8\"\nLANG=\"ko_KR.EUC-KR\"\nLANGUAGE=\"ko_KR:ko:en_GB:en\"\n" >> /etc/environment \ -&& fc-cache -r - -RUN cat /etc/environment - -# 최신 Anaconda 버전 다운로드 및 설치 -RUN wget https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-x86_64.sh \ - && bash Anaconda3-2024.10-1-Linux-x86_64.sh -b -p /opt/anaconda3 \ - && rm Anaconda3-2024.10-1-Linux-x86_64.sh - -ENV PATH /opt/anaconda3/bin:$PATH -RUN echo "export PATH="/opt/anaconda3/bin:$PATH"" >> /etc/profile \ - && /opt/anaconda3/bin/conda init - -# jupyterlab 설치 -RUN /opt/anaconda3/bin/conda install -y jupyterlab - -# jupyterlab 설정파일 생성 -RUN mkdir /jupyter_config \ - && /opt/anaconda3/bin/jupyter lab --generate-config --config=/jupyter_config/jupyter_notebook_config.py + && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends google-chrome-stable \ + && rm -rf /var/lib/apt/lists/* + +RUN printf 'LANG="ko_KR.UTF-8"\nLANGUAGE="ko_KR:ko:en_GB:en"\n' >> /etc/environment \ + && fc-cache -r + +RUN wget -q "https://github.com/conda-forge/miniforge/releases/download/${MINIFORGE_VERSION}/Miniforge3-Linux-x86_64.sh" \ + && bash Miniforge3-Linux-x86_64.sh -b -p "$CONDA_DIR" \ + && rm Miniforge3-Linux-x86_64.sh + +ENV PATH=/opt/conda/bin:$PATH + +RUN conda config --system --set channel_priority strict \ + && conda config --system --add channels conda-forge \ + && conda install -n base -y \ + "python=${PYTHON_VERSION}" \ + ipywidgets \ + jupyterlab \ + micromamba \ + notebook \ + pip \ + && python -m pip install --no-cache-dir --upgrade pip \ + && python -m pip install --no-cache-dir "${TENSORFLOW_PACKAGE}" \ + && conda clean -afy \ + && conda init bash + +RUN apt-get update \ + && apt-get install -y --no-install-recommends tigervnc-tools \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /jupyter_config \ + && touch /jupyter_config/jupyter_notebook_config.py -# entrypoint.sh 복사 COPY entrypoint.sh / -# noVNC 접속 포트. VNC 서버(5901)는 localhost에만 바인딩합니다. +# noVNC listens on 6080. The TigerVNC server binds to localhost only. EXPOSE 6080 -# SSHD 서버를 실행하고, entrypoint 파일을 start/restart 시 마다 실행, dev/null에 entrypoint 로그를 저장 RUN chmod +x /entrypoint.sh ENTRYPOINT ["bash", "/entrypoint.sh"] diff --git a/README.md b/README.md index e018ed8..8815743 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,143 @@ -## 🗒️ 이미지 버전 요약 -각 이미지 태그는 생성된 날짜(YYMMDD)를 따르며, latest 태그는 항상 최신 버전의 이미지를 가리킵니다. - -| 이미지 태그 (Image Tag) | TensorFlow 버전 | CUDA / cuDNN | 베이스 OS (Base OS) | 주요 변경사항 및 설명 | -| :--- | :--- | :--- | :--- | :--- | -| `dguailab/decs:latest`
`dguailab/decs:260506` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(최신)** `USER_PW` 환경변수로 사용자 초기 비밀번호 주입 지원, VNC 비밀번호를 `/home/$USER_ID/vnc_password.txt`에 저장, noVNC 루트 URL(`http://서버주소:VNC포트`) 접속 지원 | -| `dguailab/decs:260501` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | Xfce + TigerVNC + noVNC 기반 브라우저 GUI 접속 지원 추가 | -| `dguailab/decs:260427` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 기존 사용자 컨테이너 변경 시 `~/.bashrc`의 conda initialize 블록 갱신, 경고 주석 영문화 및 인덴트 수정 | -| `dguailab/decs:260403` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 계정/권한 검증 강화, config-server 주입 기반 sudo 권한 축소, Jupyter 및 컨테이너 유지 프로세스 비-root 실행, gosu 추가 | -| `dguailab/decs:260201` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | 의도되지 않은 MOTD 출력 방지 버그 수정 | -| `dguailab/decs:251023` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | **(이전 안정 버전)** Jupyter Notebook 설정 파일 생성/경로 오류 해결, 랜덤 토큰 저장 방식 반영 | -| `dguailab/decs:251002` | **2.18.0** | CUDA 12.5
cuDNN 8.9 | Ubuntu 22.04 | TensorFlow 2.18.0 업그레이드, 최신 GPU 환경 지원, Docker Hub 자동 빌드/푸시 워크플로 도입 | -| `dguailab/decs:250926` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | **(이전 안정 버전)** 사용자 홈 디렉토리 소유권 설정 순서 수정, 이슈/PR 템플릿 추가 | -| `dguailab/decs:250428` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | Jupyter Lab 설정 파일을 사용자 홈의 `.jupyter` 경로에서 생성하고 사용하도록 수정 | -| `dguailab/decs:250416` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | Jupyter Notebook 설정 파일 생성 로직 수정, 10자리 랜덤 토큰 생성 및 파일 저장 추가 | -| `dguailab/decs:250323` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | `useradd` 옵션 보완, MOTD 공지 추가, TensorFlow ASCII 출력 제거 | -| `dguailab/decs:250309` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | ldconfig permission denied 오류 방지, MOTD/Slack 공지 반영, Conda 최신화 반영 | -| `dguailab/decs:250218` | **2.13.0** | CUDA 11.8
cuDNN 8.6 | Ubuntu 20.04 | Anaconda3 2024.10-1 설치로 Conda 24.9.2 업그레이드 | - -## ⚙️ 사용 방법 -필요한 버전의 이미지를 Docker Hub에서 pull 받아 사용합니다. - -### 최신 버전 사용하기 -latest 태그를 사용하여 항상 최신 버전의 이미지를 받을 수 있습니다. -``` -docker pull dguailab/decs:latest -``` +## DECS Docker Images -### 날짜 태그를 직접 명시하여 받기 -``` -docker pull dguailab/decs:251002 +DECS 이미지는 GPU/driver 호환성을 관리하기 위해 CUDA variant별로 빌드한다. 공통 Dockerfile은 하나만 유지하고, 실제 CUDA/TensorFlow 조합은 `image-variants.json`에서 관리한다. + +## Image Variants + +| Variant tag | CUDA | TensorFlow | Base image | 최소 NVIDIA driver | 상태 | +| --- | --- | --- | --- | --- | --- | +| `cuda11.8-tf2.13-ubuntu22.04-260515` | 11.8 | 2.13.0 | `nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04` | 520.61.05 | stable | +| `cuda12.2-tf2.15-ubuntu22.04-260515` | 12.2 | 2.15.0 | `nvidia/cuda:12.2.2-cudnn8-devel-ubuntu22.04` | 535.104.05 | stable | +| `cuda12.5-tf2.20-ubuntu22.04-260515` | 12.5 | 2.20.0 | `nvidia/cuda:12.5.1-cudnn-devel-ubuntu22.04` | 555.42.06 | stable | +| `cuda12.8-tf2.20-ubuntu22.04-260515` | 12.8 | 2.20.0 | `nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04` | 570.124.06 | experimental | + +Alias tags: + +| Alias | Target | +| --- | --- | +| `latest`, `stable`, `cuda12.5-tf2.20` | CUDA 12.5 / TensorFlow 2.20 | +| `legacy`, `cuda11.8-tf2.13` | CUDA 11.8 / TensorFlow 2.13 | +| `cuda12.2-tf2.15` | CUDA 12.2 / TensorFlow 2.15 | +| `cuda12.8-tf2.20`, `h200-experimental` | CUDA 12.8 / TensorFlow 2.20 | + +TensorFlow 공식 빌드 구성 기준으로 TensorFlow 2.20.0은 CUDA 12.5/cuDNN 9.3 조합이다. CUDA 12.8 이미지는 H200/LAB10 검증 전까지 experimental로 둔다. + +## Included Runtime + +모든 variant는 다음을 포함한다. + +- CUDA/cuDNN base image +- TensorFlow +- system packages: SSH, sudo, auditd, Korean fonts/input, Chrome, Xfce, TigerVNC, noVNC +- Miniforge under `/opt/conda` +- micromamba +- JupyterLab / Notebook / ipywidgets + +`entrypoint.sh`는 시작 시 이미지 variant, CUDA/TensorFlow 버전, 요구 driver 버전, 실제 `nvidia-smi` 정보를 출력한다. `STRICT_CUDA_COMPAT=true`를 주면 host driver가 variant의 최소 driver보다 낮을 때 시작을 실패시킨다. + +## Build + +전체 variant dry-run: + +```bash +python3 scripts/build_variants.py --dry-run ``` -### 특정 구버전 사용하기 -이전 버전의 TensorFlow 환경이 필요한 경우, 해당 날짜 태그를 명시하여 이미지를 받습니다. +특정 variant build: + +```bash +python3 scripts/build_variants.py --variant cuda12.5-tf2.20-ubuntu22.04 ``` -docker pull dguailab/decs:250926 + +push까지 수행: + +```bash +python3 scripts/build_variants.py --variant cuda12.5-tf2.20-ubuntu22.04 --push ``` -### GUI 접속(noVNC) -`260501` 버전부터 브라우저 기반 GUI 접속을 지원합니다. GUI 접속은 기본 비활성화 상태이며, 컨테이너 생성 시 `ENABLE_VNC=true` 환경변수를 전달한 경우에만 시작됩니다. +GitHub Actions는 `main` 대상 PR이 merge되거나 `workflow_dispatch`로 실행될 때 `image-variants.json`을 읽어 matrix build/push를 수행한다. -- 컨테이너 내부 noVNC 포트: `6080` -- 컨테이너 내부 VNC 포트: `localhost:5901` 전용 -- 접속 URL: `http://서버주소:VNC포트` 또는 `http://서버주소:VNC포트/vnc.html` -- VNC 비밀번호 저장 위치: `/home/$USER_ID/vnc_password.txt` +## Usage -직접 `docker run`을 사용할 때는 다음처럼 환경변수와 포트를 함께 지정합니다. +운영 스크립트(`~/uid/script/create_container.sh`)에서는 이미지 이름과 버전을 분리해서 전달한다. -``` --e ENABLE_VNC=true -p 외부포트:6080 +```bash +bash ~/uid/script/create_container.sh \ + --image decs \ + --version cuda12.5-tf2.20-ubuntu22.04-260515 ``` -운영 스크립트(`~/uid`)에서는 `--enable_vnc true`를 주면 컨테이너 내부 `6080` 포트를 VNC 포트로 별도 할당하고, Docker 실행 환경변수에 `ENABLE_VNC=true`를 함께 전달합니다. 이 포트는 사용자가 요청한 추가 포트가 아니라 SSH, Jupyter와 같은 기본 접속 포트로 안내됩니다. +noVNC는 기존처럼 opt-in이다. -``` ---enable_vnc true +```bash +--enable-vnc true ``` 관련 환경변수: | 환경변수 | 기본값 | 설명 | -| :--- | :--- | :--- | -| `ENABLE_VNC` | `false` | `true`로 설정한 경우에만 VNC/noVNC를 시작합니다. | -| `VNC_PASSWORD` | 랜덤 8자리 | 지정하지 않으면 자동 생성 후 `vnc_password.txt`에 저장합니다. | -| `VNC_RESOLUTION` | `1920x1080` | VNC 화면 해상도입니다. | -| `VNC_DEPTH` | `24` | VNC 색상 깊이입니다. | -| `VNC_DISPLAY` | `1` | VNC display 번호입니다. 기본 VNC 포트는 `5901`입니다. | -| `NOVNC_PORT` | `6080` | noVNC가 컨테이너 내부에서 listen할 포트입니다. | +| --- | --- | --- | +| `ENABLE_VNC` | `false` | `true`이면 TigerVNC/noVNC를 시작한다. | +| `VNC_PASSWORD` | 랜덤 8자리 | 지정하지 않으면 `/home/$USER_ID/vnc_password.txt`에 저장한다. | +| `VNC_RESOLUTION` | `1920x1080` | VNC 화면 해상도 | +| `VNC_DEPTH` | `24` | VNC 색상 깊이 | +| `VNC_DISPLAY` | `1` | VNC display 번호. 기본 VNC 포트는 `5901` | +| `NOVNC_PORT` | `6080` | noVNC listen 포트 | +| `STRICT_CUDA_COMPAT` | `false` | 최소 NVIDIA driver 미만이면 startup 실패 | +## Tests + +이 저장소 내부의 테스트 파일만 사용한다. 외부 `~/uid`와 ansible inventory는 호출 대상이다. + +로컬 이미지 smoke test: + +```bash +python3 scripts/test_image_variants.py --variant cuda12.5-tf2.20-ubuntu22.04 +python3 scripts/test_image_variants.py --variant cuda12.5-tf2.20-ubuntu22.04 --gpu +``` -# ⚒️ 빌드 자동화 -이 Docker 이미지는 GitHub Actions를 통해 자동으로 빌드 및 배포됩니다. -- 실행 조건: decsYYMMDD 형식의 브랜치가 develop 브랜치로 병합(merge)될 때 -- 자동 생성 태그: -- 브랜치 이름에서 추출한 날짜 태그 (예: `dguailab/decs:251002`) -- 최신 버전을 가리키는 latest 태그 (`dguailab/decs:latest`) +`~/uid/script_test/create_container.sh` dry-run 연동 테스트: + +```bash +python3 scripts/test_uid_create_container.py --variant cuda12.5-tf2.20-ubuntu22.04 +``` + +LAB10 같은 실제 GPU host에서 ansible smoke test: + +```bash +tar -czf /tmp/decs-build-context-260515.tgz Dockerfile entrypoint.sh .dockerignore + +ansible-playbook \ + -i /home/jy/ansible/inventory.ini \ + tests/ansible/decs_image_build.yml \ + -e target_hosts=lab10 \ + -e image_tag=cuda12.8-tf2.20-ubuntu22.04-260515 \ + -e base_image=nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04 \ + -e decs_image_variant=cuda12.8-tf2.20-ubuntu22.04 \ + -e cuda_version=12.8 \ + -e tensorflow_version=2.20.0 \ + -e tensorflow_package=tensorflow==2.20.0 \ + -e min_nvidia_driver=570.124.06 + +ansible-playbook \ + -i /home/jy/ansible/inventory.ini \ + tests/ansible/decs_image_smoke.yml \ + -e target_hosts=lab10 \ + -e image_tag=cuda12.8-tf2.20-ubuntu22.04-260515 +``` + +VNC까지 확인: + +```bash +ansible-playbook \ + -i /home/jy/ansible/inventory.ini \ + tests/ansible/decs_image_smoke.yml \ + -e target_hosts=lab10 \ + -e image_tag=cuda12.5-tf2.20-ubuntu22.04-260515 \ + -e enable_vnc=true +``` +## Admin Notes -


-### 🔗 관리자용 노션 문서 링크 +관리자용 노션 문서: https://www.notion.so/DECS-280c7692a263802ca40ff68b38f58dd1?source=copy_link diff --git a/entrypoint.sh b/entrypoint.sh index a060492..1089466 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,15 +1,65 @@ #!/bin/bash +CONDA_DIR="${CONDA_DIR:-/opt/conda}" +JUPYTER_BIN="${JUPYTER_BIN:-$CONDA_DIR/bin/jupyter}" USER_PW="${USER_PW:-ailab2260}" -start_novnc() { - case "${ENABLE_VNC:-false}" in - true|TRUE|1|yes|YES|on|ON) ;; - *) - echo "VNC/noVNC disabled. Set ENABLE_VNC=true to enable it." - return 0 - ;; +is_truthy() { + case "${1:-}" in + true|TRUE|1|yes|YES|on|ON) return 0 ;; + *) return 1 ;; esac +} + +version_ge() { + local current="$1" + local required="$2" + [[ "$(printf "%s\n%s\n" "$required" "$current" | sort -V | head -n1)" == "$required" ]] +} + +print_image_runtime_info() { + echo "DECS image variant: ${DECS_IMAGE_VARIANT:-unknown}" + echo "DECS CUDA version: ${DECS_CUDA_VERSION:-unknown}" + echo "DECS TensorFlow version: ${DECS_TENSORFLOW_VERSION:-unknown}" + echo "DECS minimum NVIDIA driver: ${DECS_MIN_NVIDIA_DRIVER:-unknown}" + + if ! command -v nvidia-smi >/dev/null 2>&1; then + echo "nvidia-smi not found. GPU runtime may not be attached." + return 0 + fi + + nvidia-smi --query-gpu=name,driver_version --format=csv,noheader || true + + local required_driver="${DECS_MIN_NVIDIA_DRIVER:-}" + if [[ -z "$required_driver" ]]; then + return 0 + fi + + local host_driver + host_driver="$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -n1 | tr -d '[:space:]')" + if [[ -z "$host_driver" ]]; then + echo "Could not read NVIDIA driver version from nvidia-smi." + return 0 + fi + + if version_ge "$host_driver" "$required_driver"; then + return 0 + fi + + local message="Host NVIDIA driver $host_driver is lower than required $required_driver for ${DECS_IMAGE_VARIANT:-this image}." + if is_truthy "${STRICT_CUDA_COMPAT:-false}"; then + echo "ERROR: $message" + return 1 + fi + + echo "WARNING: $message Set STRICT_CUDA_COMPAT=true to fail startup." +} + +start_novnc() { + if ! is_truthy "${ENABLE_VNC:-false}"; then + echo "VNC/noVNC disabled. Set ENABLE_VNC=true to enable it." + return 0 + fi local user_home="/home/$USER_ID" local vnc_dir="$user_home/.vnc" @@ -27,7 +77,7 @@ start_novnc() { fi local vnc_port=$((5900 + vnc_display)) - if ! command -v vncserver >/dev/null 2>&1 || ! command -v websockify >/dev/null 2>&1; then + if ! command -v vncserver >/dev/null 2>&1 || ! command -v vncpasswd >/dev/null 2>&1 || ! command -v websockify >/dev/null 2>&1; then echo "VNC/noVNC packages are not installed. Skipping GUI startup." return 0 fi @@ -90,8 +140,7 @@ EOF echo "noVNC listening on port $novnc_port. VNC password saved to $vnc_password_file" } -sudo apt update -sudo apt install -y auditd +print_image_runtime_info || exit 1 # /etc/audit/audit.rules 파일에 줄 추가 echo "-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid=$USER_ID -k rm_commands" >> /etc/audit/audit.rules @@ -199,8 +248,11 @@ mkdir -p /home/$USER_ID/.jupyter/ if [ ! -f /home/$USER_ID/.jupyter/jupyter_notebook_config.py ]; then echo "jupyter_notebook_config.py not found, generating..." - /opt/anaconda3/bin/jupyter notebook --generate-config - cp /root/.jupyter/jupyter_notebook_config.py /home/$USER_ID/.jupyter/ + if [ -f /jupyter_config/jupyter_notebook_config.py ]; then + cp /jupyter_config/jupyter_notebook_config.py /home/$USER_ID/.jupyter/jupyter_notebook_config.py + else + "$JUPYTER_BIN" notebook --generate-config --config=/home/$USER_ID/.jupyter/jupyter_notebook_config.py + fi else echo "jupyter_notebook_config.py already exists." fi @@ -216,7 +268,7 @@ chown $USER_ID:$USER_ID /home/$USER_ID/decs_jupyter_lab/jupyter_token.txt # jupyter_lab 기동 echo "trying jupyter lab..." -nohup /opt/anaconda3/bin/jupyter lab --NotebookApp.token=$TOKEN --config=/home/$USER_ID/.jupyter/jupyter_notebook_config.py >/dev/null 2>&1 & +nohup "$JUPYTER_BIN" lab --NotebookApp.token=$TOKEN --config=/home/$USER_ID/.jupyter/jupyter_notebook_config.py >/dev/null 2>&1 & echo "jupyter lab listening!" # noVNC 기동. 외부에서는 컨테이너의 6080 포트를 매핑해서 접속합니다. diff --git a/image-variants.json b/image-variants.json new file mode 100644 index 0000000..9c59ab5 --- /dev/null +++ b/image-variants.json @@ -0,0 +1,70 @@ +{ + "repository": "dguailab/decs", + "default_date_tag": "260515", + "notes": [ + "TensorFlow 2.20.0 is officially built against CUDA 12.5 and cuDNN 9.3.", + "The CUDA 12.8 variant is provided as experimental until LAB10/H200 smoke tests pass." + ], + "variants": [ + { + "id": "cuda11.8-tf2.13-ubuntu22.04", + "base_image": "nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04", + "cuda_version": "11.8", + "tensorflow_version": "2.13.0", + "tensorflow_package": "tensorflow==2.13.0", + "python_version": "3.10", + "ubuntu_version": "22.04", + "min_nvidia_driver": "520.61.05", + "support": "stable", + "aliases": [ + "cuda11.8-tf2.13", + "legacy" + ] + }, + { + "id": "cuda12.2-tf2.15-ubuntu22.04", + "base_image": "nvidia/cuda:12.2.2-cudnn8-devel-ubuntu22.04", + "cuda_version": "12.2", + "tensorflow_version": "2.15.0", + "tensorflow_package": "tensorflow==2.15.0", + "python_version": "3.10", + "ubuntu_version": "22.04", + "min_nvidia_driver": "535.104.05", + "support": "stable", + "aliases": [ + "cuda12.2-tf2.15" + ] + }, + { + "id": "cuda12.5-tf2.20-ubuntu22.04", + "base_image": "nvidia/cuda:12.5.1-cudnn-devel-ubuntu22.04", + "cuda_version": "12.5", + "tensorflow_version": "2.20.0", + "tensorflow_package": "tensorflow==2.20.0", + "python_version": "3.10", + "ubuntu_version": "22.04", + "min_nvidia_driver": "555.42.06", + "support": "stable", + "aliases": [ + "cuda12.5-tf2.20", + "stable", + "latest" + ] + }, + { + "id": "cuda12.8-tf2.20-ubuntu22.04", + "base_image": "nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04", + "cuda_version": "12.8", + "tensorflow_version": "2.20.0", + "tensorflow_package": "tensorflow==2.20.0", + "python_version": "3.10", + "ubuntu_version": "22.04", + "min_nvidia_driver": "570.124.06", + "support": "experimental", + "aliases": [ + "cuda12.8-tf2.20", + "h200-experimental" + ] + } + ] +} diff --git a/scripts/build_variants.py b/scripts/build_variants.py new file mode 100755 index 0000000..e70c56d --- /dev/null +++ b/scripts/build_variants.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +import argparse +import json +import shlex +import subprocess +from pathlib import Path + +from variant_matrix import build_tags, load_manifest + + +BUILD_ARG_KEYS = [ + ("BASE_IMAGE", "base_image"), + ("DECS_IMAGE_VARIANT", "id"), + ("CUDA_VERSION", "cuda_version"), + ("TENSORFLOW_VERSION", "tensorflow_version"), + ("TENSORFLOW_PACKAGE", "tensorflow_package"), + ("PYTHON_VERSION", "python_version"), + ("UBUNTU_VERSION", "ubuntu_version"), + ("MIN_NVIDIA_DRIVER", "min_nvidia_driver"), +] + + +def select_variants(manifest, selected): + variants = manifest["variants"] + if not selected: + return variants + matches = [variant for variant in variants if variant["id"] == selected] + if not matches: + raise SystemExit(f"variant not found: {selected}") + return matches + + +def build_command(variant, repository, date_tag, no_cache): + cmd = ["docker", "build", "-f", "Dockerfile"] + if no_cache: + cmd.append("--no-cache") + + for docker_arg, key in BUILD_ARG_KEYS: + cmd.extend(["--build-arg", f"{docker_arg}={variant[key]}"]) + + for tag in build_tags(repository, variant, date_tag): + cmd.extend(["-t", tag]) + + cmd.append(".") + return cmd + + +def main(): + parser = argparse.ArgumentParser(description="Build DECS Docker image variants.") + parser.add_argument("--manifest", default="image-variants.json") + parser.add_argument("--repository") + parser.add_argument("--date-tag") + parser.add_argument("--variant") + parser.add_argument("--push", action="store_true") + parser.add_argument("--no-cache", action="store_true") + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + manifest = load_manifest(repo_root / args.manifest) + repository = args.repository or manifest["repository"] + date_tag = args.date_tag or manifest["default_date_tag"] + + for variant in select_variants(manifest, args.variant): + tags = build_tags(repository, variant, date_tag) + cmd = build_command(variant, repository, date_tag, args.no_cache) + print(shlex.join(cmd)) + if not args.dry_run: + subprocess.run(cmd, cwd=repo_root, check=True) + + if args.push: + for tag in tags: + push_cmd = ["docker", "push", tag] + print(shlex.join(push_cmd)) + if not args.dry_run: + subprocess.run(push_cmd, check=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_image_variants.py b/scripts/test_image_variants.py new file mode 100755 index 0000000..25eb204 --- /dev/null +++ b/scripts/test_image_variants.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +import argparse +import shlex +import subprocess +from pathlib import Path + +from variant_matrix import build_tags, load_manifest + + +CPU_SMOKE = r""" +set -euo pipefail +command -v python +command -v jupyter +command -v micromamba +test -x /entrypoint.sh +python - <<'PY' +import os +import tensorflow as tf + +expected = os.environ.get("DECS_TENSORFLOW_VERSION") +actual = tf.__version__.split("+", 1)[0] +print("tensorflow", tf.__version__) +print("expected", expected) +if expected and actual != expected: + raise SystemExit(f"TensorFlow version mismatch: {actual} != {expected}") +PY +jupyter --version +micromamba --version +""" + +GPU_SMOKE = r""" +set -euo pipefail +nvidia-smi +python - <<'PY' +import tensorflow as tf + +gpus = tf.config.list_physical_devices("GPU") +print("tensorflow", tf.__version__) +print("gpus", gpus) +if not gpus: + raise SystemExit("TensorFlow did not detect a GPU") +PY +""" + + +def select_variants(manifest, selected): + if not selected: + return manifest["variants"] + matches = [variant for variant in manifest["variants"] if variant["id"] == selected] + if not matches: + raise SystemExit(f"variant not found: {selected}") + return matches + + +def run(cmd, dry_run): + print(shlex.join(cmd)) + if not dry_run: + subprocess.run(cmd, check=True) + + +def main(): + parser = argparse.ArgumentParser(description="Run local smoke tests for DECS image variants.") + parser.add_argument("--manifest", default="image-variants.json") + parser.add_argument("--repository") + parser.add_argument("--date-tag") + parser.add_argument("--variant") + parser.add_argument("--gpu", action="store_true", help="Require GPU visibility through Docker.") + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + manifest = load_manifest(repo_root / args.manifest) + repository = args.repository or manifest["repository"] + date_tag = args.date_tag or manifest["default_date_tag"] + + for variant in select_variants(manifest, args.variant): + image = build_tags(repository, variant, date_tag)[0] + run(["docker", "image", "inspect", image], args.dry_run) + + cmd = ["docker", "run", "--rm", "--entrypoint", "bash"] + if args.gpu: + cmd.extend(["--gpus", "all"]) + cmd.extend([image, "-lc", CPU_SMOKE]) + run(cmd, args.dry_run) + + if args.gpu: + run(["docker", "run", "--rm", "--gpus", "all", "--entrypoint", "bash", image, "-lc", GPU_SMOKE], args.dry_run) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_uid_create_container.py b/scripts/test_uid_create_container.py new file mode 100755 index 0000000..08b5c8f --- /dev/null +++ b/scripts/test_uid_create_container.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +import argparse +import re +import shlex +import subprocess +from datetime import date, timedelta +from pathlib import Path + +from variant_matrix import build_tags, load_manifest + + +def select_variants(manifest, selected): + if not selected: + return manifest["variants"] + matches = [variant for variant in manifest["variants"] if variant["id"] == selected] + if not matches: + raise SystemExit(f"variant not found: {selected}") + return matches + + +def safe_name(value): + return re.sub(r"[^a-zA-Z0-9]", "", value).lower()[:24] + + +def main(): + parser = argparse.ArgumentParser( + description="Exercise ~/uid/script_test/create_container.sh with DECS image tags." + ) + parser.add_argument("--manifest", default="image-variants.json") + parser.add_argument("--repository", default="dguailab/decs") + parser.add_argument("--date-tag") + parser.add_argument("--variant") + parser.add_argument("--uid-root", default="/home/jy/uid") + parser.add_argument("--domain", default="LAB") + parser.add_argument("--server-number", default="10") + parser.add_argument("--created-by", default="decs-test") + parser.add_argument("--email", default="decs-smoke@example.invalid") + parser.add_argument("--phone", default="000-0000-0000") + parser.add_argument("--enable-vnc", action="store_true") + parser.add_argument("--print-only", action="store_true") + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[1] + manifest = load_manifest(repo_root / args.manifest) + date_tag = args.date_tag or manifest["default_date_tag"] + uid_script = Path(args.uid_root) / "script_test" / "create_container.sh" + if not uid_script.exists(): + raise SystemExit(f"uid dry-run wrapper not found: {uid_script}") + + repository_name = args.repository.rsplit("/", 1)[-1] + expiration = (date.today() + timedelta(days=7)).isoformat() + + for variant in select_variants(manifest, args.variant): + tag = build_tags(args.repository, variant, date_tag)[0].split(":", 1)[1] + username = f"decs{safe_name(variant['id'])}" + cmd = [ + str(uid_script), + "--name", + "DECS Smoke Test", + "--username", + username, + "--no-group", + "--domain", + args.domain, + "--server-number", + str(args.server_number), + "--expiration-date", + expiration, + "--image", + repository_name, + "--version", + tag, + "--no-container-name", + "--no-additional-ports", + "--created-by", + args.created_by, + "--email", + args.email, + "--phone", + args.phone, + "--note", + f"DECS image dry-run smoke for {variant['id']}", + "--user-password", + "DecsTest1234", + ] + if args.enable_vnc: + cmd.extend(["--enable-vnc", "true", "--vnc-password", "DecsVNC1"]) + + print(shlex.join(cmd)) + if not args.print_only: + subprocess.run(cmd, check=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/variant_matrix.py b/scripts/variant_matrix.py new file mode 100755 index 0000000..4dcf69b --- /dev/null +++ b/scripts/variant_matrix.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +from pathlib import Path + + +def load_manifest(path): + with Path(path).open(encoding="utf-8") as f: + manifest = json.load(f) + + seen = set() + for variant in manifest["variants"]: + variant_id = variant["id"] + if variant_id in seen: + raise SystemExit(f"duplicate variant id: {variant_id}") + seen.add(variant_id) + return manifest + + +def build_tags(repository, variant, date_tag): + tags = [f"{repository}:{variant['id']}-{date_tag}"] + tags.extend(f"{repository}:{alias}" for alias in variant.get("aliases", [])) + return tags + + +def build_matrix(manifest, repository, date_tag, selected_variant=None): + include = [] + for variant in manifest["variants"]: + if selected_variant and variant["id"] != selected_variant: + continue + + item = dict(variant) + item["docker_tags"] = "\n".join(build_tags(repository, variant, date_tag)) + include.append(item) + + if not include: + raise SystemExit(f"variant not found: {selected_variant}") + + return {"include": include} + + +def write_github_output(name, value): + output_path = os.environ.get("GITHUB_OUTPUT") + if not output_path: + print(f"{name}={value}") + return + + with Path(output_path).open("a", encoding="utf-8") as f: + if "\n" in value: + f.write(f"{name}< + rm -rf {{ remote_build_dir | quote }} + && mkdir -p {{ remote_build_dir | quote }} + + - name: Upload build context archive + ansible.builtin.copy: + src: "{{ build_context_archive }}" + dest: "{{ build_context_archive }}" + mode: "0644" + + - name: Extract build context archive + ansible.builtin.shell: > + tar -xzf {{ build_context_archive | quote }} + -C {{ remote_build_dir | quote }} + + - name: Build DECS image variant + ansible.builtin.shell: | + set -euo pipefail + docker build \ + -f Dockerfile \ + --build-arg BASE_IMAGE={{ base_image | quote }} \ + --build-arg DECS_IMAGE_VARIANT={{ decs_image_variant | quote }} \ + --build-arg CUDA_VERSION={{ cuda_version | quote }} \ + --build-arg TENSORFLOW_VERSION={{ tensorflow_version | quote }} \ + --build-arg TENSORFLOW_PACKAGE={{ tensorflow_package | quote }} \ + --build-arg PYTHON_VERSION={{ python_version | quote }} \ + --build-arg UBUNTU_VERSION={{ ubuntu_version | quote }} \ + --build-arg MIN_NVIDIA_DRIVER={{ min_nvidia_driver | quote }} \ + -t {{ image_repository }}:{{ image_tag }} \ + . + args: + chdir: "{{ remote_build_dir }}" + executable: /bin/bash diff --git a/tests/ansible/decs_image_smoke.yml b/tests/ansible/decs_image_smoke.yml new file mode 100644 index 0000000..e02ddcf --- /dev/null +++ b/tests/ansible/decs_image_smoke.yml @@ -0,0 +1,102 @@ +--- +- name: Smoke test a DECS image on GPU Docker hosts + hosts: "{{ target_hosts | default('lab10') }}" + gather_facts: false + vars: + image_repository: dguailab/decs + image_tag: cuda12.5-tf2.20-ubuntu22.04-260515 + test_container_name: "decs-smoke-{{ image_tag | regex_replace('[^A-Za-z0-9_.-]', '-') }}" + test_home_root: /tmp/decs-smoke-home + test_username: decstest + test_group: decstest + test_uid: "31000" + test_gid: "31000" + test_memory: 8g + enable_vnc: false + strict_cuda_compat: true + + tasks: + - name: Run smoke lifecycle + block: + - name: Prepare smoke home mount + ansible.builtin.shell: "mkdir -p {{ test_home_root | quote }}" + + - name: Ensure image exists locally or can be pulled + ansible.builtin.shell: > + docker image inspect {{ image_repository }}:{{ image_tag }} >/dev/null 2>&1 + || docker pull {{ image_repository }}:{{ image_tag }} + + - name: Remove old smoke container + ansible.builtin.shell: "docker rm -f {{ test_container_name | quote }} >/dev/null 2>&1 || true" + + - name: Run smoke container with create_container.sh-compatible options + ansible.builtin.shell: > + docker run -dit + --gpus device=all + --memory={{ test_memory }} + --memory-swap={{ test_memory }} + --runtime=nvidia + --cap-add=SYS_ADMIN + --ipc=host + --mount type=bind,source={{ test_home_root | quote }},target=/home/ + --name {{ test_container_name | quote }} + -e USER_ID={{ test_username | quote }} + -e GID={{ test_gid | quote }} + -e USER_PW='DecsTest1234' + -e USER_GROUP={{ test_group | quote }} + -e UID={{ test_uid | quote }} + -e ENABLE_VNC={{ (enable_vnc | bool) | ternary('true', 'false') | quote }} + -e STRICT_CUDA_COMPAT={{ (strict_cuda_compat | bool) | ternary('true', 'false') | quote }} + -e NVIDIA_DRIVER_CAPABILITIES='compute,utility,graphics,display' + {{ image_repository }}:{{ image_tag }} + + - name: Wait for entrypoint to create user assets + ansible.builtin.shell: > + timeout 90 bash -lc + 'until docker exec {{ test_container_name | quote }} test -f /home/{{ test_username }}/decs_jupyter_lab/jupyter_token.txt; + do sleep 3; done' + + - name: Validate shell, conda, Jupyter, TensorFlow, and GPU visibility + ansible.builtin.shell: | + set -euo pipefail + docker exec {{ test_container_name | quote }} bash -lc ' + set -euo pipefail + id {{ test_username | quote }} + test -x /entrypoint.sh + command -v python + command -v jupyter + command -v micromamba + nvidia-smi + python - <<'"'"'PY'"'"' + import tensorflow as tf + print("tensorflow", tf.__version__) + gpus = tf.config.list_physical_devices("GPU") + print("gpus", gpus) + if not gpus: + raise SystemExit("TensorFlow did not detect a GPU") + PY + ' + args: + executable: /bin/bash + + - name: Validate noVNC process when enabled + ansible.builtin.shell: > + docker exec {{ test_container_name | quote }} + bash -lc 'test -f /home/{{ test_username }}/vnc_password.txt && pgrep -f websockify' + when: enable_vnc | bool + + always: + - name: Show smoke container logs + ansible.builtin.shell: "docker logs --tail 120 {{ test_container_name | quote }}" + register: smoke_logs + changed_when: false + failed_when: false + + - name: Print smoke logs + ansible.builtin.debug: + var: smoke_logs.stdout_lines + + - name: Cleanup smoke container + ansible.builtin.shell: "docker rm -f {{ test_container_name | quote }} >/dev/null 2>&1 || true" + changed_when: false + failed_when: false