워로디스

Docker Compose PostgreSQL 구성 본문

정리/가상화

Docker Compose PostgreSQL 구성

워로디스 2026. 4. 5. 15:11

1. 이 문서의 최종 목표

이 문서는 아래 4가지 접속 경로를 동시에 만족시키는 구성을 설명합니다.

  1. DB Compose 자체
  2. 같은 호스트의 다른 Compose 프로젝트(앱 컨테이너)
  3. 같은 호스트의 CLI / DataGrip / 호스트 JVM(Spring Boot)
  4. 외부 별도 PC(Windows 포함)의 DataGrip / psql

Docker Compose는 기본적으로 프로젝트마다 별도 네트워크를 만들고, 같은 네트워크 안의 컨테이너는 서비스 이름으로 서로를 찾을 수 있습니다. 여러 Compose 프로젝트가 서로 통신하려면 공용 external network를 만들어 공유하는 방식이 가장 깔끔합니다. 반면 호스트 접속은 Docker 내부 네트워크가 아니라 ports: publish로 결정됩니다. 127.0.0.1에 bind하면 Docker 호스트에서만 직접 접근 가능합니다. (Docker Documentation)

2. 최종 아키텍처

최종 구조는 이렇게 생각하면 됩니다.

  • DB 컨테이너
    • external network shared-app-net에 연결
    • alias: postgres-db
    • 호스트 공개 포트: 127.0.0.1:5432:5432
  • 다른 Compose의 앱 컨테이너
    • 같은 shared-app-net에 연결
    • DB 접속 주소: postgres-db:5432
  • 호스트에서 직접 실행하는 앱/툴
    • DB 접속 주소: 127.0.0.1:5432
  • 외부 PC
    • SSH 터널을 열고 127.0.0.1:15432로 접속

이 구성을 쓰면 Compose 간 통신호스트 접속이 동시에 가능합니다. published port는 호스트 경로를 만들고, Docker 네트워크는 컨테이너 간 경로를 만듭니다. 둘은 경쟁 관계가 아니라 병행되는 두 개의 접속 경로입니다. (Docker Documentation)

3. 디렉터리 구조

DB 프로젝트 폴더는 이렇게 둡니다.

~/apps/postgresql/
├─ compose.yaml
├─ init/
│  └─ 10-create-app-db.sh
├─ secrets/
│  ├─ postgres.password
│  ├─ app.password
│  ├─ app-bootstrap.env
│  └─ pgpass
├─ backups/
└─ .gitignore

이 구조의 핵심은 사람이 직접 관리하는 파일만 현재 폴더 아래에 두고, 실제 DB 데이터는 named volume pgdata에 둔다는 점입니다. Docker는 볼륨을 컨테이너 데이터 영속화의 권장 방식으로 설명합니다. bind mount보다 호스트 디렉터리 구조와 권한에 덜 민감하기 때문입니다. (Docker Documentation)

.gitignore는 이렇게 둡니다.

secrets/
backups/

4. 호스트 psql 설치와 버전 맞춤

Ubuntu는 배포판에 PostgreSQL을 기본 포함하지만, 배포판이 특정 PostgreSQL 버전을 snapshot 해서 유지합니다. Ubuntu 24.04(Noble)는 기본 패키지가 16 계열일 수 있고, 서버가 18이면 psql 16 ↔ server 18 조합이 생길 수 있습니다. PostgreSQL 공식 문서는 psql같은 버전 또는 더 오래된 서버에 가장 잘 맞고, 서버가 psql보다 더 새 버전이면 backslash 명령들이 깨질 수 있다고 설명합니다. 또 PostgreSQL 프로젝트는 Ubuntu용 PGDG APT 저장소에서 postgresql-client-18 같은 버전 고정 클라이언트 패키지를 제공합니다. (PostgreSQL)

그래서 호스트 CLI는 postgresql-client-18로 맞추는 것을 기본 권장합니다.

4-1. 가장 권장하는 방식: PGDG 저장소로 client 18 설치

공식 Ubuntu 다운로드 페이지는 PGDG 저장소 구성을 자동 스크립트 또는 수동 방식으로 안내합니다. (PostgreSQL)

sudo apt install -y postgresql-common ca-certificates
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt update
sudo apt install -y postgresql-client-18
psql --version

이렇게 하면 호스트 psql, pg_dump, pg_restore를 서버 major와 맞출 수 있습니다.
특히 pg_dump/pg_dumpall 같은 백업 도구는 서버보다 오래된 major 버전으로 쓰는 것을 피하는 게 좋습니다. PostgreSQL 업그레이드 문서는 dump 도구는 더 새로운 버전을 쓰는 것을 권장합니다. (PostgreSQL)

4-2. 기본 Ubuntu 패키지만 쓰는 경우

sudo apt update
sudo apt install -y postgresql-client
psql --version

이 경우 Ubuntu 기본 snapshot 버전이 설치될 수 있습니다.
간단한 SQL은 대체로 되지만, 공식 문서 기준으로 psql서버가 더 새 major 버전일 때 일부 \d 류 메타 명령이 제대로 동작하지 않을 수 있습니다. 그래서 이 문서의 권장 기본은 여전히 postgresql-client-18 입니다. (PostgreSQL)

5. 공용 external network 생성

여러 Compose 프로젝트를 연결할 네트워크를 먼저 만듭니다.

sudo docker network create shared-app-net

이미 있으면 무시해도 됩니다. Docker는 user-defined bridge 네트워크를 생성해서 여러 컨테이너가 이름 기반으로 통신할 수 있게 합니다. Compose의 external: true 네트워크는 이런 기존 네트워크를 재사용하는 방식입니다. (Docker Documentation)

6. 비밀번호와 bootstrap 파일 생성

먼저 디렉터리를 만들고, 비밀번호 파일을 생성합니다.
이번 가이드는 실패 가능성을 줄이기 위해 hex 비밀번호를 씁니다. 이렇게 하면 URI/쉘/env 파일 파싱 문제가 줄어듭니다.

cd ~/apps/postgresql
mkdir -p init secrets backups

openssl rand -hex 24 > secrets/postgres.password
openssl rand -hex 24 > secrets/app.password
chmod 600 secrets/postgres.password secrets/app.password

그다음 app.password를 source of truth로 두고, init 전용 env 파일과 host CLI용 pgpass를 만듭니다.

APP_PASS="$(tr -d '\n' < secrets/app.password)"

cat > secrets/app-bootstrap.env <<EOF
APP_DB_USER=app
APP_DB_NAME=app
APP_DB_PASSWORD=${APP_PASS}
EOF
chmod 600 secrets/app-bootstrap.env

cat > secrets/pgpass <<EOF
127.0.0.1:*:app:app:${APP_PASS}
EOF
chmod 600 secrets/pgpass

PostgreSQL은 .pgpass/PGPASSFILE을 공식 지원하고, Unix에서는 권한이 0600보다 느슨하면 무시합니다. Windows에서는 기본 password file 위치가 %APPDATA%\postgresql\pgpass.conf 입니다. (PostgreSQL)

7. 최종 DB용 compose.yaml

아래 내용을 그대로 저장합니다.

services:
  db:
    image: postgres:18
    restart: unless-stopped

    env_file:
      - ./secrets/app-bootstrap.env

    environment:
      POSTGRES_USER: postgres
      POSTGRES_DB: postgres
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password

    secrets:
      - postgres_password

    volumes:
      - pgdata:/var/lib/postgresql
      - ./init:/docker-entrypoint-initdb.d:ro
      - ./backups:/backups

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

    ports:
      - "127.0.0.1:5432:5432"

    networks:
      shared:
        aliases:
          - postgres-db

secrets:
  postgres_password:
    file: ./secrets/postgres.password

volumes:
  pgdata:

networks:
  shared:
    external: true
    name: shared-app-net

이 설계의 핵심은 다음과 같습니다.

  • POSTGRES_PASSWORD_FILE은 Postgres 공식 이미지가 지원하는 _FILE 변수입니다. (Docker Hub)
  • PostgreSQL 18 계열 공식 이미지는 데이터 마운트 대상을 /var/lib/postgresql 로 잡아야 합니다. (Docker Hub)
  • 127.0.0.1:5432:5432호스트 접속 경로
  • shared-app-net + postgres-db다른 Compose의 앱 컨테이너 경로

즉, 이 한 파일로 호스트 접속 + Compose 간 통신을 동시에 만족시킵니다. (Docker Documentation)

8. 최종 init/10-create-app-db.sh

아래 파일을 저장합니다.

#!/usr/bin/env bash
set -Eeuo pipefail

: "${APP_DB_USER:?APP_DB_USER is required}"
: "${APP_DB_NAME:?APP_DB_NAME is required}"
: "${APP_DB_PASSWORD:?APP_DB_PASSWORD is required}"

psql -v ON_ERROR_STOP=1 \
  --username "$POSTGRES_USER" \
  --dbname "$POSTGRES_DB" \
  --set=app_db_user="$APP_DB_USER" \
  --set=app_db_name="$APP_DB_NAME" \
  --set=app_db_password="$APP_DB_PASSWORD" <<'SQL'
CREATE ROLE :"app_db_user" LOGIN PASSWORD :'app_db_password';
CREATE DATABASE :"app_db_name" OWNER :"app_db_user";
SQL

psql -v ON_ERROR_STOP=1 \
  --username "$POSTGRES_USER" \
  --dbname "$APP_DB_NAME" \
  --set=app_db_user="$APP_DB_USER" \
  --set=app_db_name="$APP_DB_NAME" <<'SQL'
CREATE SCHEMA IF NOT EXISTS app AUTHORIZATION :"app_db_user";
ALTER DATABASE :"app_db_name" SET search_path TO app, public;
SQL

그리고 실행 권한을 줍니다.

chmod +x init/10-create-app-db.sh

Postgres 공식 이미지는 /docker-entrypoint-initdb.d 아래의 *.sql, *.sql.gz, *.sh를 지원하고, initdb 직후 이 파일들을 실행합니다. Docker의 PostgreSQL 초기화 가이드도 bootstrap용 사용자/스키마/DB 생성에 이 경로를 쓰도록 설명합니다. (Docker Hub)

9. init가 언제 실행되는가

이건 반드시 기억해야 합니다.

init/ 파일은 첫 docker compose up 전에 준비되어 있어야 합니다.

공식 동작은 이렇습니다.

  • 데이터 디렉터리가 비어 있으면 initdb 실행
  • 그 다음 /docker-entrypoint-initdb.d의 파일 실행
  • 이미 데이터가 있으면 초기화 스킵
  • init 스크립트가 실패한 뒤 재시작해도, 이미 데이터 디렉터리가 생긴 상태면 자동 재실행되지 않음

즉, 나중에 파일을 추가해도 자동 실행되지 않습니다.
init을 다시 태우려면 pgdata 볼륨을 지워야 합니다. 이것이 지금까지 가장 많이 헷갈렸던 포인트입니다. (Docker Hub)

10. 정확한 실행 순서

이 순서를 그대로 따르면 됩니다.

10-1. Compose 구문 검증

docker compose config >/dev/null

Compose는 docker compose config로 최종 해석된 구성을 검증할 수 있습니다. (Docker Documentation)

10-2. 과거 실패 흔적 제거

이전 시도에서 pgdata가 이미 만들어졌을 수 있으므로, 처음 한 번은 꼭 볼륨까지 지우고 시작하는 것이 안전합니다.

sudo docker compose down -v

Compose의 down -v는 프로젝트의 named volume까지 제거합니다. external network는 제거하지 않습니다. (Docker Documentation)

10-3. 시작

sudo docker compose up -d

10-4. 준비 완료까지 대기

until sudo docker compose exec db pg_isready -U postgres -d postgres >/dev/null 2>&1; do
  sleep 2
done

Compose는 서비스가 running인 것과 실제로 SQL을 받을 준비가 된 것을 동일시하지 않으므로, pg_isready 기반 대기를 넣는 것이 맞습니다. (Docker Documentation)

10-5. 초기화 성공 검증

sudo docker compose exec db psql -U postgres -d postgres -c '\du'
sudo docker compose exec db psql -U postgres -d postgres -c '\l'
sudo docker compose exec db psql -U app -d app -c 'SHOW search_path;'

정상이라면:

  • \duapp
  • \lapp
  • SHOW search_path; 결과가 app, public

이 나옵니다. psql은 PostgreSQL의 공식 CLI입니다. (PostgreSQL)

11. 호스트에서 psql로 접속

가장 권장하는 방식은 PGPASSFILE입니다.

PGPASSFILE="$PWD/secrets/pgpass" psql -h 127.0.0.1 -p 5432 -U app -d app

PGPASSFILE은 PostgreSQL이 공식 지원하는 방식이고, PGPASSWORD보다 권장됩니다. PGPASSWORD는 일부 OS에서 프로세스 환경변수가 노출될 수 있어 보안상 비권장입니다. (PostgreSQL)

임시 테스트용으로만 쓰고 싶다면:

PGPASSWORD="$(cat secrets/app.password)" psql -h 127.0.0.1 -p 5432 -U app -d app

하지만 기본 방식으로 삼지는 않는 편이 좋습니다. (PostgreSQL)

12. 다른 Compose 프로젝트의 앱 컨테이너에서 접속

이 문서의 기본 방향은 external network 기반 Compose 간 통신입니다.

다른 폴더의 Spring Boot 앱 Compose 예시는 아래처럼 두면 됩니다.

services:
  app:
    build: .
    restart: unless-stopped
    networks:
      - shared
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres-db:5432/app
      SPRING_DATASOURCE_USERNAME: app
      SPRING_DATASOURCE_PASSWORD: ${APP_DB_PASSWORD}

networks:
  shared:
    external: true
    name: shared-app-net

여기서 중요한 점은 하나입니다.

다른 Compose의 앱 컨테이너는 127.0.0.1로 붙으면 안 됩니다.
그 컨테이너 안의 127.0.0.1은 DB가 아니라 앱 컨테이너 자기 자신이기 때문입니다. external network에 붙은 DB alias postgres-db를 써야 합니다. Compose 네트워크 문서는 같은 네트워크 안의 서비스가 이름으로 발견 가능하다고 설명합니다. (Docker Documentation)

13. 호스트에서 직접 실행하는 Spring Boot

호스트 JVM에서 직접 실행하는 Spring Boot는 Docker 내부 DNS를 못 봅니다.
따라서 호스트에서 실행하는 앱은 127.0.0.1:5432로 붙어야 합니다.

예시 application-local.yml:

spring:
  config:
    import: "optional:configtree:./secrets/"
  datasource:
    url: jdbc:postgresql://127.0.0.1:5432/app
    username: app
    password: ${app.password}

Spring Boot는 외부 설정과 configtree:를 지원하고, spring.datasource.*로 DataSource를 구성합니다. Docker의 PostgreSQL 네트워킹 가이드도 host-to-container 접속은 localhost/127.0.0.1 published port를 쓰는 방식으로 설명합니다. (Home)

14. 외부 별도 PC(Windows 포함)에서 DataGrip 등으로 접속

서버의 DB 포트는 127.0.0.1에만 bind되어 있으므로, 외부 PC는 직접 5432로 붙지 않습니다.
대신 SSH 터널을 엽니다. PostgreSQL 공식 문서는 SSH 터널을 PostgreSQL 연결 보호 방법으로 설명합니다. (PostgreSQL)

외부 PC에서:

ssh -L 15432:127.0.0.1:5432 ubuntu@your-server

이 SSH 프로세스가 살아 있는 동안, 그 PC의 127.0.0.1:15432는 로컬 포트 전체에 열립니다.
즉, 같은 PC의 DataGrip도 그대로 사용할 수 있습니다. PostgreSQL 문서가 설명하는 SSH 터널은 클라이언트 머신의 로컬 포트에서 원격 서버의 포트로 트래픽을 전달하는 구조입니다. (PostgreSQL)

DataGrip 설정:

  • Host: 127.0.0.1
  • Port: 15432
  • Database: app
  • User: app
  • Password: app.password

Windows에서 psql을 쓴다면 password file 기본 위치는 %APPDATA%\postgresql\pgpass.conf 입니다. PostgreSQL 문서가 그렇게 설명합니다. (PostgreSQL)

15. 백업

최소한 논리 백업은 함께 가져가세요.

sudo docker compose exec -T db pg_dump -U app -d app -Fc > backups/app-$(date +%F-%H%M%S).dump

복구는 이렇게 합니다.

sudo docker compose exec -T db pg_restore -U postgres -d app --clean --if-exists < backups/파일명.dump

pg_dump/pg_restore는 PostgreSQL의 표준 논리 백업 도구입니다. 또 업그레이드 문서는 더 새로운 dump 도구 사용을 권장하므로, 호스트 client도 18 계열로 맞추는 편이 안전합니다. (PostgreSQL)

추가내용 : 호스트에서 복구

pg_restore -h 127.0.0.1 -p 5432 -U postgres -d app --clean --if-exists backups/파일명.dump

추가내용 : Plain SQL 로 처리

백업

sudo docker compose exec -T db pg_dump -Fp -U app -d app > backups/app-$(date +%F-%H%M%S).sql

복구

sudo docker compose exec -T db psql -U postgres -d app < backups/파일명.sql

pg_dump/pg_restore는 PostgreSQL의 표준 논리 백업 도구입니다. 또 업그레이드 문서는 더 새로운 dump 도구 사용을 권장하므로, 호스트 client도 18 계열로 맞추는 편이 안전합니다. (PostgreSQL)

16. 복붙용 최종 실행 세트

아래 순서대로 하면 됩니다.

cd ~/apps/postgresql
mkdir -p init secrets backups

sudo docker network inspect shared-app-net >/dev/null 2>&1 || docker network create shared-app-net

openssl rand -hex 24 > secrets/postgres.password
openssl rand -hex 24 > secrets/app.password
chmod 600 secrets/postgres.password secrets/app.password

APP_PASS="$(tr -d '\n' < secrets/app.password)"

cat > secrets/app-bootstrap.env <<EOF
APP_DB_USER=app
APP_DB_NAME=app
APP_DB_PASSWORD=${APP_PASS}
EOF
chmod 600 secrets/app-bootstrap.env

cat > secrets/pgpass <<EOF
127.0.0.1:*:app:app:${APP_PASS}
EOF
chmod 600 secrets/pgpass

cat > compose.yaml <<'YAML'
services:
  db:
    image: postgres:18
    restart: unless-stopped

    env_file:
      - ./secrets/app-bootstrap.env

    environment:
      POSTGRES_USER: postgres
      POSTGRES_DB: postgres
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password

    secrets:
      - postgres_password

    volumes:
      - pgdata:/var/lib/postgresql
      - ./init:/docker-entrypoint-initdb.d:ro
      - ./backups:/backups

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

    ports:
      - "127.0.0.1:5432:5432"

    networks:
      shared:
        aliases:
          - postgres-db

secrets:
  postgres_password:
    file: ./secrets/postgres.password

volumes:
  pgdata:

networks:
  shared:
    external: true
    name: shared-app-net
YAML

cat > init/10-create-app-db.sh <<'SH'
#!/usr/bin/env bash
set -Eeuo pipefail

: "${APP_DB_USER:?APP_DB_USER is required}"
: "${APP_DB_NAME:?APP_DB_NAME is required}"
: "${APP_DB_PASSWORD:?APP_DB_PASSWORD is required}"

psql -v ON_ERROR_STOP=1 \
  --username "$POSTGRES_USER" \
  --dbname "$POSTGRES_DB" \
  --set=app_db_user="$APP_DB_USER" \
  --set=app_db_name="$APP_DB_NAME" \
  --set=app_db_password="$APP_DB_PASSWORD" <<'SQL'
CREATE ROLE :"app_db_user" LOGIN PASSWORD :'app_db_password';
CREATE DATABASE :"app_db_name" OWNER :"app_db_user";
SQL

psql -v ON_ERROR_STOP=1 \
  --username "$POSTGRES_USER" \
  --dbname "$APP_DB_NAME" \
  --set=app_db_user="$APP_DB_USER" \
  --set=app_db_name="$APP_DB_NAME" <<'SQL'
CREATE SCHEMA IF NOT EXISTS app AUTHORIZATION :"app_db_user";
ALTER DATABASE :"app_db_name" SET search_path TO app, public;
SQL
SH

chmod +x init/10-create-app-db.sh

cat > .gitignore <<'EOF'
secrets/
backups/
EOF

docker compose config >/dev/null || exit 1

sudo docker compose down -v
sudo docker compose up -d

until sudo docker compose exec db pg_isready -U postgres -d postgres >/dev/null 2>&1; do
  sleep 2
done

sudo docker compose exec db psql -U postgres -d postgres -c '\du'
sudo docker compose exec db psql -U postgres -d postgres -c '\l'
sudo docker compose exec db psql -U app -d app -c 'SHOW search_path;'

그다음 호스트에서 psql 18을 설치했다면:

PGPASSFILE="$PWD/secrets/pgpass" psql -h 127.0.0.1 -p 5432 -U app -d app

17. 요약

  • DB Compose는 독립 운영
  • DB 데이터는 named volume
  • 여러 Compose 프로젝트는 shared-app-net external network 공유
  • 호스트 접속은 127.0.0.1:5432
  • 컨테이너 간 접속은 postgres-db:5432
  • 외부 Windows/DataGrip 접속은 SSH 터널 후 127.0.0.1:15432
  • 호스트 psql은 가능하면 postgresql-client-18로 맞춤
  • init는 첫 sudo compose up -d 전에 준비. 재초기화가 필요하면 sudo docker compose down -v