Docker Compose 스택 백업 및 업그레이드 완벽 가이드
Docker Compose 스택을 안전하게 백업하고 업그레이드하는 방법을 설명합니다. compose 파일, 볼륨 데이터, 데이터베이스 덤프를 포함한 필수 항목과 데이터 손실 없이 안전하게 배포를 수행하는 실무적인 절차를 상세히 안내합니다.
Docker Compose 스택 백업에 포함되어야 할 항목
Docker Compose 스택 백업에는 네 가지 개별 항목이 포함되어야 하며, 이 중 하나라도 유실되면 애플리케이션을 복구할 수 없습니다. 해당 항목은 compose 파일, 그 옆에 위치한 .env, 모든 볼륨의 내용, 그리고 데이터베이스 자체 클라이언트로 생성한 데이터베이스 덤프입니다. 컨테이너가 실행 중인 상태에서 데이터베이스 파일을 복사하는 것은 백업이 아닙니다. 업그레이드 시에도 동일한 목록이 필요하며 한 가지 규칙이 추가됩니다. 스키마 마이그레이션은 앞으로 진행되도록 작성되며 대부분의 프로젝트가 되돌리는 방법을 제공하지 않으므로, 이미지를 pull하기 전에 반드시 백업을 수행하십시오.
아래의 모든 내용은 스택이 이미 배포되었고 docker compose ps 명령어로 실행 중임을 확인했다고 가정합니다. 예제에서는 프로젝트 디렉터리를 /srv/myapp으로, 서비스 이름을 app 및 db로 사용합니다. 본인의 환경에 맞게 이름을 변경하십시오. 명령어는 의도적으로 범용적으로 작성되었습니다. 볼륨과 데이터베이스와 같이 중요한 부분은 애플리케이션의 종류와 관계없이 동일하게 작동하기 때문입니다.
스택이 실제로 저장하는 데이터 파악하기
cd /srv/myapp
docker compose ps
docker compose config --volumes
docker volume ls --filter label=com.docker.compose.project=myappdocker compose config --volumes은 파일에 선언된 네임드 볼륨의 짧은 이름을 출력합니다. docker volume ls는 해당 볼륨이 디스크상에서 실제로 사용하는 이름을 출력합니다. Compose가 프로젝트 이름을 앞에 붙이기 때문에 두 목록은 서로 다릅니다. 예를 들어 파일에 db_data라고 작성된 볼륨은 실제로는 myapp_db_data이라는 이름으로 존재합니다. 프로젝트 이름은 기본적으로 디렉터리 이름을 따르므로, 디렉터리 이름을 바꾸면 스택은 새로 생성된 빈 볼륨 세트를 참조하게 되며 기존 볼륨은 데이터가 담긴 채 그대로 남게 됩니다. 아래의 모든 명령어는 docker volume ls에서 확인한 실제 이름이 필요합니다.
바인드 마운트는 두 목록 어디에도 나타나지 않습니다. Compose 파일에서 바인드 마운트는 콜론 왼쪽에 호스트 경로가 지정된 항목인 ./config:/app/config입니다. 이는 호스트의 일반적인 디렉터리이므로 일반적인 도구로 접근할 수 있습니다. 네임드 볼륨은 /var/lib/docker/volumes/ 아래에 위치하며, docker volume inspect --format '{{.Mountpoint}}' myapp_db_data은 특정 볼륨의 정확한 경로를 출력합니다. 스택이 어떤 방식을 사용하는지에 따라 복사 방법이 달라지며, 바인드 마운트와 네임드 볼륨 비교에서 그 차이점을 상세히 다룹니다.
이제 파악한 내용을 두 그룹으로 분류하십시오. 일부 볼륨은 업로드된 파일, 생성된 키, 데이터베이스 자체, 사용자가 앱에 입력한 내용 등 재현할 수 없는 상태 데이터를 담고 있습니다. 반면 썸네일이나 검색 인덱스처럼 앱이 스스로 다시 생성할 수 있는 파생 데이터도 있습니다. 두 번째 그룹을 백업하는 것은 디스크 공간과 복구 시간만 낭비할 뿐 아무런 이득이 없습니다. Redis 캐시 볼륨이 가장 대표적인 예이며, 이 볼륨을 잃더라도 첫 번째 요청이 조금 느려질 뿐입니다.
compose 파일과 .env 파일 백업하기
두 파일은 호스트의 같은 위치에 나란히 존재하며, 어떤 볼륨에도 포함되지 않습니다. .env에는 데이터베이스 비밀번호, 애플리케이션 시크릿, 각종 API 토큰이 저장되어 있으므로, 이 파일이 있어야만 볼륨 더미를 다시 작동하는 애플리케이션으로 복구할 수 있습니다. 또한 이 파일은 보통 .gitignore에 포함되므로, "내 설정은 git에 있다"는 계획은 정작 가장 중요한 파일을 제외하게 됩니다. 환경 변수 파일에 비밀 정보 보관하기는 올바른 패턴이며, 그만큼 백업 시에도 각별한 주의가 필요합니다.
sudo install -d -m 700 -o "$USER" -g "$(id -gn)" /srv/backups/myapp
cp -a compose.yaml .env /srv/backups/myapp/
chmod 600 /srv/backups/myapp/.env스택에서 사용하는 모든 compose 파일을 복사하십시오. 첫 번째 파일만 복사해서는 안 됩니다. -f compose.yaml -f compose.prod.yaml으로 시작한 스택은 복구 시에도 두 파일이 모두 필요하며, 여러 compose 파일이 병합되는 방식에 따라 컨테이너에 전달되는 최종 값이 결정됩니다.
.env와 볼륨 사이에는 주의할 점이 하나 있습니다. 공식 Postgres 이미지는 데이터 디렉터리가 비어 있을 때 초기화하는 시점에만 POSTGRES_PASSWORD를 읽습니다. 나중에 이 값을 변경해도 데이터베이스 내부의 비밀번호는 바뀌지 않습니다. 지난달의 볼륨을 오늘의 .env과 함께 복구하면, 두 파일 모두 육안으로는 올바르게 보임에도 불구하고 FATAL: password authentication failed for user "appuser" 오류로 인해 애플리케이션이 연결에 실패합니다. .env과 볼륨은 반드시 동일한 시점에 생성된 것끼리 묶어서 백업하십시오.
전용 클라이언트로 데이터베이스 덤프 생성하기
데이터베이스 서버는 지속적으로 파일에 데이터를 기록합니다. 서버가 실행 중인 상태에서 tar로 /var/lib/postgresql/data을 수행하면, 쓰기 작업 전후의 페이지가 섞여 아카이브가 일관되지 않은 시점의 데이터를 포함하게 됩니다. 반면 덤프 도구는 단일 트랜잭션 내에서 데이터를 읽으므로, 파일은 하나의 일관된 시점을 유지합니다. 이 차이가 단순 복사와 백업을 구분 짓는 핵심입니다.
docker compose exec -T db sh -c \
'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
> /srv/backups/myapp/db-$(date +%F).dump-T 옵션을 유지하십시오. 이 옵션은 TTY 할당을 비활성화합니다. TTY가 연결된 상태에서 Docker는 출력 스트림을 셸로 전달하는 과정에서 변환을 수행하는데, 이로 인해 바이너리 덤프가 손상될 수 있습니다. 이 문제는 복원을 시도하기 전까지는 알기 어렵습니다. 작은따옴표 또한 중요합니다. 작은따옴표는 호스트 셸이 $POSTGRES_USER를 확장하지 못하게 막아, 컨테이너 내부의 셸이 compose 파일에 설정된 값을 사용하여 이를 확장하도록 합니다. -Fc은 사용자 지정 형식으로 덤프를 생성하며, 이는 압축 효율이 좋고 추후 pg_restore를 통해 특정 객체만 선택적으로 추출할 수 있게 합니다.
역할(Role)과 비밀번호는 개별 데이터베이스 외부에 존재하므로 함께 백업해야 합니다.
docker compose exec -T db sh -c 'pg_dumpall -U "$POSTGRES_USER" --globals-only' \
> /srv/backups/myapp/globals.sql그다음 파일이 정상적인 덤프인지, 오류 메시지가 포함된 것은 아닌지 확인하십시오.
ls -lh /srv/backups/myapp/
head -c 5 /srv/backups/myapp/db-$(date +%F).dump사용자 지정 형식의 덤프는 5바이트인 PGDMP로 시작합니다. 파일 크기가 0바이트이거나 pg_dump:으로 시작한다면 명령이 실패한 것입니다. 셸은 명령 실행 전에 출력 파일을 먼저 생성하므로, 덤프가 실패해도 그럴듯한 이름과 타임스탬프를 가진 파일이 남습니다. 이는 가장 흔하게 발생하는 조용한 백업 실패 사례입니다.
MariaDB나 MySQL의 경우 클라이언트는 다르지만 방식은 동일합니다.
docker compose exec -T db sh -c \
'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" --single-transaction --databases "$MARIADB_DATABASE"' \
> /srv/backups/myapp/db-$(date +%F).sql--single-transaction은 쓰기 작업을 차단하지 않고 InnoDB 테이블의 일관된 덤프를 생성합니다. MySQL 이미지에서는 명령어가 mysqldump이며 변수는 MYSQL_ROOT_PASSWORD와 MYSQL_DATABASE입니다. 최신 MariaDB 이미지에서도 mysqldump은 mariadb-dump의 호환성 이름으로 여전히 작동합니다. 명령줄에 직접 입력한 비밀번호는 덤프가 실행되는 동안 컨테이너의 프로세스 목록에 노출된다는 점을 유의하십시오.
SQLite는 별도의 주의가 필요합니다. 데이터베이스는 하나의 파일이지만, 최근 트랜잭션은 별도의 -wal 파일에 남아 있을 수 있습니다. 따라서 .db만 복사하면 최신 쓰기 작업이 누락된 데이터베이스가 됩니다. 이미지에 클라이언트가 포함되어 있다면 sqlite3 /data/app.db ".backup '/data/app-backup.db'"를 사용하여 앱 실행 중에도 일관된 복사본을 생성할 수 있습니다. 클라이언트가 없다면 컨테이너를 중지한 후 .db 파일과 그에 딸린 -wal 및 -shm 파일을 함께 복사하십시오.
데이터베이스가 스택 내부가 아닌 호스트에서 실행 중이라면 docker compose exec 접두사 없이 동일한 명령을 사용하면 됩니다. 다음 재구축 전에 Docker에서 데이터베이스 실행하기 또는 호스트에서 실행하기를 읽어보는 것을 권장합니다.
볼륨 캡처
Named volume은 사용자가 직접 수정할 호스트 경로가 없으므로, 임시 컨테이너에 마운트하여 아카이브를 생성해야 합니다.
docker run --rm \
-v myapp_uploads:/data:ro \
-v /srv/backups/myapp:/backup \
alpine:3 tar czf /backup/uploads.tar.gz -C /data .도우미 컨테이너는 볼륨을 /data에 읽기 전용으로, 백업 디렉터리를 /backup에 마운트한 뒤 호스트 측으로 아카이브를 기록합니다. --rm는 tar이 종료되는 즉시 도우미 컨테이너를 제거합니다. :ro는 중요한데, tar 명령을 잘못 입력하더라도 원본을 손상시키지 않기 때문입니다. -C /data .는 복원 시 올바른 위치에 파일이 생성되도록 합니다. 이 옵션은 모든 경로를 볼륨 루트 기준으로 저장합니다. 대신 tar czf /backup/uploads.tar.gz /data을 사용하면 모든 경로 앞에 data/이 붙게 되어, 복원 시 볼륨 내부에 /data/data 디렉터리가 생성되고 애플리케이션은 빈 디렉터리만 보게 됩니다. 컨테이너 내부에서 tar가 root 권한으로 실행되므로 아카이브의 소유권은 root가 됩니다. 이 점이 문제가 된다면 sudo chown "$USER" /srv/backups/myapp/uploads.tar.gz를 실행하십시오. 복원된 파일이 애플리케이션에서 읽히지 않는다면 PUID와 PGID가 파일 소유권을 결정하는 방식을 읽어보시기 바랍니다.
Named volume마다 한 번씩 실행하십시오. Bind mount는 컨테이너가 전혀 필요 없습니다. 호스트에서 tar czf /srv/backups/myapp/config.tar.gz -C /srv/myapp/config .을 실행하면 동일한 작업을 수행할 수 있습니다.
애플리케이션을 중지해야 할지는 볼륨별로 결정하십시오. 애플리케이션이 실시간으로 덮어쓰는 볼륨을 tar로 압축하면 파일 쓰기 도중 데이터가 캡처될 수 있습니다. 파일이 한 번 기록된 후 읽기만 하는 업로드 디렉터리의 경우 위험은 작습니다. 그 외의 경우에는 docker compose stop app을 사용하여 복사하는 동안 서비스를 중지한 다음 docker compose start app를 실행하십시오. stop은 컨테이너와 볼륨을 그대로 유지하며, 이는 이 작업에서 정확히 필요한 동작입니다. 명령어를 입력하기 전에 down과 stop의 차이를 확실히 이해하는 것이 좋습니다.
데이터베이스 볼륨의 tar 파일을 데이터베이스 백업으로 간주하지 마십시오. 덤프 파일이 곧 백업입니다. 중지된 데이터베이스의 볼륨 아카이브는 유용한 빠른 재구축 수단일 뿐입니다.
작업 순서
- compose 파일과
.env를 백업 디렉터리로 복사합니다. - 데이터베이스가 실행 중인 상태에서 덤프를 생성합니다.
- 볼륨이 제자리에서 변경되는 경우 앱 컨테이너를 중지합니다.
- 각 네임드 볼륨과 바인드 마운트 디렉터리를 아카이브합니다.
- 중지했던 서비스를 시작한 뒤
docker compose ps로 확인합니다. - 스택에서 실행 중인 이미지 태그와 다이제스트를 기록합니다.
- 전체 백업 디렉터리를 이 서버 외부로 복사합니다.
7단계는 사람들이 나중으로 미루는 작업입니다.
서버 외부로 백업 복사본 전송하기
스택과 동일한 디스크에 백업을 저장하는 것은 사용자의 실수로부터는 보호해주지만, 그 외의 상황에서는 아무런 도움이 되지 않습니다. 볼륨 장애, 서버 삭제, 계정 손실이 발생하면 원본과 백업본이 동시에 사라집니다. 스케줄에 따라 보존 정책을 적용하여 해당 VPS가 아닌 별도의 저장소로 디렉터리를 전송하십시오. VPS에서 restic 백업하기 문서에서 저장소 설정, 보존 플래그, 검사 명령을 다루고 있으므로 여기서는 반복하지 않습니다.
restic은 파이프를 통해 덤프를 직접 읽을 수 있으므로, 평문 데이터베이스를 디스크에 전혀 남기지 않을 수 있습니다.
docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
| restic backup --stdin --stdin-filename db.dump어떤 도구를 사용하든 systemd timer나 cron job에 스케줄을 등록하고, 작업 실패 시 확인할 수 있는 곳으로 알림을 보내도록 설정하십시오. 출력 결과가 어디로도 전달되지 않는 백업 스크립트는 6개월 동안 작동이 멈춰도 아무도 알 수 없는 상태가 됩니다.
복구 훈련으로 백업이 정상 작동하는지 증명하기
복구해 보지 않은 백업은 가설에 불과합니다. 아래 훈련은 기존 스택 옆에 두 번째 스택을 복구하는 방식입니다. 운영 중인 서비스는 계속 유지되며, 입력하는 명령이 운영 환경에 영향을 주지 않습니다.
핵심 메커니즘은 프로젝트 이름입니다. Compose는 디렉터리 이름에서 프로젝트 이름을 가져와 생성하는 모든 컨테이너와 볼륨에 적용합니다. 백업 파일을 새 디렉터리로 복사하면 복구된 스택은 자동으로 고유한 볼륨을 갖게 됩니다.
sudo install -d -m 700 -o "$USER" -g "$(id -gn)" /srv/myapp-restore
cd /srv/myapp-restore
cp /srv/backups/myapp/compose.yaml /srv/backups/myapp/.env .복사한 compose 파일을 수정하여 호스트 포트가 실행 중인 스택과 충돌하지 않도록 합니다. 8080:8080 대신 18080:8080을 사용하거나, 복사한 .env에서 포트를 설정하는 변수를 변경하십시오. 그런 다음 컨테이너와 빈 볼륨을 생성하되 서비스는 시작하지 않습니다.
docker compose create
docker volume ls --filter label=com.docker.compose.project=myapp-restore두 번째 명령을 실행하면 운영 환경과 동일한 볼륨 이름이 출력되지만 앞에 myapp-restore_가 붙습니다. 볼륨을 채우고 데이터베이스만 단독으로 시작한 뒤 덤프 파일을 로드하십시오.
docker run --rm -v myapp-restore_uploads:/data -v /srv/backups/myapp:/backup \
alpine:3 tar xzf /backup/uploads.tar.gz -C /data
docker compose up -d db
docker compose exec -T db sh -c \
'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists' \
< /srv/backups/myapp/db-2026-08-16.dump--clean --if-exists은 각 객체를 삭제한 후 다시 생성하므로 복구 작업을 반복할 수 있게 합니다. 이 옵션이 없으면 이미 테이블이 존재하는 데이터베이스에 두 번째로 복구를 시도할 때 pg_restore: error: could not execute query: ERROR: relation "users" already exists 오류가 발생하며 중단됩니다.
그다음 나머지 서비스를 시작하고 사용자의 관점에서 확인하십시오.
docker compose up -d --wait
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt"'
docker compose logs --tail=50docker compose up -d --wait는 모든 서비스가 실행 중이거나 정상 상태(healthy)가 될 때까지 대기하며, 하나라도 실패하면 0이 아닌 종료 코드를 반환하므로 스크립트 작성에 적합합니다. 서비스가 정상 상태에 도달하지 못하면 docker compose ps 명령으로 상태를 확인하십시오. Compose healthchecks에서 해당 열이 무엇을 의미하는지 설명합니다. 이후 대체 포트로 애플리케이션을 열고 실제 계정으로 로그인하십시오. 레코드를 하나 작성하고 볼륨에 저장된 파일을 하나 열어보십시오. 이 두 가지가 바로 증거입니다. 덤프가 복구되었고, 볼륨이 복구되었으며, 두 데이터가 서로 일치한다는 것을 의미합니다. 로그인 페이지만 뜨는 것을 확인하는 훈련으로는 데이터의 무결성을 증명할 수 없습니다.
훈련이 끝나면 환경을 정리하십시오.
docker compose down -v이 경우가 -v 플래그를 사용하는 것이 올바른 유일한 상황입니다. 운영 디렉터리에서 이 명령을 실행하면 보호해야 할 볼륨까지 삭제되므로 주의하십시오.
Compose 스택 업그레이드 방법
현재 실행 중인 버전과 목표 버전 사이의 모든 릴리스 노트를 읽고 breaking 및 migration이라는 단어를 검색하십시오. 여러 메인 버전을 건너뛰는 것을 지원하지 않는 프로젝트는 해당 문서에 명시되어 있으며, 실행에 실패한 마이그레이션은 스키마의 일부를 이미 변경한 후에야 오류를 보고합니다.
변경 작업을 시작하기 전에 현재 실행 중인 상태를 기록하십시오.
docker compose images
docker image inspect --format '{{index .RepoDigests 0}}' postgres:16.4docker compose images는 각 서비스가 현재 사용 중인 이미지와 태그를 나열합니다. 태그는 언제든지 다른 곳을 가리키도록 변경될 수 있으므로, 이미지를 정확하게 식별할 수 있는 유일한 값은 다이제스트(digest)입니다.
앞선 섹션에서 생성한 백업을 서버 외부로 복사하십시오. 패치 릴리스를 진행할 때도 동일하게 수행해야 합니다. 준비를 소홀히 하는 순간 업그레이드는 위험해집니다.
그다음 compose 파일에서 버전을 고정하십시오. latest은 버전이 아닙니다.
services:
db:
image: postgres:16.4image: postgres:latest을 사용하면 docker compose pull은 오늘날 해당 태그가 가리키는 모든 것을 가져오므로, 어제 실행했던 버전을 특정할 방법이 없습니다. 태그를 고정하면 업그레이드는 git diff에서 읽을 수 있는 한 줄의 수정 사항이 되며, 다시 한 줄을 수정하여 이전 상태로 되돌릴 수 있습니다. 애플리케이션 이미지도 프로젝트 릴리스 페이지에서 정확한 버전을 확인하여 동일한 방식으로 고정하십시오.
이미지를 내려받고 컨테이너를 재생성하십시오.
docker compose pull
docker compose up -d --waitdocker compose up -d은 파일과 실행 중인 컨테이너를 비교하여 이미지나 설정이 변경된 서비스만 재생성합니다. 명명된 볼륨(named volume)은 건드리지 않으므로 새 컨테이너는 기존 데이터 위에서 시작됩니다. 이것이 이 작업의 핵심이자 위험 요소인데, 새 버전은 보통 첫 시작 시점에 스키마 마이그레이션을 수행하기 때문입니다.
진행 상황을 모니터링하십시오.
docker compose ps
docker compose logs -f --tail=100 app실패한 컨테이너는 docker compose ps의 STATUS 열에 Exited (1)을 표시하며, 실패 원인은 로그의 마지막 줄에 나타납니다. 마이그레이션 오류는 로그에서만 명확하게 드러나며 다른 곳에서는 확인하기 어렵습니다. 로그가 안정되면 시스템에 로그인하여 잠시 애플리케이션을 사용해 보십시오.
docker compose pull가 no space left on device와 함께 중단된다면, 보통 오래된 이미지 레이어가 원인입니다. 사용하지 않는 Docker 이미지 정리를 통해 공간을 확보할 수 있습니다. 정리는 업그레이드가 성공적으로 완료된 후에 수행하십시오. 이전 레이어는 신속한 롤백을 수행할 때 필요하기 때문입니다.
업그레이드 실패 시 롤백 방법
두 가지 경우가 있으며, 각각 소요되는 비용이 크게 다릅니다. 새 버전에서 스키마를 변경하지 않았다면 롤백은 한 줄로 끝납니다. compose 파일에 이전 태그를 다시 넣고 docker compose up -d을 실행하십시오. 컨테이너가 교체되고 볼륨은 그대로 유지되므로, 이전 코드가 자신이 기록했던 데이터를 읽어 들입니다.
새 버전에서 스키마를 마이그레이션했다면 이전 코드는 더 이상 데이터를 읽을 수 없습니다. 마이그레이션은 앞으로 진행되도록 작성되며, 대부분의 프로젝트는 다운그레이드 스크립트를 제공하지 않습니다. 따라서 이전 버전이 시작되더라도 이름이 바뀌거나 삭제된 컬럼에 첫 번째 쿼리를 날리는 순간 ERROR: column "avatar_url" does not exist 형태의 오류와 함께 실패합니다. 되돌아가는 방법은 업그레이드 전에 수행한 덤프뿐입니다. 이전 태그로 되돌리고, 데이터베이스 볼륨을 제거한 뒤, 빈 볼륨을 새로 생성하여 덤프를 복원하고 시작하십시오. 해당 덤프가 없다면 되돌릴 방법은 전혀 없으며, 이것이 바로 풀(pull) 작업 전에 백업을 수행해야 하는 이유입니다.
Postgres 메이저 버전 업그레이드는 이 문제의 가장 치명적인 형태이며, 롤백이 아닌 업그레이드 시점에 실패가 발생하기 때문에 사용자들을 당황하게 합니다. 디스크 내 데이터 형식은 모든 메이저 릴리스마다 변경됩니다. postgres:16.4을 postgres:17.2로 변경하고 docker compose up -d을 실행하면 새 서버는 시작을 거부합니다.
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.2.이미지가 자동으로 pg_upgrade을 수행해주지는 않습니다. Compose 스택 내에서 지원되는 정석적인 경로는 덤프, 교체, 복원입니다. 이전 버전이 실행 중일 때 덤프를 뜨고, docker compose down를 실행한 뒤, 데이터베이스 볼륨을 제거하고, 새 태그를 설정하십시오. 이후 docker compose create을 통해 깨끗한 빈 데이터 디렉터리를 생성하고, 데이터베이스를 시작하여 덤프를 복원한 다음 나머지 서비스를 시작하십시오. 새 메이저 버전이 실제 트래픽을 하루 동안 문제없이 처리할 때까지는 이전 덤프를 보관하십시오. 16.4에서 16.9와 같이 동일한 메이저 버전 내에서의 마이너 업그레이드는 이러한 과정이 필요 없습니다. 해당 버전 간에는 형식이 안정적이므로 컨테이너가 즉시 정상적으로 시작됩니다.
VPS 스냅샷은 백업인가요?
스냅샷은 백업을 보완하는 수단이며, 두 방식은 서로 다른 상황에서 실패합니다. 스냅샷은 하이퍼바이저 수준에서 디스크 전체를 복사하므로, 백업을 잊은 부분을 포함하여 전체 시스템을 몇 분 안에 이전 상태로 되돌릴 수 있습니다. 따라서 스냅샷은 '업데이트 후 서버가 고장 났을 때 20분 전 상태로 되돌리는' 특정 작업에 가장 적합한 도구입니다.
그 외의 용도로는 적합하지 않습니다. 스냅샷은 전체 시스템 단위로만 복구할 수 있으므로, 삭제된 테이블 하나를 복구하려면 서버 전체를 별도로 복원한 뒤 해당 테이블을 직접 추출해야 합니다. 보관 기간도 보통 짧습니다. 또한 스냅샷은 일반적으로 서버와 동일한 제공업체 계정에 저장되므로, 계정 접근 권한을 잃으면 서버와 스냅샷을 모두 잃게 됩니다. 실행 중인 시스템의 스냅샷을 찍으면 데이터베이스가 쓰기 도중에 멈춘 상태가 되며, 이 경우 첫 시작 시 데이터베이스가 크래시 복구를 수행하게 되고 진행 중이던 트랜잭션은 모두 유실됩니다.
두 가지를 모두 사용하십시오. 스냅샷은 업그레이드 작업 시 '실행 취소' 버튼으로 활용하고, 덤프는 계정 삭제와 같은 상황에서도 살아남을 수 있는 복사본으로 활용해야 합니다. 스냅샷과 백업의 차이점에서 각 방식이 어떤 장애 상황을 해결할 수 있는지 자세히 다룹니다. 또한 동일한 백업 디렉터리를 활용하면 스택을 새 VPS로 이전하는 작업을 기억에 의존해 재구축하는 대신 일상적인 업무로 바꿀 수 있습니다.
발생하는 문제와 확인 방법
down 명령 시 volumes 플래그 사용. docker compose down -v는 파일에 선언된 명명된 볼륨을 삭제하며, Compose는 Volume myapp_db_data Removed라는 문구로 이를 확인합니다. 이 작업은 되돌릴 수 없습니다. 단순히 docker compose down만 사용하면 볼륨은 그대로 유지됩니다. 파괴적인 플래그를 명시적으로 입력하도록 docker compose down --volumes과 같은 긴 형식을 사용하십시오.
매직 스트링이 없는 덤프. pg_restore: error: did not find magic string in file header 오류는 해당 파일이 아카이브가 아님을 의미합니다. 일반적인 원인은 docker compose exec 실행 시 -T 옵션이 누락된 경우입니다. TTY가 연결된 상태에서는 스트림이 셸로 전달되는 과정에서 변환되어 바이너리 덤프가 손상되기 때문입니다. -T을 사용하여 덤프를 다시 수행한 다음, head -c 5로 처음 5바이트를 확인하십시오.
변경되지 않는 비밀번호. 복원 후 FATAL: password authentication failed for user "appuser" 오류가 발생한다면 .env와 데이터 디렉터리의 시점이 일치하지 않는 것입니다. 이미지는 빈 데이터 디렉터리를 생성할 때만 해당 비밀번호를 설정하므로, 나중에 .env를 수정해도 데이터베이스 내부에는 아무런 변화가 없습니다. 일치하는 .env을 복원하거나, ALTER USER을 사용하여 데이터베이스 내부에서 비밀번호를 변경하십시오.
두 번째 빈 볼륨. Docker는 필요할 때 볼륨을 생성하므로, s가 누락된 상태에서 docker run -v myapp_upload:/data을 실행하면 완전히 새로운 빈 볼륨에 데이터가 기록되고 성공 메시지가 출력됩니다. 이후 docker volume ls을 실행하면 두 개의 이름이 표시되며, 그중 하나는 비어 있게 됩니다. 볼륨 이름은 기억에 의존해 입력하지 말고 docker volume ls에서 복사하십시오.
운영 환경을 대상으로 한 복원. /srv/myapp-restore 대신 /srv/myapp에서 복원 명령을 실행하면 백업 데이터로 실운영 데이터가 덮어씌워지며, 두 환경의 명령은 동일해 보입니다. 모든 복원 명령을 실행하기 전에 pwd를 확인하고, 연습용 작업은 별도의 디렉터리에서 수행하십시오.
FAQ
docker compose down가 내 데이터를 삭제합니까?
아니요. docker compose down은 컨테이너와 기본 네트워크만 제거하며, 명명된 볼륨(named volumes)과 바인드 마운트(bind mounts)는 그대로 둡니다. docker compose down -v은 파일에 선언된 명명된 볼륨을 제거하며, 이는 영구적인 작업입니다. 바인드 마운트는 호스트 디렉터리이므로 Compose는 이를 절대 제거하지 않습니다. 백업을 위해 다른 모든 것은 그대로 둔 채 서비스만 중단하고 싶다면 대신 docker compose stop을 사용하십시오.
pg_dump를 실행하는 대신 Postgres 데이터 디렉터리를 복사해도 됩니까?
컨테이너가 중지된 상태에서만 가능합니다. 서버가 실행 중일 때 파일을 복사하면 데이터가 변경되는 도중이라 복사본이 일관되지 않은 상태가 될 수 있습니다. 또한 파일 수준의 복사는 특정 Postgres 메이저 버전에 종속되므로, 다른 버전에서는 실행되지 않습니다. 컨테이너를 중지하고 볼륨을 아카이브한 뒤 다시 시작하십시오. 이 결과물은 유일한 백업이 아니라 빠른 재구축 경로로 취급해야 합니다. 덤프 파일이 이식 가능한 복사본이며, 복구 시 사용하는 원본입니다.
Compose에서 Postgres를 새로운 메이저 버전으로 어떻게 업그레이드합니까?
태그를 변경하는 것만으로는 충분하지 않습니다. 새 서버는 이전 데이터 디렉터리에서 시작을 거부하며 The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.2 로그를 출력합니다. 이전 버전이 실행 중인 상태에서 pg_dump을 실행한 다음 docker compose down을 수행하십시오. 그 후 데이터베이스 볼륨을 제거하고 새 태그를 설정한 뒤, docker compose create를 실행하여 빈 볼륨을 생성하고 데이터베이스를 시작하여 덤프를 복구하십시오. 새 버전이 실제 트래픽을 문제없이 처리할 때까지 이전 덤프를 보관하십시오.
백업은 얼마나 자주 실행해야 하며, 얼마나 오래 보관해야 합니까?
작업 내용을 얼마나 다시 수행할 수 있는지에 따라 간격을 결정하십시오. 개인이나 소규모 팀 스택에는 매일 밤 수행하는 백업이 적절하며, 업그레이드 직전에 수동 백업을 하나 더 추가하십시오. 보관 기간은 즉시 발견하지 못한 손상을 복구할 수 있을 만큼 충분한 이력을 유지해야 합니다. 금요일에 발견된 손상된 테이블은 목요일 밤의 복사본으로 해결할 수 없기 때문입니다. restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune은 합리적인 초기 정책입니다. 일정과 상관없이 분기에 한 번은 복구를 시도해 보십시오. 복구 테스트를 완료하기 전까지는 백업이 아니라 단순한 파일일 뿐입니다.
백업을 위해 전체 스택을 중지해야 합니까?
보통은 그렇지 않습니다. 데이터베이스 덤프는 서버가 실행 중일 때도 일관성을 유지하므로 데이터베이스 중단 시간은 필요하지 않습니다. 문제는 볼륨입니다. 업로드 디렉터리와 같이 애플리케이션이 파일만 추가하는 경우라면 실행 중인 상태에서 아카이브해도 충분히 안전합니다. 파일을 제자리에서 덮어쓰는 방식이라면 해당 서비스만 docker compose stop app로 복사 시간 동안 중지했다가 다시 시작하십시오. 데이터베이스를 계속 실행하면서 애플리케이션만 중지하는 것이 일반적으로 가장 짧은 안전한 작업 창을 확보하는 방법입니다.