SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-13

데이터베이스 Docker 실행 vs 호스트 설치 비교

PostgreSQL, MySQL, MongoDB 등을 Docker 컨테이너에서 운영할 때 고려해야 할 핵심 요소입니다. 볼륨 마운트 설정, 데이터 영속성 유지, 메인 버전 업그레이드 및 백업 전략을 포함하여 운영 환경에서 발생할 수 있는 실질적인 위험과 해결책을 상세히 설명합니다.

데이터베이스를 Docker에서 실행해야 할까요, 아니면 호스트에서 실행해야 할까요?

데이터베이스는 Docker에서 실행하십시오. 단일 VPS에서 운영하는 애플리케이션 스택의 경우, 컨테이너화된 PostgreSQL, MySQL, MongoDB 또는 Redis를 사용하는 것은 일반적인 운영 환경의 선택이며, 이에 대해 흔히 제기되는 논쟁은 대개 잘못된 방향입니다. 컨테이너는 가상 머신이 아니라 네임스페이스와 cgroup으로 격리된 리눅스 프로세스이므로, 데이터베이스와 디스크 사이에 하이퍼바이저가 존재하지 않습니다. 바인드 마운트(bind mount)나 로컬 네임드 볼륨(named volume)을 사용하면, 패키지 설치 방식과 동일하게 호스트 파일 시스템에 직접 읽기 및 쓰기가 수행됩니다.

진정한 비용은 운영 측면에서 발생합니다. 데이터가 어디에 저장되는지, 해당 디렉터리의 소유권은 누구에게 있는지, 메인 버전 업그레이드는 어떻게 진행되는지, 그리고 백업 복원을 수행해 본 적이 있는지에 따라 이 설정이 적절할지 아니면 재앙이 될지가 결정됩니다. 이 요소들을 제대로 관리한다면 컨테이너는 단지 세부 사항일 뿐입니다. 반대로 이를 소홀히 하면 문제가 발생했을 때 컨테이너를 탓하게 될 것입니다.

이는 모든 서버 데이터베이스에 동일하게 적용되는 결정입니다. 아래 예시에서는 PostgreSQL, MySQL, MongoDB, Redis를 사용하며, 제품별로 중요한 차이점이 있는 경우 별도로 명시합니다.

컨테이너가 실제로 변경하는 것

마운트 지점을 설정했다면 저장 경로가 바뀌지는 않습니다. 동일한 커널, 동일한 페이지 캐시, 동일한 파일 시스템을 사용합니다.

성능상 주의해야 할 실질적인 함정이 하나 있는데, 아무것도 마운트하지 않는 경우입니다. 볼륨을 사용하지 않으면 데이터 디렉터리는 컨테이너의 쓰기 가능 계층(writable layer)에 저장되는데, 이는 이미지 위에 쌓이는 오버레이 파일 시스템입니다. 이곳에 쓰는 작업은 속도가 느리며, 컨테이너가 삭제되면 해당 계층 전체가 사라집니다. "오늘 아침에 데이터베이스가 비어 있었다"는 현상은 바로 이 때문에 발생합니다.

실제로 변경되는 사항은 다음과 같습니다.

  • 수명 주기(Lifecycle). docker compose down은 컨테이너를 파괴합니다. 볼륨에 저장되지 않은 모든 데이터는 함께 사라집니다.
  • 버전. 이미지 태그가 곧 버전입니다. 데이터베이스 컨테이너 내부에는 다음 docker compose pull 이후에도 살아남는 apt upgrade가 존재하지 않습니다.
  • 메모리 계정(Memory accounting). cgroup 제한은 커널이 강제하는 엄격한 장벽이며, 데이터베이스는 이 제한이 존재하는지 알지 못합니다.
  • 사용자. 프로세스는 컨테이너 내부의 숫자 형태 사용자 ID로 실행되며, 이 사용자는 호스트 시스템의 어떤 자원도 소유하지 못할 수 있습니다.

데이터가 저장되는 위치가 모든 것을 결정합니다

두 가지 좋은 선택지와 흔히 저지르는 실수 한 가지가 있습니다.

  • 네임드 볼륨(Named volume): pgdata:/var/lib/postgresql/data. Docker가 /var/lib/docker/volumes/<project>_pgdata/_data에 디렉터리를 생성하며, 이미지의 entrypoint가 첫 실행 시 소유권을 설정합니다. 이것이 기본 권장 사항입니다.
  • 바인드 마운트(Bind mount): /srv/appname/pg:/var/lib/postgresql/data. 사용자가 경로를 직접 선택하므로 권한 문제는 사용자가 직접 해결해야 합니다.
  • 마운트 없음: 위 항목을 참조하십시오. 데이터가 컨테이너 내부에 저장됩니다.

전체적인 장단점은 별도의 주제이며, 바인드 마운트와 네임드 볼륨 비교에서 다룹니다. 데이터베이스의 경우 요약하자면 다음과 같습니다. 호스트 경로를 반드시 알아야 하는 특별한 이유가 없다면 네임드 볼륨을 사용하십시오. 만약 바인드 마운트를 사용해야 한다면, git clean가 접근할 수 있는 프로젝트 디렉터리 내부보다는 /srv/appname/pg와 같이 안정적인 위치에 두어야 합니다.

한 가지 엄격한 제한 사항이 있습니다. NFS(network file system)나 잠금 및 fsync 동작을 검증하지 않은 네트워크 마운트에는 데이터베이스 데이터 디렉터리를 두지 마십시오. 데이터베이스는 fsync 작업이 성공하면 데이터가 안정적인 저장소에 기록되었다고 가정합니다. 이 가정이 틀리면 몇 주 뒤에 데이터 손상이 발생할 수 있습니다.

볼륨이 사라지기 전에 볼륨 이름을 고정하십시오

Compose는 볼륨 이름을 <project>_<volume>로 지정하며, 프로젝트 이름은 기본적으로 디렉터리 이름을 따릅니다. 따라서 볼륨의 식별자는 디렉터리 이름에 의존하게 되는데, 이는 사용자가 별생각 없이 변경하기 쉬운 요소입니다.

/srv/app/srv/app-old으로 이동하거나 compose 파일에서 pgdata 키의 이름을 바꾸면, 다음 docker compose up -d 실행 시 완전히 새로운 빈 볼륨이 생성됩니다. Postgres는 그 안에 새로운 클러스터를 초기화합니다. 컨테이너는 정상적으로 작동하고 애플리케이션도 시작되지만, 모든 테이블은 사라진 상태가 됩니다. 다행인 점은 이전 볼륨이 여전히 이전 이름으로 디스크에 남아 있다는 것입니다.

docker volume ls
docker volume inspect app_pgdata

이런 상황을 방지하려면 이름을 고정하십시오. 프로젝트 이름과 볼륨 이름을 명시적으로 설정해야 합니다.

name: myapp

services:
  db:
    image: postgres:17
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
    name: myapp_pgdata

데이터가 들어 있는 유실된 볼륨이 이미 존재한다면, 데이터베이스를 중지한 상태에서 해당 데이터를 복사하십시오:

docker compose stop db
docker run --rm -v app_pgdata:/from -v myapp_pgdata:/to alpine sh -c 'cp -a /from/. /to/'
docker compose start db

데이터베이스가 실행 중일 때 복사하면 쓰기 작업 중이던 파일들이 손상된 상태로 복사될 수 있습니다. 반드시 먼저 중지하십시오.

데이터 디렉터리의 소유권

공식 Postgres, MySQL 및 MongoDB 이미지는 서버를 권한이 없는 사용자 ID(보통 999)로 실행합니다. 컨테이너가 root로 시작되면, 엔트리포인트는 데이터 디렉터리의 소유권을 해당 사용자로 변경한 뒤 권한을 포기합니다. 이것이 빈 bind mount가 보통 첫 시도에 정상 작동하는 이유입니다.

compose 파일에 user:을 설정하는 순간 문제가 발생합니다. 엔트리포인트가 소유권을 수정할 권한을 잃기 때문입니다. Postgres는 이를 다음과 같이 명시합니다.

initdb: error: could not change permissions of directory "/var/lib/postgresql/data": Operation not permitted

잘못된 모드로 존재하는 데이터 디렉터리는 다른 메시지를 출력합니다. 이 메시지는 chown가 아닌 chmod로 해결해야 하므로 기억해 둘 필요가 있습니다.

FATAL:  data directory "/var/lib/postgresql/data" has invalid permissions
DETAIL:  Permissions should be u=rwx (0700) or u=rwx,g=rx (0750).

root가 소유한 bind mount에서 MongoDB를 실행하면 잠금 파일(lock file) 문제로 실패합니다.

Unable to create/open the lock file: /data/db/mongod.lock (Permission denied). Ensure the user executing mongod is the owner of the lock file and has the appropriate permissions.

해결 방법은 호스트 디렉터리의 소유권을 이름이 아닌 숫자 ID로 변경하는 것입니다.

sudo chown -R 999:999 /srv/appname/pg
sudo chmod 700 /srv/appname/pg
ls -ldn /srv/appname/pg

ls -ldn은 이름 대신 숫자를 출력하며, 999 999이 표시되어야 합니다. 호스트의 postgres 계정과 이미지 내부의 postgres 계정은 서로 관련이 없습니다. 커널은 숫자를 비교하며, 이름은 양쪽에서 각각 별도로 조회됩니다. PUID와 PGID가 호스트 사용자를 컨테이너로 매핑하는 방법에서 해당 매핑 과정을 상세히 다룹니다. rootless Docker나 사용자 네임스페이스 리매핑(user namespace remapping) 환경에서는 숫자가 다시 변경되므로, 999라고 가정하지 말고 실행 중인 컨테이너에서 직접 ID를 확인하십시오.

Named volume을 사용하면 Docker가 빈 디렉터리를 생성하고 엔트리포인트가 소유권을 가지므로, 첫 실행 시 이 섹션의 모든 문제가 사라집니다.

업그레이드: 이미지 태그 변경을 통한 패키지 업그레이드

호스트 환경에서 apt upgrade은 마이너 버전을 업데이트합니다. 배포판은 사용자의 의도와 무관하게 데이터베이스 메이저 버전을 강제로 올리지 않으며, 메이저 버전 업그레이드를 선택할 경우 두 버전의 바이너리를 동시에 설치할 수 있는데, 이는 pg_upgrade이 요구하는 방식과 정확히 일치합니다.

컨테이너 환경에서 태그는 곧 버전이므로, 업그레이드는 단 한 줄을 수정하는 작업입니다. 이로 인해 마이너 업그레이드는 사소한 작업이 되지만, 메이저 업그레이드는 하나의 절차가 됩니다.

postgres:16postgres:17으로 변경하고 docker compose up -d를 실행하면 컨테이너는 즉시 종료됩니다:

PostgreSQL Database directory appears to contain a database; Skipping initialization
FATAL:  database files are incompatible with server
DETAIL:  The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.

아무것도 손상되지 않습니다. 새로운 바이너리는 메이저 버전 간에 변경된 이전 디스크 카탈로그 레이아웃을 읽기를 거부합니다. 태그를 다시 postgres:16로 되돌리면 컨테이너는 다시 시작됩니다. 이러한 롤백은 컨테이너가 제공하는 진정한 업그레이드 이점 중 하나입니다.

지원되는 업그레이드 경로는 덤프 후 복원입니다. PostgreSQL은 더 최신 버전의 클라이언트로 덤프를 수행하는 것을 권장하므로, compose 네트워크상에서 여전히 실행 중인 이전 서버를 대상으로 새 이미지에서 덤프를 실행하십시오:

docker run --rm --network myapp_default -e PGPASSWORD="$POSTGRES_PASSWORD" \
  postgres:17 pg_dumpall -h db -U postgres > /srv/backups/all.sql
ls -lh /srv/backups/all.sql
tail -n 2 /srv/backups/all.sql

파일 크기는 최소 수십 킬로바이트 이상이어야 하며, 마지막 줄은 PostgreSQL database cluster dump complete으로 끝나야 합니다. 파일 크기가 수백 바이트에 불과하다면 덤프가 실패한 것이며, 이 상태에서 볼륨을 삭제하면 데이터가 모두 유실됩니다. 반드시 확인 후 다음을 진행하십시오:

docker compose down
docker volume rm myapp_pgdata
# edit the compose file: image: postgres:17
docker compose up -d db
docker compose exec -T db psql -U postgres -f /dev/stdin < /srv/backups/all.sql

다른 엔진들의 특성은 다음과 같습니다:

  • MySQL 8은 시작 시 자체 데이터 사전을 업그레이드하므로, 마이너 태그 변경은 일반적으로 재시작만으로 충분합니다. 릴리스 시리즈 간의 메이저 업그레이드 전에는 반드시 릴리스 노트를 읽고, 어떤 경우든 먼저 덤프를 수행하십시오.
  • MariaDB는 새 버전으로 서버가 올라온 후 mariadb-upgrade을 실행할 것을 요구합니다.
  • MongoDB는 한 번에 하나의 메이저 버전씩 업그레이드해야 하며, 각 단계 후 다음 단계로 넘어가기 전에 기능 호환성 버전(feature compatibility version)을 설정해야 합니다. 버전을 건너뛰면 mongod은 시작을 거부하며 featureCompatibilityVersion을 명시하는 UPGRADE PROBLEM 라인을 로그에 남깁니다. MongoDB 7.0부터는 명령 실행 시 명시적인 확인 플래그인 db.adminCommand({ setFeatureCompatibilityVersion: "8.0", confirm: true })이 필요합니다.
  • Redis는 이전 스냅샷 파일을 문제없이 불러오지만 더 최신 버전의 파일은 불러오지 못할 수 있으므로, 업그레이드는 재시작으로 가능하지만 다운그레이드 시에는 데이터 로드에 실패할 수 있습니다.

일반적인 규칙은 다음과 같습니다: 컨테이너는 다운그레이드를 쉽게 만들어 주지만, 업그레이드 과정을 더 쉽게 만들어 주지는 않습니다.

데이터베이스 컨테이너가 종료 코드 137로 종료되는 이유는 무엇입니까?

커널의 OOM(Out of Memory) 킬러가 해당 프로세스를 강제 종료했기 때문입니다. 137은 128에 시그널 9를 더한 값입니다.

docker compose ps
docker inspect myapp-db-1 | grep -i oomkilled
journalctl -k | tail -n 20

docker compose ps 명령을 실행하면 Exited (137)이 표시되고, inspect 결과의 해당 줄은 "OOMKilled": true로 나타나며, 커널 로그에는 다음과 같은 항목이 기록됩니다.

Memory cgroup out of memory: Killed process 4711 (postgres) total-vm:2170416kB

이 메커니즘은 많은 사용자를 당황하게 합니다. PostgreSQL과 MySQL은 호스트가 보고하는 전체 메모리 용량을 기준으로 버퍼 크기를 결정합니다. cgroup 제한은 이 수치를 변경하지 않습니다. 16 GB 호스트에 2 GB 제한을 설정하면, 데이터베이스는 마치 16 GB를 사용할 수 있는 것처럼 계획을 세우고, 호스트 자체에 부하가 걸리기도 전에 cgroup이 프로세스를 종료해 버립니다. 따라서 메모리 제한만 설정하는 것으로는 부족합니다. 데이터베이스에 실제 가용 자원을 명시적으로 알려주어야 합니다.

  • PostgreSQL: shared_buffers를 설정하고 work_mem에 주의하십시오. work_mem은 연결당 정렬 작업마다 할당되므로, 넉넉한 값을 설정한 상태에서 50개의 연결이 발생하면 시작 시점이 아닌 부하 발생 시점에 컨테이너가 죽는 주된 원인이 됩니다.
  • MySQL 및 MariaDB: innodb_buffer_pool_size을 설정하십시오. 기본값은 128M입니다. 컨테이너 환경에서는 innodb_dedicated_server를 끄십시오. 이 설정의 목적 자체가 감지된 시스템 메모리를 기준으로 크기를 조정하는 것이기 때문입니다.
  • MongoDB: 호스트 메모리를 기반으로 추정하게 두지 말고 WiredTiger 캐시 크기를 명시적으로 설정하십시오.
  • Redis: maxmemory은 기본적으로 무제한이므로, cgroup이 멈출 때까지 Redis 메모리 사용량이 증가합니다. maxmemory을 컨테이너 제한보다 충분히 낮게 설정하고 적절한 maxmemory-policy를 선택하십시오.

Postgres는 자체적으로도 이 이벤트를 보고하며, 로그에서 다음과 같은 두 줄을 확인할 수 있습니다.

LOG:  server process (PID 123) was terminated by signal 9: Killed
LOG:  terminating any other active sessions due to crash of another server process

하나의 백엔드가 강제 종료되면 공유 메모리의 일관성이 깨질 수 있으므로 다른 모든 백엔드도 재시작해야 합니다. 이는 애플리케이션 입장에서 조용한 이벤트가 아니라 연결 폭주(connection storm)를 유발합니다. Docker Compose에서 메모리 제한 설정하기에서 관련 문법과 mem_limitdeploy.resources 형식의 차이를 다룹니다.

이 문제가 호스트에서 사라지는 것은 아닙니다. 단지 위치가 바뀔 뿐입니다. cgroup이 없으면 데이터베이스는 호스트의 다른 모든 프로세스와 자원을 경쟁하게 되며, 호스트의 OOM 킬러는 점수에 따라 희생자를 선택하는데, 이때 sshd가 선택될 수도 있습니다. 데이터베이스를 예측 가능하게 종료시키는 제한을 두는 것이, 호스트 OOM으로 인해 시스템 접근이 차단되는 상황보다 운영하기에 훨씬 안전합니다.

백업: 내부에서 덤프하고 외부로 백업하기

실행 중인 데이터베이스의 데이터 디렉터리를 복사하는 방식으로 백업하지 마십시오. 서버가 데이터를 기록하는 도중에 파일 수준에서 복사하면 데이터가 일관되지 않은 'torn copy'가 생성되며, 이는 복구 시점에야 문제가 드러납니다.

두 가지 올바른 방법이 있습니다. 데이터베이스가 실행 중일 때 자체 도구로 덤프를 생성한 뒤 해당 덤프 파일을 백업하거나, 컨테이너를 중지하고 볼륨을 콜드(cold) 상태에서 복사하는 것입니다.

docker compose exec -T db pg_dump -U postgres -Fc appdb > /srv/backups/appdb.dump
docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases > /srv/backups/mysql.sql
docker compose exec -T db mongodump --archive --gzip --db appdb > /srv/backups/appdb.archive.gz
docker compose exec -T redis redis-cli BGSAVE

-T는 중요합니다. 이 옵션이 없으면 docker compose exec이 명령에 터미널을 연결할 수 있으며, 터미널 계층은 출력 스트림에 캐리지 리턴(carriage return)을 추가합니다. 이 경우 텍스트 덤프는 복구 시 이상한 오류를 발생시키며, 바이너리 덤프는 단순히 손상됩니다. 이 문제는 백업 시점에는 조용히 넘어가지만, 한 달 뒤 복구 시점에 큰 문제를 일으킵니다.

--single-transaction은 서버 전체를 잠그지 않고도 mysqldump에 InnoDB 테이블의 일관된 스냅샷을 제공합니다.

위 명령들은 각각 하나의 파일을 생성할 뿐입니다. 이것만으로는 백업 시스템이라고 할 수 없습니다. 보관 주기(retention) 관리, 외부 서버로의 복사, 검증 과정이 없기 때문입니다. 이 세 가지 기능을 모두 수행하는 도구에 덤프 디렉터리를 전달하십시오. VPS에서 restic으로 백업하기가 바로 이를 위한 것입니다. /var/lib/docker/volumes이 아닌 /srv/backups를 백업하십시오.

그런 다음 복구 테스트를 수행하십시오. 한 번도 복구해 본 적 없는 백업은 백업이 아니기 때문입니다.

docker compose exec -T db createdb -U postgres restore_test
docker compose exec -T db pg_restore -U postgres -d restore_test < /srv/backups/appdb.dump
docker compose exec -T db psql -U postgres -d restore_test -c '\dt'

\dt은 애플리케이션의 테이블 목록을 출력해야 합니다. 결과가 비어 있거나 Did not find any relations.가 출력된다면, 덤프가 의도한 대로 생성되지 않은 것입니다. 작업이 끝나면 restore_test을 삭제하십시오.

모든 것을 삭제하는 명령어

docker compose down -v.

단순한 down는 컨테이너와 네트워크를 제거합니다. -v은 compose 파일에 선언된 모든 명명된 볼륨과 해당 컨테이너에 연결된 모든 익명 볼륨까지 제거합니다. 확인 절차는 없으며 되돌릴 수도 없습니다. 이는 셀프 호스팅 데이터베이스가 파괴되는 가장 흔한 원인이며, 보통 포럼의 답변을 따라 하다가 무관한 문제를 해결하는 과정에서 발생합니다.

피해 범위를 줄이는 네 가지 방법은 다음과 같습니다.

  • 데이터베이스 볼륨을 external: true로 선언하십시오. Compose는 자신이 소유하지 않은 볼륨은 제거하지 않으므로 -v이 해당 볼륨에 접근할 수 없습니다. 볼륨은 docker volume create myapp_pgdata를 사용하여 한 번만 생성하십시오.
  • 일상적인 재시작에는 docker compose stopdocker compose start을 사용하십시오. Compose에서 down과 stop의 차이에서 각 명령어가 무엇을 제거하는지 자세히 설명합니다.
  • 덤프 파일은 Compose가 관리하는 모든 볼륨 외부의 호스트 경로에 보관하십시오.
  • 데이터가 중요한 스택에서는 문제 해결 답변에 포함된 -v를 절대 복사하여 붙여넣지 마십시오.

데이터베이스 포트를 공개하지 마십시오

다음 설정은 데이터베이스를 공용 인터넷에 노출합니다:

    ports:
      - "5432:5432"

이 설정은 모든 인터페이스에 바인딩됩니다. Docker는 방화벽의 입력 규칙이 패킷을 확인하기 전에 패킷의 목적지를 재작성하여 포트를 게시하며, ufw 규칙은 입력 체인에 위치하므로 ufw deny 5432은 아무런 효과가 없습니다. Docker가 게시한 포트가 ufw를 우회하는 이유에서 체인 순회 과정을 확인할 수 있습니다.

동일한 compose 프로젝트 내의 애플리케이션은 compose 네트워크상의 서비스 이름을 통해 데이터베이스에 접근하므로 포트를 게시할 필요가 없습니다. 해당 블록을 삭제하십시오. 호스트에서 클라이언트를 사용해야 한다면 루프백 인터페이스에만 바인딩하십시오:

    ports:
      - "127.0.0.1:5432:5432"

실제로 어떤 포트가 수신 대기 중인지 확인하십시오:

sudo ss -ltnp | grep 5432

127.0.0.1:5432가 올바른 상태입니다. 0.0.0.0:5432는 누구나 귀하의 비밀번호를 시도할 수 있음을 의미합니다.

어디에 무엇을 실행할 것인가

VPS 한 대에 애플리케이션 하나. 컨테이너를 사용합니다. 고정된 이름을 가진 명명된 볼륨(named volume)을 사용하고, 포트는 외부에 노출하지 않으며, 데이터베이스 설정에 맞춘 메모리 제한을 설정합니다. 매일 밤 호스트 경로로 덤프를 생성하고 restic으로 이를 수집합니다. VPS의 깔끔한 Docker 설치부터 시작하여 전체 스택을 하나의 compose 파일로 관리하고 커밋하십시오. 데이터베이스 버전이 git에서 검토 가능한 한 줄의 코드가 된다는 점은 확실한 이점입니다.

여러 서비스를 운영하는 호스트. 컨테이너를 사용하되, 모든 애플리케이션이 하나의 데이터베이스 서버를 공유하지 않도록 합니다. 서버를 공유하면 모든 애플리케이션이 하나의 업그레이드 일정에 종속되며, 하나의 잘못된 쿼리가 전체 서비스의 장애로 이어질 수 있습니다. 각 컨테이너에 메모리 제한을 설정하여 잘못된 쿼리가 발생해도 해당 애플리케이션 내에서만 영향이 미치도록 격리하십시오. 여러 개의 작은 Postgres 인스턴스를 운영하는 것이 디스크 공간은 조금 더 차지할지라도, 관리 측면에서는 훨씬 효율적입니다.

데이터베이스가 핵심 제품인 경우. 벤더의 패키지 저장소에서 제공하는 패키지를 사용하여 호스트에서 직접 실행하거나, 관리형 서비스를 이용하십시오. pg_upgrade은 두 가지 메인 버전의 바이너리를 동시에 설치해야 하는 경우가 있는데, 이는 패키지 관리자가 제공하는 기능이며 단일 버전 이미지로는 불가능합니다. 데이터베이스가 기기와 디스크를 직접 제어할 때 WAL(write ahead log) 아카이빙을 통한 복제 및 특정 시점 복구(point in time recovery)가 훨씬 수월합니다. 새벽 03:00에 호출을 받지 않으려면 가장 검증되고 지루한 방식을 선택하십시오.

애플리케이션 규모가 작은 경우. 서버형 데이터베이스를 아예 사용하지 않는 것을 고려하십시오. VPS 한 대에서 단일 작성자(single-writer)로 동작하는 웹 애플리케이션은 VPS 운영 환경에서의 SQLite를 사용하는 것이 더 나을 때가 많습니다. 이 경우 백업은 파일 하나로 충분하며, 업그레이드 경로는 라이브러리 버전을 업데이트하는 것만으로 해결됩니다.

FAQ

프로덕션 환경의 데이터베이스를 Docker에서 실행해도 안전합니까?

단일 서버 애플리케이션 스택이라면 안전합니다. 컨테이너는 네임스페이스와 cgroup으로 격리된 리눅스 프로세스일 뿐이므로, 볼륨을 마운트하면 패키지로 설치했을 때와 동일한 호스트 파일 시스템에 데이터를 기록합니다. 위험 요소는 속도가 아니라 운영상의 실수에 있습니다. 이름이 고정되지 않은 볼륨, 잘못된 사용자 ID가 소유한 바인드 마운트, 한 번도 테스트하지 않은 복구 절차, 그리고 docker compose down -v 등이 이에 해당합니다. 이 네 가지만 해결하면 컨테이너 환경도 충분히 안정적입니다. 데이터베이스가 주된 워크로드이며 pg_upgrade, 복제(replication), 특정 시점 복구(point in time recovery)가 필요해지면 그때 호스트 설치 방식으로 전환하십시오.

데이터베이스 데이터 저장에 바인드 마운트와 네임드 볼륨 중 무엇을 사용해야 합니까?

호스트 경로를 반드시 알아야 할 특별한 이유가 없다면 네임드 볼륨을 사용하십시오. Docker가 디렉터리를 생성하고 이미지의 entrypoint가 첫 실행 시 소유권을 설정하므로 권한 문제가 발생하지 않습니다. name:로 볼륨을 명시적으로 고정하거나 external: true으로 표시하십시오. 그렇지 않으면 프로젝트 디렉터리 이름을 바꾸는 것만으로도 새로운 빈 볼륨이 생성되어 데이터베이스가 초기화될 수 있습니다. 바인드 마운트를 사용하려면 호스트 디렉터리의 소유자를 이미지 내부의 실행 사용자 ID(공식 Postgres, MySQL, MongoDB 이미지의 경우 999)로 변경해야 합니다. ls -ldn로 확인하십시오. ls -l는 호스트의 사용자 이름을 보여주지만, 이는 컨테이너 내부에서는 아무런 의미가 없기 때문입니다.

docker compose down -v은 무엇을 삭제합니까?

일반 down와 마찬가지로 컨테이너와 네트워크를 제거하며, -v는 여기에 더해 compose 파일에 선언된 모든 네임드 볼륨과 컨테이너에 연결된 모든 익명 볼륨을 추가로 삭제합니다. 여기에는 데이터베이스도 포함됩니다. 삭제 시 확인 절차는 없으며 복구도 불가능합니다. external: true으로 표시된 볼륨은 삭제되지 않으므로, 데이터베이스 볼륨을 외부(external)로 표시하는 주된 이유가 됩니다. 일상적인 재시작에는 docker compose stopdocker compose start을 사용하십시오.

Docker에서 PostgreSQL을 새로운 메이저 버전으로 업그레이드하려면 어떻게 해야 합니까?

덤프 후 복원(dump and restore) 방식을 사용해야 합니다. postgres:16postgres:17으로 변경하고 재시작하면 DETAIL 줄에 두 버전이 모두 명시된 FATAL: database files are incompatible with server 오류가 발생합니다. 새로운 바이너리는 이전 카탈로그 레이아웃을 읽을 수 없기 때문입니다. 데이터가 손상되지는 않으므로 이전 태그로 되돌리면 다시 정상적으로 시작됩니다. 실행 중인 이전 컨테이너를 대상으로 새 버전의 클라이언트를 사용하여 pg_dumpall을 수행하고, 파일 끝에 PostgreSQL database cluster dump complete가 있는지 확인한 뒤, 빈 볼륨에서 새 태그를 실행하고 덤프 파일을 로드하십시오. 동일 메이저 버전 내의 마이너 업그레이드는 이미지 pull과 재시작만으로 충분합니다.

데이터베이스 컨테이너가 종료 코드 137로 종료되는 이유는 무엇입니까?

137은 128에 시그널 9가 더해진 값으로, 프로세스가 강제로 종료되었음을 의미합니다. docker inspect <container> | grep -i oomkilled를 실행하십시오. true 값이 출력된다면 컨테이너가 cgroup 메모리 제한에 도달한 것입니다. 일반적인 원인은 PostgreSQL과 MySQL이 호스트의 전체 메모리 용량을 읽어 들여 컨테이너의 제한을 인식하지 못하기 때문입니다. 2GB 제한 내에서 16GB를 사용하는 것처럼 계획을 세우는 것입니다. shared_bufferswork_mem, 또는 innodb_buffer_pool_size를 설정하여 컨테이너에 할당한 제한에 맞추십시오. journalctl -k에서 Memory cgroup out of memory 줄을 확인하여 커널이 어떤 프로세스를 종료 대상으로 선택했는지 확인하십시오.