이 글은 Data Engineering Zoomcamp의 Module 1: Containerization and Infrastructure as Code를 기반으로, Docker로 PostgreSQL을 띄우고 NYC TLC 택시 데이터를 청크 단위로 적재하는 로컬 데이터 엔지니어링 실습 환경을 소개합니다.
① 재현 가능한 실행 환경(Docker)
② DB 컨테이너 운영(네트워크/볼륨)
③ 대용량 적재(Chunk + SQLAlchemy)
순서로 진행됩니다.
목차
- 왜 Docker인가
- Docker 기본: 컨테이너는 기본적으로 무상태(stateless)
- Volume로 데이터/파일을 컨테이너 밖에 고정하기
- uv로 Python 의존성 관리하기(재현성 강화)
- 파이프라인 Dockerizing (pip 버전 / uv 버전)
- PostgreSQL을 Docker로 실행하기 (환경변수/포트/볼륨)
- pgcli로 접속해서 빠르게 점검하기
- NYC TLC 데이터 적재: dtype/parse_dates + chunksize
- Notebook → ingest 스크립트로 전환 + click으로 CLI화
- pgAdmin 컨테이너 + Docker 네트워크
- Docker Compose로 한 번에 띄우기
- 정리/클린업(디스크 회수)
- 참고자료/실습
1. 왜 Docker인가
데이터 파이프라인을 개발하다 보면 "내 로컬에서는 되는데, 서버에서는 안 된다"가 흔합니다. Python 버전, 라이브러리 버전, OS 라이브러리, DB 드라이버에 약간의 차이가 있어도 문제가 발생합니다.
이 문제와 관련해서 Docker는 다음과 같은 장점을 가지고 있습니다.
- Reproducibility(재현성): 동일 이미지 = 동일 실행 환경
- Isolation(격리): 프로젝트 간 의존성 충돌 최소화
- Portability(이식성): Docker만 있으면 어디서든 실행 가능
따라서 Docker는 "단순 실행 도구"가 아니라 데이터 엔지니어링 워크플로우의 표준 실행 환경으로 사용할 수 있습니다.
2. Docker 기본: 컨테이너는 기본적으로 무상태(stateless)
Docker 설치 확인 & hello-world
docker --version
docker run hello-world
ubuntu 컨테이너로 들어가기
docker run -it ubuntu
중요
컨테이너 내부에서 설치/수정한 내용은 컨테이너가 종료되면 기본적으로 사라집니다(무상태).
그래서 데이터는 별도로 보존(Volume/Bind mount)해야 합니다.
컨테이너 목록 확인 및 정리
docker ps -a
docker rm `docker ps -aq` # 실습용 일괄 삭제(주의)
실습에서는 실행 후 자동 삭제 옵션을 추가하여 진행됩니다.
docker run -it --rm ubuntu
3. Volume/Bind mount로 데이터(또는 코드)를 컨테이너 밖에 고정하기
호스트 폴더를 컨테이너에 마운트하면, 컨테이너를 삭제해도 파일은 유지됩니다.
예를 들어 로컬에 test 폴더를 만들고 파일을 생성해볼 수 있습니다.
mkdir test
cd test
touch file1.txt file2.txt file3.txt
echo "Hello from host" > file1.txt
cd ..
컨테이너 실행 시 로컬 폴더를 /app/test로 마운트합니다.
docker run -it --rm \
-v $(pwd)/test:/app/test \
--entrypoint=bash \
python:3.13.11-slim
컨테이너 안에서 확인할 수 있습니다.
cd /app/test
ls -la
cat file1.txt
4. uv로 Python 의존성 관리하기(재현성 강화)
pip로 전역 설치하면 프로젝트별 버전 충돌이 생기기 쉽습니다. 실습에서는 uv로 프로젝트 환경을 관리합니다.
pip install uv
uv init --python=3.13
uv add pandas pyarrow
uv run python -V
팁
Docker 빌드에서도 lockfile(예: uv.lock) 기반으로 설치하면 빌드마다 버전이 바뀌는 문제를 줄일 수 있습니다.
5. 파이프라인 Dockerizing
5-1. 간단 버전: pip로 바로 설치
FROM python:3.13.11-slim
RUN pip install pandas pyarrow
WORKDIR /app
COPY pipeline.py pipeline.py
ENTRYPOINT ["python", "pipeline.py"]
빌드 및 실행
docker build -t test:pandas .
docker run -it test:pandas 10
5-2. 권장 버전: uv + lockfile 기반(재현성/캐시 효율)
FROM python:3.13.10-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/
WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH"
COPY "pyproject.toml" "uv.lock" ".python-version" ./
RUN uv sync --locked
COPY pipeline.py pipeline.py
ENTRYPOINT ["python", "pipeline.py"]
왜 dependency 파일을 먼저 COPY하나요?
코드가 바뀌어도 의존성 레이어는 캐시를 재사용할 수 있어, 빌드가 빨라집니다.
6. PostgreSQL을 Docker로 실행하기 (환경변수/포트/볼륨)
Postgres 공식 이미지로 DB를 컨테이너로 띄웁니다.
Named Volume 방식
docker run -it --rm \
-e POSTGRES_USER="root" \
-e POSTGRES_PASSWORD="root" \
-e POSTGRES_DB="ny_taxi" \
-v ny_taxi_postgres_data:/var/lib/postgresql \
-p 5432:5432 \
postgres:18
- -e: 환경변수(user/pass/db)
- -v: 볼륨(컨테이너 삭제해도 데이터 유지)
- -p: 호스트 포트(5432) ↔ 컨테이너 포트(5432) 매핑
Bind mount 방식(호스트 파일시스템 직접 매핑)
mkdir ny_taxi_postgres_data
docker run -it \
-e POSTGRES_USER="root" \
-e POSTGRES_PASSWORD="root" \
-e POSTGRES_DB="ny_taxi" \
-v $(pwd)/ny_taxi_postgres_data:/var/lib/postgresql \
-p 5432:5432 \
postgres:18
주의(권한 이슈)
Linux에서는 디렉터리 소유권/권한 때문에 bind mount가 꼬일 수 있습니다. Named volume를 추천합니다.
7. pgcli로 접속해서 점검하기
uv add --dev pgcli
uv run pgcli -h localhost -p 5432 -u root -d ny_taxi
점검
\dt
CREATE TABLE test (id INTEGER, name VARCHAR(50));
INSERT INTO test VALUES (1, 'Hello Docker');
SELECT * FROM test;
\q
8. NYC TLC 데이터 적재: dtype/parse_dates + chunksize

데이터는 NYC TLC(공식) 및 학습용 릴리즈를 사용합니다.
- TLC 공식 데이터: https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page
- 학습용 CSV 데이터: https://github.com/DataTalksClub/nyc-tlc-data/releases
Pandas로 샘플을 먼저 읽고 dtype과 parse_dates를 지정합니다.
import pandas as pd
prefix = 'https://github.com/DataTalksClub/nyc-tlc-data/releases/download/yellow/'
url = f'{prefix}/yellow_tripdata_2021-01.csv.gz'
dtype = {
"VendorID": "Int64",
"passenger_count": "Int64",
"trip_distance": "float64",
"RatecodeID": "Int64",
"store_and_fwd_flag": "string",
"PULocationID": "Int64",
"DOLocationID": "Int64",
"payment_type": "Int64",
"fare_amount": "float64",
"extra": "float64",
"mta_tax": "float64",
"tip_amount": "float64",
"tolls_amount": "float64",
"improvement_surcharge": "float64",
"total_amount": "float64",
"congestion_surcharge": "float64"
}
parse_dates = ["tpep_pickup_datetime", "tpep_dropoff_datetime"]
df = pd.read_csv(url, nrows=100, dtype=dtype, parse_dates=parse_dates)
df.head()
핵심 포인트
대용량 파일을 한 번에 적재하면 메모리 문제가 쉽게 발생합니다.
iterator=True + chunksize=100000로 청크 처리하여 해결할 수 있습니다.
9. Notebook → ingest 스크립트로 전환 + click으로 CLI화
필요 패키지
uv add sqlalchemy psycopg2-binary tqdm click
엔진 생성
from sqlalchemy import create_engine
engine = create_engine('postgresql://root:root@localhost:5432/ny_taxi')
청크 적재
import pandas as pd
from tqdm.auto import tqdm
df_iter = pd.read_csv(
url,
dtype=dtype,
parse_dates=parse_dates,
iterator=True,
chunksize=100000
)
first_chunk = next(df_iter)
first_chunk.head(0).to_sql(name="yellow_taxi_trips", con=engine, if_exists="replace")
first_chunk.to_sql(name="yellow_taxi_trips", con=engine, if_exists="append")
for df_chunk in tqdm(df_iter):
df_chunk.to_sql(name="yellow_taxi_trips", con=engine, if_exists="append")
CLI 실행 예시
uv run python ingest_data.py \
--pg-user=root \
--pg-pass=root \
--pg-host=localhost \
--pg-port=5432 \
--pg-db=ny_taxi \
--target-table=yellow_taxi_trips \
--year=2021 \
--month=1 \
--chunksize=100000
검증 쿼리
SELECT COUNT(*) FROM yellow_taxi_trips;
SELECT * FROM yellow_taxi_trips LIMIT 10;
10. pgAdmin 컨테이너 + Docker 네트워크
pgcli도 좋지만, 스키마/인덱스/GUI 쿼리 편집 등은 pgAdmin이 사용하기 수월합니다.
문제는 pgAdmin 컨테이너가 Postgres 컨테이너를 "찾을 수 있어야" 한다는 점입니다.
이를 위해, 두 컨테이너를 동일한 Docker 네트워크에 올려야합니다.
네트워크 생성
docker network create pg-network
Postgres(네트워크 + 이름 지정)
docker run -it \
-e POSTGRES_USER="root" \
-e POSTGRES_PASSWORD="root" \
-e POSTGRES_DB="ny_taxi" \
-v ny_taxi_postgres_data:/var/lib/postgresql \
-p 5432:5432 \
--network=pg-network \
--name pgdatabase \
postgres:18
pgAdmin(같은 네트워크 + 이름 지정)
docker run -it \
-e PGADMIN_DEFAULT_EMAIL="admin@admin.com" \
-e PGADMIN_DEFAULT_PASSWORD="root" \
-v pgadmin_data:/var/lib/pgadmin \
-p 8085:80 \
--network=pg-network \
--name pgadmin \
dpage/pgadmin4
접속
- 브라우저: http://localhost:8085
- 로그인: admin@admin.com / root
- 서버 등록 시 Host는 localhost가 아니라 pgdatabase
왜 Host가 pgdatabase인가요?
같은 네트워크 내부에서 컨테이너 이름이 DNS처럼 동작하기 때문입니다.
11. Docker Compose로 한 번에 띄우기
docker-compose.yaml
services:
pgdatabase:
image: postgres:18
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=root
- POSTGRES_DB=ny_taxi
volumes:
- "ny_taxi_postgres_data:/var/lib/postgresql:rw"
ports:
- "5432:5432"
pgadmin:
image: dpage/pgadmin4
environment:
- PGADMIN_DEFAULT_EMAIL=admin@admin.com
- PGADMIN_DEFAULT_PASSWORD=root
volumes:
- "pgadmin_data:/var/lib/pgadmin"
ports:
- "8085:80"
volumes:
ny_taxi_postgres_data:
pgadmin_data:
실행/종료
docker-compose up
docker-compose down
docker-compose up -d
docker-compose logs
docker-compose down -v
Compose의 장점
1. 한 번에 여러 서비스 실행
2. 네트워크 자동 생성(서비스 이름으로 접근 가능)
3. 설정을 코드(YAML)로 관리
12. 정리(디스크 회수)
실습 후 Docker 리소스를 정리해 디스크를 회수합니다.
docker-compose down
docker container prune
docker image prune -a
docker volume prune
docker network prune
# 전체 정리(주의: 모든 리소스 삭제)
docker system prune -a --volumes
13. 참고 자료 / 출처
- Workshop Video: https://www.youtube.com/watch?v=lP8xXebHmuE
- Slides: https://docs.google.com/presentation/d/19pXcInDwBnlvKWCukP5sDoCAb69SPqgIoxJ_0Bikr00/edit
- Data Engineering Zoomcamp Repo: https://github.com/DataTalksClub/data-engineering-zoomcamp
- NYC TLC Trip Record Data(공식): https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page
- DataTalksClub NYC TLC Data(학습용): https://github.com/DataTalksClub/nyc-tlc-data
- Postgres Docker Image Docs: https://hub.docker.com/_/postgres
- uv Docs: https://docs.astral.sh/uv/
- pgAdmin Container Docs: https://www.pgadmin.org/docs/pgadmin4/latest/container_deployment.html
- pgcli Docs: https://www.pgcli.com/docs