[s2n] ⭐️⭐️⭐️⭐️⭐️ 도커 이미지 빌드/등록 공부 + 로직 설계
November 17, 2025
1. 배경지식 공부
🐳 1. DockerFile 작성 (DEV/PROD 분리)
두 개의 이미지를 하나의 DockerFile로 관리하거나, 명확히 분리된 두 개의 파일로 관리할 수 있다.
-
멀티 스테이지 빌드
- 하나의 Dockerfile 내에서
FROM ... as base,FROM base as dev,FROM base as prod처럼 여러 빌드 단계 정의 - dev 이미지는 테스트 도구/디버거 등 포함, prod 이미지는 base에서 필요한 런타임 파일만 복사 -> 매우 개볍고 보안성이 높게 만들 수 있음, 공통 의존성은 base 단계에서 처리해 중복 줄임
https://kimjingo.tistory.com/63#google_vignette
- 하나의 Dockerfile 내에서
-
docker build --target- 멀티 스테이지 빌드 사용 -> 특정 스테이지 (ex: dev)까지만 빌드하도록 지정하는 옵션
- 사용법:
docker build --target dev -t s2n:dev ./docker build --target prod -t s2n:latest .`
-
ARG vs ENV
- ARG: 빌드 시점에만 사용되는 변수 (docker build –build-arg … ) DEV/PROD 빌드를 구분하는 플래그를 전달할 때 유용
- ENV: 런타임(컨테이너 실행 시점)에 사용되는 환경 변수. ARG 값을 ENV로 설정할 수 있음 (ex:
ARG BUILD_ENV=prod,ENVAPP_ENV=${BUILD_ENV})
-
.dockerignore:
- 빌드 컨텍스트에 불필요한 파일 (.git, .venv,
__pycache__, 로컬 설정 파일 등)이 포함되지 않도록 해서 빌드 속도를 높이고 이미지 용량을 줄이며 보안을 강화함
- 빌드 컨텍스트에 불필요한 파일 (.git, .venv,
-
레이어 캐시 최적화 (Layer Caching)
- 소스 코드가 변경될 때마다 이를 도커에서 확인해서 이미지를 다시 빌드하고 컨테이너를 생성하는 일은 소모적이다. 각 레이어는 도커 파일의 명령어가 실행 전/후 파일시스템 변경사항을 포함하고 있기 때문에 도커 파일이 실행되면 레이어들은 파일시스템에서 무엇이 변경되었는지/변경되지 않았는지 알고 있다. 그래서 변경사항이 없으면 캐싱된 레이어를 사용한다. (https://velog.io/@kdaeyeop/%EB%8F%84%EC%BB%A4-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%BA%90%EC%8B%B1)
- Dockerfile 명령어 순서의 중요성
COPY requirements.txt->RUN pip install -r requirements.txt-> `COPY . . 순서로 작성해야 소스 코드가 변경되어도 의존성 설치 레이어는 캐시된 버전을 사용함. DEV/PROD 의존성 파일을 분리(requirements-dev.txt)해서 각각 처리하는 것이 좋음.
-
Base Image (python-slim, apline)
- prod 이미지는 python:3.10-slim(필수 라이브러리만 포함)이나 python:3.10-apline(초경량)을 사용해 이미지 크기를 극단적으로 줄이는 것을 고려해야 함 (단 알파인은 C 의존성 문제가 있을 수 있음)
🏗️ 2. Docker Image 빌드 및 관리
-
docker build -t (tag)- 이미지에 이름:태그를 지정한다. (예: `docker build -t s2n:latest .)
- DEV/PROD 태그 확실히 구분해야 함. (s2n:dev, s2n:latest)
-
docker build -f (file)- 멀티 스테이지 대신 Dockerfile.dev, Dockerfile.prod 처럼 두 개의 파일 사용하는 경우 특정 Dockerfile 지정하는 옵션 (ex:
docker build -f Dockerfile.dev -t s2n:dev)
- 멀티 스테이지 대신 Dockerfile.dev, Dockerfile.prod 처럼 두 개의 파일 사용하는 경우 특정 Dockerfile 지정하는 옵션 (ex:
-
빌드 컨텍스트
- docker build .의 .이 의미하는 것으로, 현재 디렉토리의 파일들을 도커 데몬으로 전송하는 것을 의미함.
3. ☁️ Docker Registry 등록 및 배포
빌드한 이미지를 공유하고 사용자가 pull 받을 수 있도록 등록
- Docker Hub: 기본적인 공개 도커 레지스트리
-
GHCR(GitHub Container Registry): 오픈소스 프로젝트에서 많이 사용. GitHub Actions와 연동이 쉬움
- docker login: Docker Hub/GHCR같은 원격 레지스트리에 로그인
-
docker tag (레지스트리용 태그)
- push 하기 전에 이미지를 레지스트리 주소 형식에 맞게 태그를 하나 더 붙여야 함
- ex: docker tag s2n:latest ghcr.io/your-username/s2n:latest
- ex: docker tag s2n:dev ghcr.io/your-username/s2n:dev
- docker push: 태그된 이미지를 원격 레지스트리로 업로드

4. 지금 쓰고 있는 .venv는…?
(1) 현재 .venv의 역할
- 목적: 의존성 격리/버전 관리
- 작동 방식: 호스트 OS의 메인 파이썬과 분리된 독립적인 파이썬 실행 환경 생성
- 효과: s2n 프로젝트에 필요한 pip install 패키지들이 다른 파이썬 프로젝트나 시스템 전체 환경에 영향을 주지 않도록 보장한다.
(2) 가상환경과 Docker 환경의 관계
Docker를 도입하면 컨테이너 환경이 가상 환경의 역할을 대신한다.
1) 격리의 이중화
- 로컬 환경: 호스트 OS -> venv (1차 격리)
- 배포 환경: 호스트 OS -> Docker 컨테이너 (최종 격리) venv는 개발자 PC 내에서만 격리하는 반면 도커는 OS 수준에서 환경 전체를 격리하기 때문에 최종 런타임 환경인 s2n:prod 이미지는 .venv의 영향을 받지 않는다.
2) 의존성 전달 방식
- venv의 역할: 개발자가 코드를 작성하거나, 로컬에서 pytest 등으로 테스트를 실행할 때 필요한 의존성만 관리
- dockerfile의 역할: dockerfile 내부에서 FROM python:3.11-slim을 사용해 새로운 격리 환경을 만들고, requirements.txt에 명시된 목록을 기반으로 pip install을 컨테이너 내부에서 다시 수행함.
➡️ .venv에 설치된 패키지는 requirements.txt 파일을 통해 도커 컨테이너로 전달되는 것이고, .venv의 실제 파일들이 복사되는 것이 아님
5. 팀 내부 논의 결과
- dev, prod 모두 GHCR 사용하기로 결정
- 멀티 스테이지 빌드 사용
- 단일 컨테이너 실행 구조
2. 직접 구축
1. Dockerfile 구현 및 빌드 환경 구축
DEV/pPROD 환경 분리 및 GHCR 등록의 기초. 멀티 스테이지 빌드를 사용해 하나의 Dockerfile로 두 개의 이미지를 생성함.
(1) Dockerfile 작성 원칙
PROD
- FROM python:3.11-slim-bookworm AS prod
- 최소한의 런타임 환경, 작은 이미지 크기
DEV
- FROM python:3.11 AS dev
- 컴파일러, 디버깅 도구 등 개발 편의성 확보
(2) 핵심 구현 사항
- 의존성 분리: requirements.txt (prod용), requirements-dev.txt 분리 / 각 스테이지에서 해당 파일만 설치하도록 해야 함.
- 레이어 캐싱:
COPY requirements*.txt .후RUN pip install실행, 마지막에 소스 코드 COPY ➡️ 개발 중 코드 수정 시 불필요한 의존성 설치를 건너뛰도록 최적화 - 최종 PROD 스테이지: Prod 스테이지에서는 dev 스테이지에 설치된 개발 도구 (Pytest 등)이 최종 이미지에 포함되지 않도록 오직 필요한 런타임 파일만 복사해 와야 함.
-
엔트리포인트: 단일 컨테이너 구조에 맞춰 ENTRYPOINT를 설정해 컨테이너 실행 시 항상 python runner.py가 실행되도록 지정
- .dockerignore 설정
- 빌드 스크립트 (docker build 명령어를 환경별로 정리한 간단한 셸 스크립트) 작성
2. GHCR 연동 및 자동화
(1) 수동 등록
GHCR 로그인 ` echo $PAT | docker login ghcr.io -u YOUR_USERNAME –password0stdin`
이미지 태그: GHCR 레지스트리 형식에 맞게 이미지 태그
docker tag s2n:latest ghcr.io/YOUR_USERNAME/s2n:latest
이미지 푸시: 태그된 이미지를 GHCR로 푸시
docker push ghcr.io/YOUR_USERNAME/s2n:latest
(2) GitHub Actions Workflow 작성
- docker/build-push-action, docker/login-action 사용
3. 런타임 핵심 로직 구현
단일 컨테이너 구조를 위해 runner.py가 모든 플러그인을 컨테이너 내부에서 처리하도록 구현해야 함.
- 플러그인 로딩: runner.py는 외부에서 볼륨 마운트로 넘어온 플러그인 디렉토리를 읽어 들여, 컨테이너 내부에서 동적으로 파이썬 모듈을 로드 (importlib 또는 유사 방식)하고 실행하는 로직을 구현해야 함.
- 스캐너 함수 실행: 모든 스캔 및 플러그인 실행은 runner.py의 제어 하에 단일 컨테이너 프로세스 내에서 완료됨
(1) runner.py
현재 runner.py는 컨테이너 내부에서 실행될 최종 런타임 코드이다.
- CLI Root/Command: runner.py 자체가 CLI 진입점으로, 컨테이너의 ENTRYPOINT가 이 파일을 실행하면 모든 것이 컨테이너 내에서 시작된다.
- 스캐너 실행: Scanner 인스턴스화, scan() 함수 호출이 단일 프로세스 내에서 이루어짐
- 플러그인 로딩: 플러그인이 별도의 컨테이너 없이 scanner.scan() 프로세스 내에서 실행됨
- 의존성: 필요한 모듈이 컨테이너 내부에 설치되었다고 가정하고 로드됨
runner.py 수정 방향
-
플러그인 로딩 로직 검증 -> Scanner 내부 수정 필요
- 코드가 플러그인 파일을 검색하는 경로가 컨테이너 내부의 특정 경로여야 함.
- CLI 래퍼가 사용자의 플러그인 폴더를 컨테이너의 해당 경로에 마운트하도록 설계해야 함.
(2) Scanner
현재 Scanner 클래스는 플러그인 로딩에 대해 두 가지 우선순위를 가진다.
(1) 우선순위 1 (Pre-loaded): 외부에서 __init__시 self.plugins 리스트로 직접 플러그인 인스턴스를 주입받아 사용하는 경우 (동적 파일 시스템 탐색 필요없음)
(2) 우선순위 2 (Package Discovery): pkgutil.iter_modules 사용 -> PLUGIN_PACKAGE = "s2n.s2nscanner.plugins" 패키지 하위 모듈에서 Plugin() 팩토리를 찾아 인스턴스화 하는 경우. 얘는 빌드된 패키지 내부에 포함된 기본 플러그인만 탐색할 수 있다.
Scanner 수정 방향: 외부 플러그인 로드
- Host OS의 로컬 플러그인 폴더를 컨테이너에 마운트하여 사용하기 위해서는, discover_plugins 메서드에 새로운 탐색 경로를 추가해야 한다.
- scan_eninge.py 상단에 외부 플러그인이 마운트될 컨테이너 내부의 경로를 상수로 정의해야 한다. (CLI 래퍼가 마운트할 경로와 일치해야 함)
- discover_plugins에 패키지 탐색(PLUGIN_PACKAGE) 외에 외부 경로 탐색 로직을 추가해야 함 ➡️ 이 로직이 작동하려면 CLI wrapper가 반드시 docker run -v ~ -e~의 명령을 실행해야 한다.
🧐 여기서 외부 플러그인이란? 내장 플러그인은 우리가 개발한 s2n 패키지 안의 플러그인들을 말하고, 외부 플러그인은 사용자가 사용자의 로컬 파일 PC 시스템에 있는 .py 파일들을 말한다. 즉 사용자들이 s2n을 설치한 후 로컬에서 사용할 때 필요하게 된다.
사용자가 s2n scan -u example.com -p custom_scan 명령 실행하면 s2n은 custom_scan이라는 플러그인을 찾을 것이다. 하지만 스캐너 엔진은 도커 컨테이너 내부에 존재할 것이고, 컨테이너는 호스트 PC의 파일 시스템을 기본적으로 볼 수 없다. 그래서 CLI Wrapper가 docker run -v를 사용해 사용자 로컬 디렉토리와 컨테이너 내부의 경로를 연결해 주어야 한다. 이 연결 경로를 통해 Scanner 엔진은 해당 .py 파일을 읽고 동적으로 로드해 실행할 것이다.