폐쇄망에서 pip install 사용하기: pypiserver + Docker로 PyPI 미러 서버 구축

외부 인터넷이 차단된 폐쇄망 환경에서도 `pip install`로 Python 패키지를 설치할 수 있습니다. pypiserver + Docker를 활용한 PyPI 미러 서버 구축 방법을 단계별로 설명합니다.

1. 문제 상황

1.1 환경

많은 기업에서 보안상의 이유로 폐쇄망(air-gapped network)을 운영합니다. 특히 VDI(Virtual Desktop Infrastructure) 환경에서는 외부 인터넷 접근이 완전히 차단되어 있는 경우가 많습니다.

┌─────────────────────────────────────────────────────────────┐
│                     현재 환경 구성                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   [외부망]                         [폐쇄망]                  │
│   ┌─────────────┐                 ┌─────────────────────┐   │
│   │ 개발 PC     │   ──파일전송──>  │ VDI 서버            │   │
│   │ - 인터넷 O  │                 │ - 인터넷 X          │   │
│   │ - Docker O  │                 │ - Docker O          │   │
│   └─────────────┘                 │                     │   │
│                                   │  ┌───────────────┐  │   │
│                                   │  │ 사용자 VDI    │  │   │
│                                   │  │ - Python 3.x  │  │   │
│                                   │  │ - Windows 64  │  │   │
│                                   │  └───────────────┘  │   │
│                                   └─────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 기존 방식의 문제점

기존에는 Docker 이미지에 필요한 패키지를 미리 설치하고, 이미지를 통째로 폐쇄망으로 전송하는 방식을 사용했습니다.

# 기존 방식: Docker 이미지에 패키지 포함
FROM python:3.11
RUN pip install numpy pandas matplotlib  # ← 이미지에 패키지 고정

이 방식의 문제점:

문제 설명
유연성 부족 새 패키지가 필요하면 이미지 전체를 다시 빌드해야 함
시간 소요 이미지 재빌드 + 전송에 수 시간 소요
학습 비용 사용자가 Docker를 이해해야 함
버전 관리 어려움 패키지 버전이 이미지에 종속됨

1.3 목표

사용자들이 익숙한 표준 pip install 명령으로 패키지를 설치할 수 있도록 폐쇄망 내부에 PyPI 미러 서버를 구축하는 것이 목표입니다.

# 목표: 표준 pip 명령 사용
pip install numpy pandas matplotlib

2. 솔루션 비교 및 선택

2.1 후보 솔루션 비교

폐쇄망 PyPI 미러를 위한 여러 도구를 검토했습니다.

도구 특징 장점 단점
pypiserver 경량 PyPI 서버 Docker 지원, 간단한 설정, 선택적 미러링 고급 기능 부족
devpi 다기능 PyPI 서버 캐싱, 인덱스 관리, 테스트 통합 복잡한 설정
Morgan Air-gapped 특화 단일 파일, 표준 라이브러리만 사용 기능 제한적
Posit Package Manager 엔터프라이즈급 부분 미러링, GUI 관리 라이선스 비용

2.2 선택: pypiserver + Docker

pypiserver를 선택한 이유:

  1. 기존 워크플로우 호환: 이미 Docker를 사용 중이므로 일관성 유지
  2. 빠른 구축: 최소한의 설정으로 즉시 사용 가능
  3. 선택적 미러링: 전체 PyPI(20TB+)가 아닌 필요한 패키지만 다운로드
  4. 활발한 커뮤니티: GitHub 3k+ stars, 지속적인 유지보수
# pypiserver 기본 실행 (단 한 줄)
docker run -p 8080:8080 pypiserver/pypiserver run /data/packages

3. 아키텍처 설계

3.1 전체 구조

[외부망 PC]                           [폐쇄망]
┌───────────────────┐                 ┌─────────────────────────────┐
│                   │                 │                             │
│  1. pip download  │                 │   pypiserver (Docker)       │
│     패키지 다운로드 │                 │   ┌─────────────────────┐   │
│                   │   ──파일전송──>  │   │  /packages          │   │
│  2. Docker 이미지  │                 │   │  - numpy-1.24.whl   │   │
│     빌드 & 저장    │                 │   │  - pandas-2.0.whl   │   │
│                   │                 │   │  - ...              │   │
└───────────────────┘                 │   └─────────────────────┘   │
                                      │            │                │
                                      │            ▼                │
                                      │   http://서버:8080/simple/  │
                                      │            │                │
                                      │            ▼                │
                                      │   ┌─────────────────────┐   │
                                      │   │    VDI 사용자들      │   │
                                      │   │  pip install numpy  │   │
                                      │   └─────────────────────┘   │
                                      │                             │
                                      └─────────────────────────────┘

3.2 핵심 컴포넌트

컴포넌트 역할 기술
패키지 저장소 .whl 파일 저장 파일 시스템
미러 서버 HTTP로 패키지 제공 pypiserver (Docker)
다운로드 스크립트 외부망에서 패키지 다운로드 Shell + pip download
클라이언트 설정 pip가 미러 서버를 바라보도록 설정 pip.ini

4. 단계별 구현

4.1 디렉토리 구조

pypi-mirror/
├── Dockerfile                   # pypiserver 이미지 정의
├── docker-compose.yml           # Docker 실행 설정
├── requirements.txt             # 다운로드할 패키지 목록
├── packages/                    # .whl 파일 저장 디렉토리
├── download-packages.sh         # [외부망] 패키지 일괄 다운로드
├── download-single-package.sh   # [외부망] 개별 패키지 다운로드
├── build-and-save.sh            # [외부망] 이미지 빌드/저장
├── load-and-run.sh              # [폐쇄망] 서버 실행
└── client-config/
    └── pip.ini                  # 클라이언트 설정

4.2 requirements.txt 작성

먼저 필요한 패키지 목록을 작성합니다.

# requirements.txt
# 데이터 분석 기본 패키지
numpy
pandas
matplotlib
scikit-learn
jupyter
seaborn
openpyxl
xlrd
requests

# 추가 유용한 패키지
scipy
statsmodels
plotly

4.3 패키지 다운로드 스크립트

pip download 명령을 사용해 특정 플랫폼용 wheel 파일을 다운로드합니다.

#!/bin/bash
# download-packages.sh
# 외부망에서 실행

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACKAGES_DIR="${SCRIPT_DIR}/packages"
REQUIREMENTS_FILE="${SCRIPT_DIR}/requirements.txt"

mkdir -p "$PACKAGES_DIR"

echo "=== PyPI 패키지 다운로드 시작 ==="

# Python 버전 배열 (사용자 환경에 맞게 조정)
PYTHON_VERSIONS=("3.10" "3.11" "3.12")

# 각 Python 버전별로 Windows 64bit wheel 다운로드
for version in "${PYTHON_VERSIONS[@]}"; do
    echo ">>> Python ${version} 용 wheel 다운로드 중..."
    pip download \
        --only-binary=:all: \
        --platform win_amd64 \          # ← Windows 64bit 타겟
        --python-version "$version" \   # ← Python 버전 지정
        -r "$REQUIREMENTS_FILE" \
        -d "$PACKAGES_DIR" \
        || echo "일부 패키지에서 wheel을 찾을 수 없음"
done

# 소스 배포판도 함께 (wheel이 없는 패키지 대비)
echo ">>> 소스 배포판 다운로드 중..."
pip download \
    --no-binary=:all: \
    -r "$REQUIREMENTS_FILE" \
    -d "$PACKAGES_DIR" 2>/dev/null || true

echo "=== 다운로드 완료 ==="
echo "총 패키지 수: $(ls -1 "$PACKAGES_DIR" | wc -l)"

핵심 옵션 설명:

옵션 설명
--only-binary=:all: wheel 파일만 다운로드 (소스 제외)
--platform win_amd64 Windows 64bit용 wheel 지정
--python-version 3.11 특정 Python 버전용 wheel
-d ./packages 저장 디렉토리 지정

4.4 개별 패키지 추가 스크립트

새 패키지가 필요할 때 사용하는 스크립트입니다.

#!/bin/bash
# download-single-package.sh
# 사용법: ./download-single-package.sh tensorflow pytorch

set -e

if [ -z "$1" ]; then
    echo "사용법: $0 <패키지명> [패키지명2] ..."
    exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACKAGES_DIR="${SCRIPT_DIR}/packages"

mkdir -p "$PACKAGES_DIR"

PYTHON_VERSIONS=("3.10" "3.11" "3.12")

for PACKAGE in "$@"; do
    echo "=== 패키지 다운로드: $PACKAGE ==="

    for version in "${PYTHON_VERSIONS[@]}"; do
        pip download \
            --only-binary=:all: \
            --platform win_amd64 \
            --python-version "$version" \
            "$PACKAGE" \
            -d "$PACKAGES_DIR" \
            || echo "wheel을 찾을 수 없음 (Python ${version})"
    done

    # 소스 배포판
    pip download --no-binary=:all: "$PACKAGE" -d "$PACKAGES_DIR" 2>/dev/null || true
done

echo "=== 다운로드 완료 ==="

4.5 Dockerfile 작성

pypiserver 공식 이미지를 기반으로 합니다.

# Dockerfile
FROM pypiserver/pypiserver:latest

# 패키지 디렉토리 생성
RUN mkdir -p /data/packages

# 패키지 복사 (빌드 시 이미지에 포함)
COPY ./packages /data/packages

# 서버 실행
# --disable-fallback: 외부 PyPI 접근 차단 (진정한 오프라인 모드)
CMD ["run", "-p", "8080", "--disable-fallback", "/data/packages"]

--disable-fallback 옵션이 중요한 이유:

기본적으로 pypiserver는 로컬에 패키지가 없으면 외부 PyPI로 요청을 전달합니다. 폐쇄망에서는 이 옵션으로 외부 연결 시도를 완전히 차단해야 합니다.

# fallback 활성화 (기본값) - 폐쇄망에서 오류 발생
pypiserver run /packages
# → 로컬에 없으면 pypi.org로 요청 → 타임아웃/오류

# fallback 비활성화 - 폐쇄망 권장
pypiserver run --disable-fallback /packages
# → 로컬에 없으면 즉시 404 반환 → 명확한 에러

4.6 docker-compose.yml

# docker-compose.yml
services:
  pypi-server:
    image: pypi-mirror:latest
    container_name: pypi-server
    ports:
      - "8080:8080"
    volumes:
      # 호스트 디렉토리 마운트 (패키지 추가 용이)
      - ./packages:/data/packages    # ← 볼륨 마운트로 재빌드 없이 패키지 추가
    restart: unless-stopped

볼륨 마운트의 장점:

# 새 패키지 추가 시
# 1. packages/ 디렉토리에 .whl 파일 복사
# 2. 서버 재시작만으로 적용
docker compose restart

# 이미지 재빌드 불필요!

4.7 빌드 및 저장 스크립트

#!/bin/bash
# build-and-save.sh
# 외부망에서 실행

set -e

cd "$(dirname "${BASH_SOURCE[0]}")"

IMAGE_NAME="pypi-mirror"
IMAGE_TAG="latest"
OUTPUT_FILE="pypi-mirror.tar"

echo "=== Docker 이미지 빌드 ==="
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" .

echo "=== Docker 이미지 저장 ==="
docker save "${IMAGE_NAME}:${IMAGE_TAG}" -o "$OUTPUT_FILE"

echo "=== 완료 ==="
echo "생성된 파일: $OUTPUT_FILE"
echo "파일 크기: $(du -h "$OUTPUT_FILE" | cut -f1)"

4.8 폐쇄망 실행 스크립트

#!/bin/bash
# load-and-run.sh
# 폐쇄망에서 실행

set -e

cd "$(dirname "${BASH_SOURCE[0]}")"

IMAGE_FILE="pypi-mirror.tar"

if [ ! -f "$IMAGE_FILE" ]; then
    echo "오류: ${IMAGE_FILE} 파일이 없습니다."
    exit 1
fi

echo "=== Docker 이미지 로드 ==="
docker load -i "$IMAGE_FILE"

echo "=== 기존 컨테이너 정리 ==="
docker compose down 2>/dev/null || true

echo "=== 서버 시작 ==="
docker compose up -d

echo "=== 완료 ==="
echo "PyPI 미러 서버: http://localhost:8080"

4.9 클라이언트 pip 설정

사용자 PC에서 pip가 미러 서버를 바라보도록 설정합니다.

; pip.ini (Windows: %APPDATA%\pip\pip.ini)
[global]
index-url = http://서버IP:8080/simple/
trusted-host = 서버IP

; 타임아웃 설정
timeout = 60

설정 파일 위치:

OS 경로
Windows %APPDATA%\pip\pip.ini
Linux/Mac ~/.pip/pip.conf
가상환경 <venv>/pip.conf

일회성 사용 (설정 파일 없이):

pip install --index-url http://서버IP:8080/simple/ \
            --trusted-host 서버IP \
            numpy

5. 전체 워크플로우

5.1 초기 구축

# 1. 외부망에서
cd pypi-mirror

# 1-1. requirements.txt 작성
nano requirements.txt

# 1-2. 패키지 다운로드
./download-packages.sh

# 1-3. Docker 이미지 빌드 및 저장
./build-and-save.sh

# 2. 파일 전송 (WinSCP 등)
# - pypi-mirror.tar
# - packages/
# - docker-compose.yml
# - load-and-run.sh

# 3. 폐쇄망에서
./load-and-run.sh

# 4. 클라이언트 설정
# pip.ini를 %APPDATA%\pip\에 복사

5.2 패키지 업데이트

┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ 1. 요청 접수 │ -> │ 2. 패키지   │ -> │ 3. 파일     │ -> │ 4. 서버     │
│   (사용자)   │    │   다운로드   │    │   전송      │    │   재시작    │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘
# 외부망
./download-single-package.sh tensorflow

# 파일 전송 (새로 추가된 .whl 파일만)

# 폐쇄망
docker compose restart

6. 검증 및 테스트

6.1 서버 동작 확인

# 패키지 목록 확인
curl http://localhost:8080/simple/

# 출력 예시:
# <html>
# <head><title>Simple Index</title></head>
# <body>
# <a href="/simple/numpy/">numpy</a><br>
# <a href="/simple/pandas/">pandas</a><br>
# ...
# </body>
# </html>

6.2 패키지 설치 테스트

# 임시 환경에서 테스트
python -m venv test-env
source test-env/bin/activate  # Windows: test-env\Scripts\activate

# 설치
pip install --index-url http://localhost:8080/simple/ \
            --trusted-host localhost \
            numpy pandas

# 확인
pip list

6.3 import 테스트

import numpy as np
import pandas as pd

print(f"numpy: {np.__version__}")
print(f"pandas: {pd.__version__}")

# 간단한 동작 테스트
arr = np.array([1, 2, 3])
df = pd.DataFrame({'a': [1, 2], 'b': [3, 4]})
print(arr, df)

7. 핵심 개념 정리

7.1 pip download 옵션

옵션 설명 예시
--only-binary=:all: wheel만 다운로드 컴파일 불필요
--no-binary=:all: 소스만 다운로드 wheel 없는 패키지용
--platform 타겟 플랫폼 win_amd64, manylinux1_x86_64
--python-version Python 버전 3.10, 3.11
-d 저장 디렉토리 ./packages

7.2 pypiserver 주요 옵션

옵션 설명 폐쇄망 권장
--disable-fallback 외부 PyPI 연결 차단 필수
-p PORT 포트 설정 8080
--passwords FILE htpasswd 인증 선택
--log-stream stdout 로그 출력 선택

7.3 용량 참고

구성 예상 용량
기본 데이터 분석 (numpy, pandas, matplotlib, sklearn, jupyter) 500MB ~ 1GB
딥러닝 추가 (tensorflow, pytorch) 5GB ~ 10GB
전체 PyPI 미러 (bandersnatch) 20TB+

8. 베스트 프랙티스

8.1 체크리스트

  • [ ] --disable-fallback 옵션 설정 확인
  • [ ] 다중 Python 버전 wheel 다운로드
  • [ ] 소스 배포판도 함께 다운로드 (wheel 없는 패키지 대비)
  • [ ] trusted-host 설정 (HTTPS가 아니므로)
  • [ ] 볼륨 마운트로 패키지 추가 용이하게 구성
  • [ ] 주기적 패키지 업데이트 일정 수립

8.2 보안 고려사항

# 인증 추가 (선택)
htpasswd -c .htpasswd admin
docker run -p 8080:8080 \
    -v $(pwd)/packages:/data/packages \
    -v $(pwd)/.htpasswd:/data/.htpasswd \
    pypiserver/pypiserver run --passwords /data/.htpasswd /data/packages

8.3 트러블슈팅

문제: 패키지를 찾을 수 없음

# 확인 1: 서버에 패키지가 있는지
curl http://서버:8080/simple/패키지명/

# 확인 2: Python 버전 호환성
pip debug --verbose  # 클라이언트 플랫폼 태그 확인
ls packages/ | grep 패키지명  # 서버의 wheel 파일 확인

문제: 연결 타임아웃

# 방화벽 확인
telnet 서버IP 8080

# Docker 컨테이너 상태 확인
docker compose ps
docker compose logs

9. FAQ

Q: 전체 PyPI를 미러링하면 안 되나요?

A: 가능하지만 20TB 이상의 스토리지가 필요합니다. 대부분의 경우 필요한 패키지만 선택적으로 다운로드하는 것이 효율적입니다. 전체 미러가 필요하다면 bandersnatch 도구를 사용하세요.

Q: wheel이 없는 패키지는 어떻게 하나요?

A: --no-binary=:all: 옵션으로 소스 배포판(.tar.gz)을 함께 다운로드하세요. 단, 클라이언트에서 컴파일러(gcc, Visual Studio Build Tools 등)가 필요할 수 있습니다.

Q: 의존성 패키지도 자동으로 다운로드되나요?

A: 네, pip download는 지정한 패키지의 모든 의존성을 함께 다운로드합니다.

Q: HTTPS를 적용하려면?

A: pypiserver 앞에 nginx나 traefik 같은 리버스 프록시를 두고 SSL 인증서를 설정하세요.

# nginx 예시
server {
    listen 443 ssl;
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://localhost:8080;
    }
}

Q: 여러 Python 버전을 지원하려면?

A: 다운로드 시 각 버전별로 wheel을 받으면 됩니다. pypiserver가 클라이언트 Python 버전에 맞는 wheel을 자동으로 선택합니다.

for version in 3.10 3.11 3.12; do
    pip download --python-version $version ...
done

10. 참고 자료