본문 바로가기
대외활동/DateEngineering Zoom Camp

[DE-Zoomcamp] 1-1. Docker + PostgreSQL로 로컬 데이터 엔지니어링 실습 환경 만들기

by 드인 2026. 1. 25.

 이 글은 Data Engineering Zoomcamp의 Module 1: Containerization and Infrastructure as Code를 기반으로, Docker로 PostgreSQL을 띄우고 NYC TLC 택시 데이터를 청크 단위로 적재하는 로컬 데이터 엔지니어링 실습 환경을 소개합니다. 

① 재현 가능한 실행 환경(Docker)
② DB 컨테이너 운영(네트워크/볼륨)
③ 대용량 적재(Chunk + SQLAlchemy)
순서로 진행됩니다.


목차

  1. 왜 Docker인가
  2. Docker 기본: 컨테이너는 기본적으로 무상태(stateless)
  3. Volume로 데이터/파일을 컨테이너 밖에 고정하기
  4. uv로 Python 의존성 관리하기(재현성 강화)
  5. 파이프라인 Dockerizing (pip 버전 / uv 버전)
  6. PostgreSQL을 Docker로 실행하기 (환경변수/포트/볼륨)
  7. pgcli로 접속해서 빠르게 점검하기
  8. NYC TLC 데이터 적재: dtype/parse_dates + chunksize
  9. Notebook → ingest 스크립트로 전환 + click으로 CLI화
  10. pgAdmin 컨테이너 + Docker 네트워크
  11. Docker Compose로 한 번에 띄우기
  12. 정리/클린업(디스크 회수)
  13. 참고자료/실습

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

https://github.com/DataTalksClub/data-engineering-zoomcamp/blob/main/01-docker-terraform/docker-sql/02-virtual-environment.md

 

데이터는 NYC TLC(공식) 및 학습용 릴리즈를 사용합니다.

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. 참고 자료 / 출처