본문으로 건너뛰기

3.1 Docker Compose로 Neon 구동

컨테이너 8개로 Neon 한 세트를 구동합니다. PostgreSQL 하나를 구동하는 것보다 부담이 큽니다. 하지만 각 컨테이너가 Part II에서 본 구성 요소와 1:1로 대응합니다. 따라서 아키텍처를 직접 확인하기에는 이 방식이 가장 빠릅니다.

준비물

항목이 노트의 환경비고
Docker28.x, Compose v5docker composedocker-compose 어느 쪽이든 동작
jq, curl, python3macOS 기본 또는 Homebrew도우미 스크립트가 pageserver API 응답을 처리하는 데 사용. jqbrew install jq
이미지 ghcr.io/neondatabase/neonlatest, 6.98 GBpageserver, safekeeper, storage_broker 바이너리
이미지 ghcr.io/neondatabase/compute-node-v17latest, 1.75 GB패치된 PostgreSQL 17 + compute_ctl
Docker VM 자원2 vCPU, 12 GB기동 후 전체 메모리 사용량 약 450 MB
디스크여유 15 GB 권장이미지 9 GB + MinIO 데이터

호스트에 psql이 없어도 됩니다. 실습 스크립트가 compute 컨테이너 안의 psql을 대신 실행합니다.

latest 태그는 계속 바뀝니다. 이 노트의 출력(PostgreSQL 17.5, neon 확장 1.6, API 필드, tenant 기본값)은 2026-09-07에 받은 이미지 기준입니다. 같은 결과를 재현하려면 아래 digest를 TAG 대신 지정합니다. 다른 버전에서는 필드 이름이나 기본값이 다를 수 있습니다.

ghcr.io/neondatabase/neon@sha256:7a4f124917bb929964b2d696d710f19584f80bb9bd51b2af4a6e2425434c761f
ghcr.io/neondatabase/compute-node-v17@sha256:13ab146d3e7bbabb25a8532f315ac443e7512351d1ede0bab586def5c70e26c3

storage 이미지가 7 GB에 가까운 이유는 WAL redo용 PostgreSQL 바이너리를 지원 버전마다 포함하기 때문입니다. 처음 pull할 때는 네트워크에 따라 수 분에서 수십 분이 걸립니다.

파일 구성

/examples/neon-lab/ 전체를 내려받습니다. upstream neondatabase/neon 저장소의 docker-compose/ 디렉터리를 바탕으로 PostgreSQL 기본 버전을 17로 바꾼 구성입니다. 확장 테스트용 프로파일을 제거하고 branch용 compute와 도우미 스크립트를 추가했습니다.

neon-lab/
README.md # 빠른 시작과 구성 요약
compose.yml # 서비스 8개 정의
compute_wrapper/
Dockerfile # compute 이미지에 curl, jq, nc 추가
shell/compute.sh # tenant/timeline 확보 후 compute_ctl 실행
var/db/postgres/configs/config.json # compute spec (GUC, role, JWKS)
pageserver_config/
pageserver.toml # broker, S3(MinIO), 리스너 주소
identity.toml # pageserver id
scripts/
branch.sh # pageserver API로 branch(timeline) 생성
status.sh # timeline 트리와 크기 출력
psql.sh # compute 컨테이너 안의 psql 실행

서비스와 포트

서비스역할호스트 포트
minioS3 호환 object storage9000 (API), 9001 (콘솔)
minio_create_bucketsneon 버킷 생성 후 종료없음
storage_brokersafekeeper와 pageserver 사이의 상태 전파50051
pageserverpage 재구성, layer 저장, 관리 API9898 (HTTP), 6400 (libpq)
safekeeper1~3WAL quorum 저장7676~7678 (HTTP)
compute1main timeline의 PostgreSQL55433 (postgres), 3080 (compute_ctl)
compute2branch timeline의 PostgreSQL, 프로파일 branch55434, 3081

pageserver.toml에는 control_plane_api가 존재하지 않는 주소로 설정되어 있습니다. control_plane_emergency_mode=true도 활성화되어 있습니다. storage controller 없이 pageserver만으로 tenant를 attach하는 설정입니다.

기동

cd neon-lab
docker compose build compute1 # compute 이미지에 curl, jq, nc 설치 (1~2분)
docker compose up -d
docker compose ps

이 노트의 환경에서는 up -d가 24초 만에 끝났습니다. 약 10초 뒤 compute1이 연결을 받기 시작했습니다. compute 기동 로그에서 다음 순서를 확인할 수 있습니다.

docker compose logs -f compute1
  1. compute.sh가 pageserver 9898 포트에 tenant가 있는지 조회합니다. tenant가 없으면 PUT /v1/tenant/{id}/location_config로 tenant를 만들고 POST /v1/tenant/{id}/timeline/으로 첫 timeline을 만듭니다.
  2. config.jsonTENANT_ID, TIMELINE_ID 자리를 실제 값으로 바꿉니다.
  3. compute_ctl이 safekeeper 3대와 handshake를 합니다([WP] got VoteResponse from sk ...). 이어서 pageserver에서 basebackup tarball을 받아 데이터 디렉터리를 만든 뒤 PostgreSQL을 시작합니다.
  4. neon_superuser 역할 생성과 권한 부여 같은 초기 migration 12개를 실행합니다.

첫 timeline은 pageserver가 initdb를 실행해 만듭니다. 따라서 compute 쪽에는 initdb가 없습니다.

접속

./scripts/psql.sh compute1 -c "select version();"
./scripts/psql.sh compute1 -c "show neon.tenant_id;" -c "show neon.timeline_id;" -c "show neon.safekeepers;"
PostgreSQL 17.5 on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit

neon.tenant_id
----------------------------------
d9f2df3a9c6331377916d2538527d640

neon.timeline_id
----------------------------------
7eac85e3d7199edb170e1e283707fcdd

neon.safekeepers
----------------------------------------------------
safekeeper1:5454,safekeeper2:5454,safekeeper3:5454

호스트에 psql이 있다면 postgresql://cloud_admin:cloud_admin@localhost:55433/postgres로 직접 접속해도 됩니다. 비밀번호는 config.json에 md5로 저장된 실습용 값이므로 외부 인터페이스에 노출하지 않습니다.

pg_extension을 조회하면 neon 확장(1.6)이 미리 설치되어 있습니다. 이 확장이 compute 안의 smgr 인터페이스를 pageserver 통신 방식으로 바꿉니다.

상태 확인

./scripts/status.sh
tenant: d9f2df3a9c6331377916d2538527d640
timeline_id name ancestor ancestor_lsn last_record_lsn logical_MB physical_MB
7eac85e3d7199edb170e1e283707fcdd main - - 0/15CFE68 22 22.1

status.sh는 pageserver의 GET /v1/tenant/{tenant}/timeline을 호출합니다. timeline마다 ancestor, 분기 LSN, 마지막 WAL 위치, logical size와 physical size를 표로 만듭니다. 이름 열은 scripts/branch.sh가 남긴 .branches 파일에서 가져옵니다. Neon 저장 계층에는 사람이 읽는 branch 이름이 없고 timeline id만 있습니다. 이름은 control plane의 개념입니다.

실습 데이터 적재

이후 장에서 계속 사용하는 테이블을 만듭니다.

CREATE TABLE orders(
id bigserial PRIMARY KEY,
customer_id int NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
note text
);
INSERT INTO orders(customer_id, amount, note)
SELECT (random()*10000)::int, round((random()*500)::numeric, 2), md5(random()::text)
FROM generate_series(1, 500000);
CREATE INDEX ON orders(customer_id);
SELECT count(*), pg_size_pretty(pg_total_relation_size('orders')) FROM orders;
SELECT pg_current_wal_flush_lsn();
INSERT 0 500000 Time: 4070.346 ms
CREATE INDEX Time: 2311.091 ms
count | pg_size_pretty
--------+----------------
500000 | 62 MB
pg_current_wal_flush_lsn: 0/A84E000

적재 직후 status.sh를 다시 실행하면 logical size는 84 MB로 늘어납니다. 반면 physical size는 22.1 MB 그대로입니다. 적재 과정에서 생성된 WAL이 아직 pageserver의 in-memory layer에 있기 때문입니다. pageserver가 이를 아직 디스크 layer 파일로 flush하지 않았습니다. tenant 설정을 조회하면 checkpoint_distance가 256 MB(268435456), checkpoint_timeout이 10분입니다. WAL이 그만큼 쌓이거나 마지막 flush 후 10분이 지나면 in-memory layer가 L0 delta layer로 flush됩니다. 실습을 빠르게 이어가면 시간 조건보다 크기 조건을 먼저 충족합니다. 3.4에서 이 값이 바뀌는 시점을 확인합니다.

정리

docker compose --profile branch down -v # 컨테이너와 MinIO 볼륨까지 제거

-v 없이 종료하면 MinIO 볼륨이 남아 다음 기동 때 이전 tenant를 다시 attach하려고 합니다. 실습을 처음부터 다시 하려면 -v를 붙이고 .env, .branches 파일도 삭제합니다.

연습 문제

  1. docker compose logs compute1에서 [WP]로 시작하는 줄을 찾아 safekeeper 3대 중 몇 대의 응답으로 election이 끝났는지 확인합니다.
  2. safekeeper 하나를 docker compose stop safekeeper3으로 중지한 뒤 INSERT가 계속 성공하는지, 둘을 중지하면 어떻게 되는지 관찰합니다.
  3. curl -s localhost:9898/v1/tenant/<tenant>/config | jq .effective_config에서 gc_horizon, pitr_interval, compaction_threshold, checkpoint_timeout 값을 읽고 2.3의 설명과 대응시킵니다.

참고