1. 배경 (Background)
당신은 GPU 서버 관리팀의 일원입니다. 최근 교내 정전 사태로 인해 서버 전체가 재부팅되는 사고가 있었습니다. 이 과정에서 Docker 컨테이너들이 재시작되었으나, DB에 저장된 설정 정보와 실제 구동 중인 컨테이너 상태 간에 데이터 불일치(Port, UID/GID 등)가 발생했습니다.
우선 수동으로 복구를 완료했으나, 향후 동일한 사태를 대비해 동료 A와 함께 자동화 스크립트(sync_containers.sh)를 작성하는 업무를 맡았습니다. 동료 A는 빠르게 스크립트를 작성하고, 당신에게 전달해주었습니다. 당신의 임무는 이 스크립트를 다른 동료도 쉽게 사용할 수 있도록 기술 문서를 작성하여 배포하는 것입니다.
2. 목표 (Objectives)
동료 A가 작성한 쉘 스크립트 코드를 분석하고, 다른 팀원들도 이 도구를 이해하고 사용할 수 있도록 하기 위한 기술 문서를 노션으로 작성하십시오.
3. 필수 포함 항목
제출하는 PDF 문서에는 다음의 내용이 반드시 포함되어야 합니다. (목차 구성은 자유입니다.)
- 스크립트 개요: 이 스크립트가 해결하고자 하는 문제와 핵심 동작 로직(Flow) 설명
- 사용 가이드:
- 사용법 및 옵션 설명 (-dry-run 등)
- 코드 분석:
- 주요 함수 또는 로직 블록별 기능 설명
4. 첨부 자료 (Source Code)
- 파일명: sync_containers.sh
- 작성자: 동료 A
#!/bin/bash
# ==========================================================
# sync_containers.sh
# DB 기준으로 컨테이너 상태 동기화
# 이미지/버전/UID/GID/포트 불일치 시 자동 recreate / 정지된 컨테이너 재시작
# – –dry-run: 실제 실행하지 않고 시뮬레이션만
# – –auto-delete: DB에 없는 컨테이너 자동 삭제
# ==========================================================
DB_ADDRESS=192.168.2.11
DB_PORT=3307
DB_NAME=”nfs_db”
DB_USER=”nfs_user”
DB_PASSWORD=”nfs_password”
DRY_RUN=false
AUTO_DELETE=false
# 옵션 파싱
for arg in “$@”; do
case “$arg” in
–dry-run) DRY_RUN=true ;;
–auto-delete) AUTO_DELETE=true ;;
esac
done
echo “[INFO] Using DB: $DB_NAME at $DB_ADDRESS:$DB_PORT”
echo “[INFO] Options: dry-run=$DRY_RUN, auto-delete=$AUTO_DELETE”
# MySQL 접속 설정 파일 생성
cat <<EOF > ~/.my.cnf
[client]
user=$DB_USER
password=$DB_PASSWORD
host=$DB_ADDRESS
port=$DB_PORT
EOF
chmod 600 ~/.my.cnf
# DB에서 컨테이너 목록 불러오기
containers=$(mysql -N -D $DB_NAME -e “
SELECT dc.container_name, dc.image, dc.image_version,
u.ubuntu_username, u.ubuntu_uid, u.ubuntu_gid,
dc.server_id, dc.id
FROM docker_container dc
JOIN user u
ON dc.user_id=u.id
WHERE dc.existing=1;
“)
echo “[INFO] Loaded $(echo “$containers” | wc -l) containers from DB.”
# 서버 컨테이너 상태 확인
docker ps -a –format “{{.Names}} {{.Status}}” > /tmp/docker_status.txt
while read -r cname image version uname uid gid sid dbid; do
server_status=$(grep -w “$cname” /tmp/docker_status.txt | awk ‘{print $2}’)
# (1) DB에 있고 서버에는 없는 경우
if [ -z “$server_status” ]; then
echo “[CREATE] $cname (image=$image:$version, user=$uname)”
ports=$(mysql -N -D $DB_NAME -e “
SELECT port_number, purpose_of_use FROM used_ports
WHERE docker_container_record_id=$dbid;
“)
port_args=””
while read -r port purpose; do
[ -z “$port” ] && continue
if ss -tulpn | grep -q “:$port “; then
echo “[SKIP] Port $port ($purpose) already in use”
continue
fi
case “$purpose” in
ssh) port_args=”$port_args -p ${port}:22″ ;;
“jupyter notebook”) port_args=”$port_args -p ${port}:8888″ ;;
*) port_args=”$port_args -p ${port}:${port}” ;;
esac
done <<< “$ports”
if $DRY_RUN; then
echo “[DRY-RUN] docker run -dit $port_args –name $cname …”
else
docker run -dit \\
–name “$cname” \\
$port_args \\
-e USER_ID=$uname -e UID=$uid -e GID=$gid \\
dguailab/$image:$version
fi
continue
fi
# (2) 서버에는 있는데 정지된 경우
if [ “$server_status” == “Exited” ]; then
if $DRY_RUN; then
echo “[DRY-RUN] restart $cname”
else
echo “[RESTART] $cname”
docker start “$cname”
fi
continue
fi
# (3) 서버/DB 세부정보 비교 (이미지/UID/GID/포트)
mismatch=false
actual_image=$(docker inspect –format ‘{{.Config.Image}}’ “$cname” 2>/dev/null)
if [ “$actual_image” != “dguailab/$image:$version” ]; then
echo “[MISMATCH] Image differs: DB=$image:$version, Actual=$actual_image”
mismatch=true
fi
actual_uid=$(docker inspect –format ‘{{range .Config.Env}}{{println .}}{{end}}’ “$cname” | grep ‘^UID=’ | cut -d= -f2)
actual_gid=$(docker inspect –format ‘{{range .Config.Env}}{{println .}}{{end}}’ “$cname” | grep ‘^GID=’ | cut -d= -f2)
if [ “$actual_uid” != “$uid” ] || [ “$actual_gid” != “$gid” ]; then
echo “[MISMATCH] UID/GID differs: DB=$uid/$gid, Actual=$actual_uid/$actual_gid”
mismatch=true
fi
db_ports=$(mysql -N -D $DB_NAME -e “
SELECT port_number FROM used_ports
WHERE docker_container_record_id=$dbid;
” | sort)
actual_ports_sorted=$(docker inspect “$cname” \\
| jq -r ‘[.[] | .NetworkSettings.Ports | to_entries[] | .value[]?.HostPort] | unique | .[]’ \\
| sort -n | tr ‘\\n’ ‘ ‘ | sed ‘s/ *$//’)
if [ “$db_ports” != “$actual_ports” ]; then
echo “[MISMATCH] Ports differ”
echo ” DB: $db_ports”
echo ” Actual: $actual_ports”
mismatch=true
fi
# 불일치 시 재생성
if $mismatch; then
if $DRY_RUN; then
echo “[DRY-RUN] Would recreate $cname”
else
echo “[RECREATE] $cname”
docker rm -f “$cname”
ports=$(mysql -N -D $DB_NAME -e “
SELECT port_number, purpose_of_use FROM used_ports
WHERE docker_container_record_id=$dbid;
“)
port_args=””
while read -r port purpose; do
[ -z “$port” ] && continue
case “$purpose” in
ssh) port_args=”$port_args -p ${port}:22″ ;;
“jupyter notebook”) port_args=”$port_args -p ${port}:8888″ ;;
*) port_args=”$port_args -p ${port}:${port}” ;;
esac
done <<< “$ports”
docker run -dit \\
–name “$cname” \\
$port_args \\
-e USER_ID=$uname -e UID=$uid -e GID=$gid \\
dguailab/$image:$version
fi
else
echo “[OK] $cname is running and matches DB”
fi
done <<<“$containers”
# (4) 서버에 있고 DB에는 없는 경우
server_only=$(comm -23 <(awk ‘{print $1}’ /tmp/docker_status.txt | sort) \\
<(echo “$containers” | awk ‘{print $1}’ | sort))
if [ -n “$server_only” ]; then
if $AUTO_DELETE; then
for cname in $server_only; do
if $DRY_RUN; then
echo “[DRY-RUN] Would delete $cname (not in DB)”
else
echo “[DELETE] $cname (not in DB)”
docker rm -f “$cname”
fi
done
else
echo “[WARN] Containers on server but not in DB:”
echo “$server_only”
fi
fi
echo “[DONE] Sync completed.”
Cf. 위 source 코드를 이해하기 위한 추가자료
- MySQL 테이블 및 뷰 정의 sql
— Create the database with explicit character set
CREATE DATABASE IF NOT EXISTS nfs_db CHARACTER
SET
= utf8mb4 COLLATE = utf8mb4_unicode_ci;
USE nfs_db;
— Create used_ids table for ID management
CREATE TABLE
used_ids (id INT PRIMARY KEY AUTO_INCREMENT) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Create group table
CREATE TABLE
`group` (
id INT PRIMARY KEY AUTO_INCREMENT,
ubuntu_groupname VARCHAR(255) NOT NULL,
ubuntu_gid INT NOT NULL,
UNIQUE KEY unique_gid (ubuntu_gid),
FOREIGN KEY (ubuntu_gid) REFERENCES used_ids (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Create user table without circular references
CREATE TABLE
user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
ubuntu_username VARCHAR(255) NOT NULL,
ubuntu_uid INT NOT NULL,
ubuntu_gid INT,
note TEXT,
UNIQUE KEY unique_uid (ubuntu_uid),
FOREIGN KEY (ubuntu_uid) REFERENCES used_ids (id),
FOREIGN KEY (ubuntu_gid) REFERENCES `group` (ubuntu_gid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Create docker_container table
CREATE TABLE
docker_container (
id INT PRIMARY KEY AUTO_INCREMENT,
image VARCHAR(255) NOT NULL,
image_version VARCHAR(50) NOT NULL,
container_id VARCHAR(64) NOT NULL,
container_name VARCHAR(255) NOT NULL,
server_id VARCHAR(255) NOT NULL,
expiring_at DATETIME NOT NULL,
deleted_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
existing BOOLEAN DEFAULT TRUE,
created_by VARCHAR(255),
user_id INT,
UNIQUE KEY unique_container (container_id),
FOREIGN KEY (user_id) REFERENCES user (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Create used_ports table after docker_container exists
CREATE TABLE
used_ports (
port_number INT PRIMARY KEY,
docker_container_record_id INT,
purpose_of_use VARCHAR(255),
FOREIGN KEY (docker_container_record_id) REFERENCES docker_container (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Add indexes
CREATE INDEX idx_container_existing ON docker_container (existing);
CREATE INDEX idx_container_expiring ON docker_container (expiring_at);
CREATE INDEX idx_user_username ON user (ubuntu_username);
— Verify character set settings
SET
NAMES utf8mb4;
CREATE VIEW
user_container_info AS
SELECT
u.name AS ‘사용자 이름’,
u.ubuntu_username AS ‘우분투 아이디’,
g.ubuntu_groupname AS ‘우분투 그룹 이름’,
dc.server_id AS ‘배정된 서버’,
(
SELECT
up.port_number
FROM
used_ports up
WHERE
up.docker_container_record_id = dc.id
AND up.purpose_of_use = ‘ssh’
) AS ‘ssh 포트’,
(
SELECT
up.port_number
FROM
used_ports up
WHERE
up.docker_container_record_id = dc.id
AND up.purpose_of_use = ‘jupyter notebook’
) AS ‘jupyter 포트’,
(
SELECT
GROUP_CONCAT (up.port_number) # 오류 : 빨간색 삭제
FROM
used_ports up
WHERE
up.docker_container_record_id = dc.id
AND up.purpose_of_use != ‘ssh’
AND up.purpose_of_use != ‘jupyter notebook’
) AS ‘할당된 다른 포트’,
dc.expiring_at AS ‘사용 만료일’,
dc.created_by AS ‘컨테이너 생성한 관리자’,
dc.created_at AS ‘컨테이너 생성 일자’,
dc.image AS ‘컨테이너 이미지’,
dc.image_version AS ‘컨테이너 버전’,
dc.container_name AS ‘컨테이너 이름’,
u.note AS ‘노트’
FROM
user u
LEFT JOIN `group` g ON u.ubuntu_gid = g.ubuntu_gid
JOIN docker_container dc ON u.id = dc.user_id
WHERE
dc.existing = TRUE
ORDER BY
dc.server_id ASC,
u.name ASC;
제공해주신 전체 스크립트를 기반으로 **완전한 기술 문서(노션에 바로 붙여넣기 가능한 형태)**를 작성해드리겠습니다.
📘 sync_containers.sh 기술 문서
GPU 서버 관리팀 내부용
Docker 컨테이너 상태 자동 동기화 스크립트
🧩 1. 개요 (Overview)
최근 정전으로 인한 서버 전체 재부팅 과정에서 Docker 컨테이너 런타임 상태와
DB에 저장된 설정 정보(이미지 버전, 포트, UID/GID 등) 간 불일치 현상이 발생했습니다.
이 문제를 해결하기 위해
DB를 기준으로 Docker 환경을 자동 동기화하는 도구인 sync_containers.sh를 개발했습니다.
🎯 2. 주요 기능 (Key Features)
✔ DB를 기준으로 컨테이너 정보 읽기
- 컨테이너 이름
- 이미지 / 버전
- UID / GID
- 할당된 포트 정보
✔ 서버의 실제 Docker 컨테이너와 비교
- 이미지 버전 비교
- UID/GID 비교
- 포트 매핑 비교
- 컨테이너 실행 여부 확인
✔ 불일치 발생 시
- 컨테이너 자동 재생성(recreate)
- 정지된 컨테이너 자동 재시작
✔ 추가 옵션
| 옵션 | 설명 |
| –dry-run | 실행하지 않고 어떤 작업이 수행되는지 출력만 함 |
| –auto-delete | DB에 없는 ‘고아 컨테이너’를 서버에서 자동 삭제 |
⚙️ 3. 스크립트 구조 요약
1. 환경변수( DB 정보 / 옵션 ) 설정
2. 명령행 옵션 파싱
3. ~/.my.cnf 자동 생성 (MySQL 인증)
4. DB에서 컨테이너 메타데이터 로드
5. 서버 docker 상태 조회
6. DB 기준으로 다음 항목 점검
– 컨테이너 없음 → 생성
– 컨테이너 정지됨 → 시작
– 이미지/UID/GID/Port mismatch → 재생성
7. 서버에는 있고 DB에는 없는 컨테이너 처리
8. 종료
🏗 4. 설치 방법 (Setup)
필요 패키지 설치
sudo apt install docker.io jq mariadb-client -y
스크립트 권한 부여
chmod +x sync_containers.sh
🚀 5. 실행 방법
5.1 기본 실행
./sync_containers.sh
5.2 드라이런(dry-run)
./sync_containers.sh –dry-run
5.3 DB에 없는 컨테이너 자동 삭제
./sync_containers.sh –auto-delete
두 옵션을 동시에 사용할 수도 있음:
./sync_containers.sh –dry-run –auto-delete
🔐 6. MySQL 자동 인증 설정
스크립트는 실행 시 다음 파일을 자동 생성하여
패스워드 없이 mysql 명령을 수행할 수 있게 합니다.
~/.my.cnf
[client]
user=nfs_user
password=nfs_password
host=192.168.2.11
port=3307
권한 자동 설정:
chmod 600 ~/.my.cnf
🧠 7. 동작 상세 (Detailed Logic)
🔍 7.1 DB에서 컨테이너 목록 조회
SELECT dc.container_name, dc.image, dc.image_version,
u.ubuntu_username, u.ubuntu_uid, u.ubuntu_gid,
dc.server_id, dc.id
FROM docker_container dc
JOIN user u ON dc.user_id=u.id
WHERE dc.existing=1;
DB의 정상 컨테이너 목록을 불러와 스크립트에서 순회 처리합니다.
🔍 7.2 서버 docker 상태 수집
docker ps -a –format “{{.Names}} {{.Status}}” > /tmp/docker_status.txt
컨테이너 이름 + 상태를 가져와 DB 정보와 비교합니다.
🔄 7.3 동기화 로직
✔ (1) DB에는 있지만 서버에는 없는 경우 → 생성
- DB에서 포트 목록 조회
- 사용 중인지 확인
- 포트 바인딩 옵션 생성
- 다음 명령 실행
docker run -dit \
–name <cname> \
-p <port-mapping> \
-e USER_ID=<ubuntu_username> \
-e UID=<uid> \
-e GID=<gid> \
dguailab/<image>:<version>
✔ (2) 서버에는 있으나 정지됨 → 자동 시작
docker start <cname>
✔ (3) 이미지/UID/GID/Port mismatch → 재생성
비교 항목
| 항목 | 비교 방법 |
| 이미지 | docker inspect |
| UID/GID | 컨테이너 환경변수 비교 |
| Port | Docker inspect → jq로 파싱 |
mismatch 발생 시:
- 컨테이너 종료 + 삭제
docker rm -f <cname>
- DB 기준으로 새로 생성
🗑 8. 서버에는 있으나 DB에는 없는 컨테이너 처리
DB 목록
vs
서버 컨테이너 목록
을 비교하여 서버만 가지고 있는 컨테이너를 검사합니다.
기본 동작
- 삭제하지 않고 경고 출력:
[WARN] Containers on server but not in DB:
<container-list>
–auto-delete 사용 시
docker rm -f <container>
📘 9. 전체 Flow Chart
[Start]
↓
[Load DB Containers]
↓
[Check Docker Runtime]
↓
● DB에 있고 서버에 없음? → Create
● 서버에 있으나 Exited? → Start
● 정보 mismatch? → Recreate
● 모두 정상? → OK 표시
↓
[Server-only Containers?]
↓
● auto-delete 옵션? → Delete
● 아니면 Warning 출력
↓
[End]
📄 10. 샘플 실행 로그
Dry-run 예시
[INFO] Using DB: nfs_db at 192.168.2.11:3307
[INFO] Options: dry-run=true, auto-delete=false
[INFO] Loaded 23 containers from DB.
[CREATE] user01_container (image=python:3.10, user=user01)
[DRY-RUN] docker run -dit -p 2201:22 …
[MISMATCH] UID/GID differs: DB=1001/1001, Actual=1000/1000
[DRY-RUN] Would recreate user02_container
[OK] user03_container is running and matches DB
…
[DONE] Sync completed.
🪪 11. 주의사항
- 반드시 root 또는 docker 그룹에서 실행
- Docker 이미지 dguailab/* must exist
- DB 정보는 재부팅 후 변경되지 않도록 .my.cnf를 유지
- 도중 포트 충돌 시 자동 skip
- jq 필수 설치
🕒 12. 자동 실행 설정 (Cron)
매일 새벽 4시에 자동 점검:
crontab -e
0 4 * * * /path/sync_containers.sh >> /var/log/sync_containers.log 2>&1
🎉 문서 완성
해당 문서는 노션에 그대로 붙여넣으면 정리된 포맷으로 잘 표시됩니다.
필요하다면 표·색상·아이콘 추가한 강화 버전도 제작해드릴 수 있습니다.
추가로 다음도 제공 가능합니다:
- Flowchart 이미지 SVG 생성
- 스크립트 개선 버전 (에러 대응 강화)
- 운영팀 교육용 요약본
원하시면 말씀해주세요!
