본문 바로가기
AI Development/Workflow

Rootless Docker daemon에 PostgreSQL 18 설치하기

by JLearn 2026. 9. 2.
반응형

Ubuntu 24.04에서 PostgreSQL 전용 OS 사용자 postgres-svc를 만들고, 이 사용자가 독립적인 Rootless Docker daemon을 실행하도록 구성하는 방법을 정리합니다.

구성의 핵심은 PostgreSQL, Gitea, Codex Worker가 하나의 Docker daemon을 공유하지 않는 것입니다. PostgreSQL은 postgres-svc 사용자에게 속한 별도의 Rootless Docker daemon에서만 실행합니다.

Ubuntu 24.04
│
└─ postgres-svc
   └─ Rootless Docker daemon A
      └─ PostgreSQL Compose
         └─ postgres:18

Gitea와 Codex Worker는 이 daemon을 사용하지 않습니다. 각 서비스가 별도의 OS 사용자와 Rootless Docker daemon을 사용하면 daemon socket, container, image, volume, Compose project의 관리 경계를 서비스 단위로 분리할 수 있습니다.


Docker Engine과 Rootless 실행 환경 준비

Docker 패키지는 host에 한 번 설치하고, 각 서비스 사용자가 자신의 Rootless Docker daemon을 실행합니다.

먼저 Docker 공식 APT 저장소를 등록하는 데 필요한 패키지를 설치합니다.

sudo apt update
sudo apt install -y \
  ca-certificates \
  curl \
  uidmap \
  dbus-user-session \
  systemd-container

Docker 공식 signing key와 APT repository를 등록합니다.

sudo install -m 0755 -d /etc/apt/keyrings

sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update

Docker Engine, Compose plugin, Rootless 지원 패키지를 설치합니다.

sudo apt install -y \
  docker-ce \
  docker-ce-cli \
  containerd.io \
  docker-buildx-plugin \
  docker-compose-plugin \
  docker-ce-rootless-extras

Ubuntu 24.04 이상은 unprivileged user namespace 사용을 기본적으로 제한합니다. Docker 공식 DEB 방식으로 docker-ce-rootless-extras를 설치하면 RootlessKit에 필요한 AppArmor 구성이 패키지에 포함되므로, 이 설치 방식에서는 처음부터 별도의 RootlessKit AppArmor profile을 직접 작성할 필요가 없습니다.

Rootful Docker daemon 사용 여부

Rootless Docker를 사용하기 위해 rootful Docker daemon을 반드시 중지하거나 비활성화할 필요는 없습니다.

Rootful Docker와 Rootless Docker는 서로 다른 daemon과 socket을 사용하므로 같은 host에서 함께 실행할 수 있습니다.

일반적으로 다음과 같이 구분됩니다.

Rootful Docker
└─ /var/run/docker.sock

Rootless Docker
└─ /run/user/<UID>/docker.sock

따라서 rootful Docker daemon이 실행 중이더라도 postgres-svc가 자신의 Rootless Docker socket을 사용하도록 설정되어 있다면 PostgreSQL Rootless Docker 환경에는 문제가 없습니다.

Rootless Docker에서 중요한 것은 각 service user가 자신의 Docker daemon과 socket을 사용하도록 명확히 구분하는 것입니다.

postgres-svc shell에서는 다음처럼 Rootless Docker socket을 지정할 수 있습니다.

export XDG_RUNTIME_DIR=/run/user/$(id -u)
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

이 서버에서 rootful Docker workload를 전혀 사용하지 않고, 관리 대상을 Rootless Docker daemon으로만 제한하려는 경우에는 선택적으로 rootful Docker daemon을 비활성화할 수 있습니다.

sudo systemctl disable --now docker.service docker.socket

이 명령은 Docker 패키지를 삭제하는 것이 아닙니다. host의 rootful Docker daemon을 중지하고 부팅 시 자동으로 시작되지 않도록 설정합니다.

반대로 기존에 rootful Docker container나 Compose workload가 있다면 이 명령을 실행하지 않습니다. Rootless Docker 사용 자체에는 rootful Docker daemon 비활성화가 필요하지 않으므로 기존 rootful workload와 Rootless Docker를 함께 운영할 수 있습니다.

rootful Docker daemon 비활성화는 Rootless Docker 설치의 필수 단계가 아닙니다. 이 문서에서는 서버를 Rootless Docker 전용으로 운영하려는 경우에만 선택적으로 적용할 수 있는 설정으로 설명합니다.


PostgreSQL 전용 OS 사용자 구성

PostgreSQL Rootless Docker daemon 전용 사용자를 생성합니다.

sudo useradd \
  --create-home \
  --home-dir /var/lib/postgres-svc \
  --shell /bin/bash \
  postgres-svc

이 구성에서는 일반 로그인 계정으로 사용할 필요가 없으므로 password login을 잠급니다.

sudo passwd -l postgres-svc

사용자가 로그인 세션을 유지하지 않더라도 systemd --user 서비스가 부팅 후 동작할 수 있도록 linger를 활성화합니다.

sudo loginctl enable-linger postgres-svc

생성 결과를 확인합니다.

id postgres-svc
getent passwd postgres-svc

postgres-svc의 home directory는 다음과 같습니다.

/var/lib/postgres-svc

이 경로 아래에 Rootless Docker data-root와 PostgreSQL 운영 파일을 분리해 배치합니다.


subordinate UID/GID 범위 확인

Rootless Docker는 user namespace 안의 container UID/GID를 host UID/GID에 매핑하기 위해 subordinate UID/GID 범위를 사용합니다.

postgres-svc에 할당된 범위를 확인합니다.

grep '^postgres-svc:' /etc/subuid
grep '^postgres-svc:' /etc/subgid

일반적인 출력 형식은 다음과 같습니다.

postgres-svc:<start>:65536

Rootless Docker를 사용하려면 최소 65,536개의 subordinate UID와 GID가 필요합니다.

범위가 없다면 먼저 현재 시스템의 다른 사용자 범위를 확인합니다.

cat /etc/subuid
cat /etc/subgid

다른 사용자와 겹치지 않는 범위를 선택한 뒤 추가합니다.

예를 들어 다음과 같이 설정할 수 있습니다.

sudo usermod --add-subuids 231072-296607 postgres-svc
sudo usermod --add-subgids 231072-296607 postgres-svc

231072-296607은 정확히 65,536개의 ID 범위를 나타냅니다.

위 숫자는 예시입니다. /etc/subuid/etc/subgid에서 이미 다른 사용자에게 할당된 범위를 그대로 복사하면 안 됩니다. 각 Rootless Docker 사용자는 서로 겹치지 않는 subordinate UID/GID 범위를 가져야 합니다.

설정 후 다시 확인합니다.

grep '^postgres-svc:' /etc/subuid
grep '^postgres-svc:' /etc/subgid

PostgreSQL 전용 Rootless Docker daemon 설치

postgres-svc는 password login을 잠근 서비스 사용자입니다. 이런 계정으로 sudo -iu postgres-svc 또는 sudo su - postgres-svc를 사용하면 systemctl --user가 필요한 user bus를 찾지 못하는 경우가 있습니다.

Docker 공식 Rootless troubleshooting에서는 systemd가 있는 host에서 로그인할 수 없는 사용자로 전환할 때 machinectl shell을 사용하도록 안내합니다.

sudo machinectl shell postgres-svc@

이후 명령은 postgres-svc shell에서 실행합니다.

현재 사용자와 home directory를 확인합니다.

whoami
echo "$HOME"
id

기대 값은 다음과 같습니다.

postgres-svc
/var/lib/postgres-svc

XDG_RUNTIME_DIR을 현재 사용자의 systemd runtime directory로 지정합니다.

export XDG_RUNTIME_DIR=/run/user/$(id -u)

Rootless Docker daemon을 설치합니다.

dockerd-rootless-setuptool.sh install

user service를 활성화하고 시작합니다.

systemctl --user enable --now docker

상태를 확인합니다.

systemctl --user status docker

Docker client가 이 사용자의 Rootless daemon에 연결되는지 확인합니다.

docker info

docker infoServerSecurity Options에 다음 항목이 포함되어야 합니다.

rootless

Docker socket과 data-root 확인

사용자 UID를 확인합니다.

id -u

Rootless Docker의 기본 socket은 다음 형식입니다.

/run/user/<postgres-svc UID>/docker.sock

실제 socket을 확인합니다.

ls -l "$XDG_RUNTIME_DIR/docker.sock"

필요하면 DOCKER_HOST를 명시할 수 있습니다.

export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

기본 Docker data-root는 postgres-svc의 home directory 아래에 생성됩니다.

/var/lib/postgres-svc/.local/share/docker

확인:

docker info | grep -i 'Docker Root Dir'

Docker 공식 Rootless 문서에서는 NFS를 Docker data-root로 사용하는 구성을 지원하지 않는다고 안내합니다. Docker data-root는 local filesystem에 두는 것이 안전합니다.


PostgreSQL Compose 운영 디렉터리와 설정 파일 생성

운영 파일은 postgres-svc의 home directory 아래에 다음과 같이 구성합니다.

/var/lib/postgres-svc/platform/postgresql/
├─ compose.yaml
├─ .env.example
├─ .env
├─ init/
│  └─ 10-create-app-databases.sh
├─ scripts/
│  └─ provision-app-databases.sh
└─ secrets/
   ├─ postgres_admin_password
   ├─ gitea_db_password
   └─ codex_agent_db_password

여기서 역할은 다음과 같습니다.

경로 역할
compose.yaml PostgreSQL 18 container, volume, port, secret 구성
.env.example host bind IP와 port의 예제 설정
.env 실제 서버에서 사용하는 Compose 환경 변수
init/ PostgreSQL volume 최초 초기화 시 실행할 script
scripts/ 초기화 이후 DB/role을 다시 확인하거나 보정하는 script
secrets/ PostgreSQL 관리자 및 application DB password

운영 디렉터리 생성

postgres-svc계정에서 나와 root 권한으로 기본 디렉터리를 생성합니다.

sudo install -d -o postgres-svc -g postgres-svc \
  /var/lib/postgres-svc/platform/postgresql

sudo install -d -o postgres-svc -g postgres-svc \
  /var/lib/postgres-svc/platform/postgresql/init

sudo install -d -o postgres-svc -g postgres-svc \
  /var/lib/postgres-svc/platform/postgresql/scripts

sudo install -d -m 700 -o postgres-svc -g postgres-svc \
  /var/lib/postgres-svc/platform/postgresql/secrets

이후 postgres-svc shell로 전환합니다.

sudo machinectl shell postgres-svc@

운영 디렉터리로 이동합니다.

cd /var/lib/postgres-svc/platform/postgresql

현재 위치를 확인합니다.

pwd

정상이라면 다음 경로가 출력됩니다.

/var/lib/postgres-svc/platform/postgresql

compose.yaml 생성

PostgreSQL 18을 실행할 Compose 파일을 생성합니다.

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

    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_admin_password

    ports:
      - "${POSTGRES_BIND_IP:?POSTGRES_BIND_IP is required}:${POSTGRES_PORT:-5432}:5432"

    volumes:
      - postgres-data:/var/lib/postgresql
      - ./init:/docker-entrypoint-initdb.d:ro

    secrets:
      - postgres_admin_password
      - gitea_db_password
      - codex_agent_db_password

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

volumes:
  postgres-data:

secrets:
  postgres_admin_password:
    file: ./secrets/postgres_admin_password

  gitea_db_password:
    file: ./secrets/gitea_db_password

  codex_agent_db_password:
    file: ./secrets/codex_agent_db_password
EOF

이 구성에서 중요한 부분은 다음과 같습니다.
PostgreSQL 18 공식 image를 사용합니다.

image: postgres:18

관리자 password를 Compose 파일이나 .env에 직접 기록하지 않고 secret 파일에서 읽습니다.

POSTGRES_PASSWORD_FILE: /run/secrets/postgres_admin_password

POSTGRES_BIND_IP이 설정되지 않으면 Compose가 설정 오류를 발생시키도록 합니다. 이를 통해 실수로 모든 host interface에 PostgreSQL port를 publish하는 상황을 방지합니다.

ports:
  - "${POSTGRES_BIND_IP:?POSTGRES_BIND_IP is required}:${POSTGRES_PORT:-5432}:5432"

PostgreSQL 18에서는 named volume을 다음 위치에 연결합니다.

volumes:
  - postgres-data:/var/lib/postgresql

PostgreSQL 공식 image는 18부터 기본 PGDATA/var/lib/postgresql/18/docker와 같은 major version별 경로로 사용하며, Docker volume 대상은 /var/lib/postgresql입니다.

.env.example 생성

실제 서버 값을 넣기 전에 예제 환경 파일을 만듭니다.

cat > .env.example <<'EOF'
POSTGRES_BIND_IP=
POSTGRES_PORT=5432
EOF

.env.example에는 password와 같은 secret을 넣지 않습니다.

실제 .env 파일은 뒤에서 서버의 private/internal IP를 확인한 뒤 생성합니다.

최초 DB/role 생성 script 작성

PostgreSQL 공식 image는 empty data directory를 최초 초기화할 때 /docker-entrypoint-initdb.d 아래의 script를 실행합니다.

Gitea와 Codex Agent용 DB와 role을 생성하는 script를 작성합니다.

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

GITEA_DB_PASSWORD="$(cat /run/secrets/gitea_db_password)"
CODEX_AGENT_DB_PASSWORD="$(cat /run/secrets/codex_agent_db_password)"

psql \
  --username "$POSTGRES_USER" \
  --dbname postgres \
  --set=ON_ERROR_STOP=1 \
  --set=gitea_password="$GITEA_DB_PASSWORD" \
  --set=codex_agent_password="$CODEX_AGENT_DB_PASSWORD" <<'EOSQL'

CREATE ROLE gitea
  WITH LOGIN
  PASSWORD :'gitea_password';

CREATE DATABASE gitea
  WITH OWNER gitea;

CREATE ROLE codex_agent
  WITH LOGIN
  PASSWORD :'codex_agent_password';

CREATE DATABASE codex_agent
  WITH OWNER codex_agent;

EOSQL
EOF

실행 권한을 부여합니다.

chmod 750 init/10-create-app-databases.sh

이 script는 PostgreSQL data volume의 최초 초기화 시점에 사용하는 것을 전제로 합니다.

최종적으로 다음 DB와 role이 생성됩니다.

database: gitea
role:     gitea

database: codex_agent
role:     codex_agent

/docker-entrypoint-initdb.d의 script는 기존 PostgreSQL data가 이미 존재하면 다시 자동 실행되지 않습니다. 초기 구축 이후 role이나 DB 구성을 보정하려면 별도의 provisioning script를 사용합니다.

운영 중 DB/role 보정 script 작성

PostgreSQL volume이 이미 만들어진 뒤에도 DB와 role을 확인하고 필요한 항목을 생성할 수 있도록 idempotent(멱등성) provisioning script를 작성합니다.

cat > scripts/provision-app-databases.sh <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"

cd "$PROJECT_DIR"

GITEA_DB_PASSWORD="$(cat secrets/gitea_db_password)"
CODEX_AGENT_DB_PASSWORD="$(cat secrets/codex_agent_db_password)"

docker compose exec -T postgres \
  psql \
    --username postgres \
    --dbname postgres \
    --set=ON_ERROR_STOP=1 \
    --set=gitea_password="$GITEA_DB_PASSWORD" \
    --set=codex_agent_password="$CODEX_AGENT_DB_PASSWORD" <<'EOSQL'

SELECT format(
  'CREATE ROLE gitea WITH LOGIN PASSWORD %L',
  :'gitea_password'
)
WHERE NOT EXISTS (
  SELECT 1
  FROM pg_roles
  WHERE rolname = 'gitea'
)
\gexec

SELECT format(
  'ALTER ROLE gitea WITH LOGIN PASSWORD %L',
  :'gitea_password'
)
\gexec

SELECT 'CREATE DATABASE gitea OWNER gitea'
WHERE NOT EXISTS (
  SELECT 1
  FROM pg_database
  WHERE datname = 'gitea'
)
\gexec

ALTER DATABASE gitea OWNER TO gitea;

SELECT format(
  'CREATE ROLE codex_agent WITH LOGIN PASSWORD %L',
  :'codex_agent_password'
)
WHERE NOT EXISTS (
  SELECT 1
  FROM pg_roles
  WHERE rolname = 'codex_agent'
)
\gexec

SELECT format(
  'ALTER ROLE codex_agent WITH LOGIN PASSWORD %L',
  :'codex_agent_password'
)
\gexec

SELECT 'CREATE DATABASE codex_agent OWNER codex_agent'
WHERE NOT EXISTS (
  SELECT 1
  FROM pg_database
  WHERE datname = 'codex_agent'
)
\gexec

ALTER DATABASE codex_agent OWNER TO codex_agent;

EOSQL
EOF

Idempotent (멱등성 / 멱등) : "동일한 작업을 여러 번 반복해서 실행하더라도, 결과가 항상 같으며 시스템에 부작용(에러)을 일으키지 않는 성질"을 뜻합니다.

실행 권한을 부여합니다.

chmod 750 scripts/provision-app-databases.sh

이 script는 다음 상황에서 사용할 수 있습니다.

  • 최초 초기화 script가 실행되지 않은 기존 volume을 사용하는 경우
  • application DB 또는 role이 누락되었는지 확인하는 경우
  • gitea 또는 codex_agent password secret을 변경한 뒤 role password를 다시 적용하는 경우

생성된 파일 확인

현재까지 생성된 파일을 확인합니다.

find . -maxdepth 2 -type f -print

다음과 비슷하게 출력되어야 합니다.

./compose.yaml
./.env.example
./init/10-create-app-databases.sh
./scripts/provision-app-databases.sh

디렉터리까지 확인하려면 다음 명령을 사용합니다.

ls -la
ls -la init scripts secrets

이 시점에서는 secrets/ 내부의 실제 password 파일과 .env가 아직 없어도 정상입니다. 이후 단계에서 서버 IP를 결정하고 secret을 생성합니다.


PostgreSQL 접속용 private bind IP 설정

다른 Rootless Docker daemon에서 실행되는 Gitea container는 PostgreSQL container의 127.0.0.1을 사용할 수 없습니다.

각 Rootless Docker daemon은 서로 다른 network namespace와 Docker network를 사용하므로, Gitea에서 PostgreSQL로 접근하려면 host에 publish된 PostgreSQL endpoint를 사용해야 합니다.

서버의 주소를 확인합니다.

ip -br address
ip route

예를 들어 서버의 내부 IP가 다음과 같다고 가정합니다.

192.168.10.50

이 주소는 예시입니다. 실제 환경에서는 Gitea가 도달할 수 있고 외부 공개 범위를 통제할 수 있는 host private/internal IP를 선택합니다.

Docker에서 port를 특정 host IP에 publish하면 다음과 같은 형태가 됩니다.

<HOST_IP>:<HOST_PORT>:<CONTAINER_PORT>

PostgreSQL의 경우 다음과 같습니다.

192.168.10.50:5432:5432

5432:5432처럼 host IP를 생략하면 기본적으로 모든 host interface에 publish될 수 있으므로 DB 서비스에서는 명시적으로 bind IP를 지정하는 편이 안전합니다.


PostgreSQL 환경 파일과 Secret 생성

postgres-svc shell로 전환합니다.

sudo machinectl shell postgres-svc@

운영 디렉터리로 이동합니다.

cd /var/lib/postgres-svc/platform/postgresql

.env 파일 생성

예제 파일을 복사합니다.

cp .env.example .env
chmod 600 .env

편집합니다.

editor .env

예:

POSTGRES_BIND_IP=192.168.10.50
POSTGRES_PORT=5432

POSTGRES_BIND_IP에는 앞에서 확인한 실제 서버 내부 IP를 입력합니다.

compose.yaml에서 POSTGRES_BIND_IP이 비어 있으면 시작하지 않도록 ${POSTGRES_BIND_IP:?POSTGRES_BIND_IP is required} 형식으로 구성했습니다. 이렇게 하면 실수로 PostgreSQL이 0.0.0.0:5432에 publish되는 상황을 줄일 수 있습니다.

Secret 파일 생성

secret 디렉터리를 준비합니다.

mkdir -p secrets
chmod 700 secrets

현재 shell에서 새로 만드는 파일의 기본 권한을 제한합니다.

umask 077

PostgreSQL 관리자와 애플리케이션 DB 계정의 password를 생성합니다.

openssl rand -base64 36 > secrets/postgres_admin_password
openssl rand -base64 36 > secrets/gitea_db_password
openssl rand -base64 36 > secrets/codex_agent_db_password

또는 지정하여 password를 생성합니다.

echo -n 'postgres_super_secret_password' > secrets/postgres_admin_password
echo -n 'gitea_super_secret_password' > secrets/gitea_db_password
echo -n 'codex_agent_super_secret_password' > secrets/codex_agent_db_password

권한을 다시 확인합니다.

chmod 600 secrets/*
ls -la secrets

구조는 다음과 같습니다.

secrets/
├─ postgres_admin_password
├─ gitea_db_password
└─ codex_agent_db_password

gitea_db_password는 이후 Gitea 전용 서비스 계정이 DB에 접속할 때 필요합니다. Gitea 설치 단계에서 필요한 파일만 안전하게 별도 전달하고, PostgreSQL의 전체 secrets 디렉터리를 공유하지 않는 것이 좋습니다.

Secret 파일은 Git에 commit하지 않습니다. 이 디렉터리를 이후 Git 저장소에 포함한다면 .gitignore에서도 secrets/* 실제 값과 .env를 제외해야 합니다.


Compose 구성 검증과 PostgreSQL 18 시작

postgres-svc shell에서 Docker socket을 명확히 지정합니다.

export XDG_RUNTIME_DIR=/run/user/$(id -u)
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

운영 디렉터리로 이동합니다.

cd /var/lib/postgres-svc/platform/postgresql

Compose 최종 구성을 검증합니다.

docker compose config

핵심적으로 다음과 같은 구성이 반영되어 있는지 확인합니다.

services:
  postgres:
    image: postgres:18
    ports:
      - "<private-ip>:5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql

PostgreSQL 18 data directory

PostgreSQL 공식 Docker image는 PostgreSQL 18부터 기본 PGDATA 경로가 major version별 디렉터리로 변경되었습니다.

PostgreSQL 18의 기본값은 다음과 같습니다.

/var/lib/postgresql/18/docker

동시에 image의 Docker VOLUME 대상은 다음과 같이 변경되었습니다.

/var/lib/postgresql

따라서 PostgreSQL 18에서는 named volume을 다음 위치에 연결하는 구성이 맞습니다.

volumes:
  - postgres-data:/var/lib/postgresql

PostgreSQL 17 이하에서 일반적으로 사용하던 /var/lib/postgresql/data에 그대로 고정하는 방식과 구분해야 합니다.

Image pull과 container 시작

PostgreSQL image를 받습니다.

docker compose pull

Compose project를 시작합니다.

docker compose up -d

상태를 확인합니다.

docker compose ps

로그를 확인합니다.

docker compose logs -f postgres

healthcheck가 정의되어 있다면 실제 container 이름을 먼저 확인합니다.

docker compose ps

그 다음 health 상태를 조회할 수 있습니다.

docker inspect \
  --format '{{json .State.Health}}' \
  <실제-container-name>

Compose가 생성하는 container 이름은 project name, Compose 버전, scale 구성 등에 따라 달라질 수 있으므로 문서에 고정된 이름보다 docker compose ps 결과를 기준으로 확인하는 것이 안전합니다.


Gitea와 Codex Agent용 DB/role 확인

PostgreSQL 공식 image는 비어 있는 data directory를 처음 초기화할 때만 /docker-entrypoint-initdb.d에 있는 초기화 script를 실행합니다.

앞에서 작성한 10-create-app-databases.sh는 다음 DB와 role을 생성합니다.

database: gitea
role:     gitea

database: codex_agent
role:     codex_agent

DB 목록을 확인합니다.

docker compose exec -T postgres \
  psql -U postgres -d postgres -c '\l'

-T : 가상 터미널(TTY) 생성을 끕니다. (스크립트/자동화용)
docker compose로 띄운 postgres 컨테이너 안에 관리자 계정(-U postgres)으로 postgres DB(-d postgres)에 접속하여 현재 만들어져 있는 데이터베이스 목록(\l)을 터미널에 보여달라는 명령어입니다.

role을 확인합니다.

docker compose exec -T postgres \
  psql -U postgres -d postgres -c '\du'

docker compose로 띄운 postgres 컨테이너에 접속하여, 어떤 사용자 계정(Role)들이 생성되어 있고 어떤 권한을 가지고 있는지 목록으로 확인해달라는 명령어입니다.

초기화 이후 DB/role 구성을 변경한 경우

이미 PostgreSQL volume이 초기화된 뒤 10-create-app-databases.sh를 추가하거나 수정해도 공식 entrypoint가 해당 script를 다시 실행하지 않습니다.

앞에서 작성한 반복 실행 가능한 provisioning script를 사용합니다.

./scripts/provision-app-databases.sh

두 script의 역할을 구분하면 다음과 같습니다.

구분 실행 시점 용도
init/10-create-app-databases.sh empty volume 최초 초기화 최초 DB/role 생성
scripts/provision-app-databases.sh 운영 중 필요 시 누락된 DB/role 보정 및 재적용

운영 환경에서는 volume을 삭제해서 init script를 억지로 다시 실행하는 방식보다 idempotent provisioning script를 사용하는 편이 안전합니다.


Host endpoint와 DB 접속 확인

host에서 PostgreSQL port가 어느 주소에 bind되어 있는지 확인합니다.

ss -lnt | grep ':5432'

예를 들어 다음처럼 실제 private IP에만 listen되어야 합니다.

192.168.10.50:5432

의도하지 않은 다음 상태는 피합니다.

0.0.0.0:5432

host에 psql client가 설치되어 있다면 Gitea DB 계정으로 실제 TCP 접속을 확인할 수 있습니다.

cd /var/lib/postgres-svc/platform/postgresql

PGPASSWORD="$(cat secrets/gitea_db_password)" \
psql \
  -h 192.168.10.50 \
  -p 5432 \
  -U gitea \
  -d gitea \
  -c 'select current_user, current_database();'

192.168.10.50은 실제 POSTGRES_BIND_IP 값으로 변경합니다.

정상이라면 current_usercurrent_database가 모두 gitea로 표시됩니다.


네트워크와 방화벽 적용 기준

PostgreSQL port를 특정 private IP에 publish하더라도 그 IP가 LAN의 다른 host에서 접근 가능한 주소라면 5432/tcp 역시 해당 네트워크에서 접근 가능한 endpoint가 될 수 있습니다.

따라서 다음 보안 계층을 함께 적용하는 것이 좋습니다.

  • Compose에서 PostgreSQL port를 명시적인 host private IP에만 bind
  • PostgreSQL 계정별 강력한 password 사용
  • SCRAM 기반 password 인증 사용
  • host firewall에서 실제 필요한 source만 허용
  • 필요하면 상위 network ACL 또는 방화벽에서도 source segment 제한
  • 인터넷 또는 전체 사내망에 5432/tcp를 일괄 허용하지 않음

Docker 공식 문서에서도 published port는 host 외부에서 접근 가능한 경로가 될 수 있으므로 DB와 같은 민감한 서비스의 publish 범위를 주의해서 설정하도록 안내합니다.

Rootless Docker의 port forwarding은 기본 구성에서 원래 container source IP를 그대로 전달하지 않을 수 있습니다. 따라서 접근 제어를 특정 container IP 하나에만 의존하지 않는 것이 좋습니다.

이 구성에서는 다음을 함께 사용합니다.

명시적인 host bind IP
        +
DB 계정/SCRAM 인증
        +
host/network firewall

Gitea Rootless Docker daemon 구성이 완료된 뒤에는 Gitea 쪽에서 one-shot psql container를 실행해 다음 경로의 실제 연결을 반드시 확인하는 것이 좋습니다.

Gitea container
   ↓
Gitea Rootless Docker network
   ↓
Host private IP:5432
   ↓
PostgreSQL Rootless port forwarding
   ↓
PostgreSQL container:5432

Rootless Docker와 PostgreSQL 운영 명령

postgres-svc 사용자로 전환합니다.

sudo machinectl shell postgres-svc@

필요하면 runtime directory와 Docker socket을 지정합니다.

export XDG_RUNTIME_DIR=/run/user/$(id -u)
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

Rootless Docker daemon 관리

상태 확인:

systemctl --user status docker

재시작:

systemctl --user restart docker

로그 확인:

journalctl --user -u docker -f

Docker daemon이 부팅 후 사용자 로그인 없이 시작되는 것은 앞에서 설정한 loginctl enable-linger postgres-svc와 연결됩니다.

PostgreSQL Compose 관리

운영 디렉터리로 이동합니다.

cd /var/lib/postgres-svc/platform/postgresql

상태 확인:

docker compose ps

PostgreSQL container 재시작:

docker compose restart postgres

로그 확인:

docker compose logs -f postgres

전체 Compose project를 중지하려면 다음을 사용합니다.

docker compose down

named volume의 DB 데이터는 기본적으로 유지됩니다.

반대로 다음과 같이 -v를 추가하면 Compose volume까지 삭제될 수 있으므로 운영 DB에서는 주의해야 합니다.

docker compose down -v

docker compose down -v는 PostgreSQL named volume을 삭제할 수 있습니다. DB 초기화를 다시 할 목적이 명확한 개발 환경이 아니라면 운영 서버에서는 사용하지 않는 것이 안전합니다.


설치 완료 확인

최종적으로 아래 항목을 확인합니다.

Rootless Docker daemon:

systemctl --user status docker
docker info

Docker socket:

ls -l "$XDG_RUNTIME_DIR/docker.sock"

PostgreSQL Compose:

cd /var/lib/postgres-svc/platform/postgresql
docker compose ps

DB와 role:

docker compose exec -T postgres \
  psql -U postgres -d postgres -c '\l'

docker compose exec -T postgres \
  psql -U postgres -d postgres -c '\du'

host bind:

ss -lnt | grep ':5432'

전체 구성이 정상이라면 다음 구조가 됩니다.

Ubuntu 24.04
│
├─ rootful Docker daemon
│  └─ disabled
│
└─ postgres-svc
   ├─ HOME
   │  └─ /var/lib/postgres-svc
   │
   ├─ Rootless Docker
   │  ├─ socket
   │  │  └─ /run/user/<UID>/docker.sock
   │  └─ data-root
   │     └─ /var/lib/postgres-svc/.local/share/docker
   │
   └─ platform/postgresql
      ├─ compose.yaml
      ├─ .env
      ├─ init/
      ├─ scripts/
      └─ secrets/

PostgreSQL은 이 Rootless daemon에만 속하며, Gitea와 Codex Worker는 각각 자신에게 할당된 별도 daemon에서 실행합니다.


참고 자료

반응형

댓글