폐쇄망에서 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를 선택한 이유:
- 기존 워크플로우 호환: 이미 Docker를 사용 중이므로 일관성 유지
- 빠른 구축: 최소한의 설정으로 즉시 사용 가능
- 선택적 미러링: 전체 PyPI(20TB+)가 아닌 필요한 패키지만 다운로드
- 활발한 커뮤니티: 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