Skip to main content
Summary

컨테이너 안에 저장한 파일은 안전하지 않다

컨테이너는 이미지에서 만들어진다. 이미지는 읽기 전용이고, 컨테이너가 실행되면 그 위에 컨테이너 전용 쓰기 레이어가 하나 붙는다. 컨테이너 안에서 파일을 만들거나 수정하면 이 쓰기 레이어에 저장된다.

문제는 이 쓰기 레이어가 특정 컨테이너에만 속한다는 점이다. 컨테이너를 중지했다가 다시 시작하면 파일은 남아 있다. 하지만 컨테이너를 삭제하면 쓰기 레이어도 함께 사라진다. Docker 공식 문서도 컨테이너 안에 기본으로 기록된 데이터는 컨테이너가 삭제될 때 함께 사라지므로, 보존해야 하는 데이터는 컨테이너 쓰기 레이어 밖에 저장해야 한다고 설명한다.1

간단히 확인해 보자.

docker run -d --name file-demo alpine sleep 1000

docker exec file-demo sh -c 'echo "important data" > /memo.txt'
docker exec file-demo cat /memo.txt

출력은 다음과 비슷하다.

important data

컨테이너를 중지했다가 다시 시작해도 파일은 남아 있다.

docker stop file-demo
docker start file-demo

docker exec file-demo cat /memo.txt

하지만 컨테이너를 삭제하고 같은 이미지로 새 컨테이너를 만들면 이야기가 달라진다.

docker rm -f file-demo

docker run -d --name file-demo alpine sleep 1000
docker exec file-demo cat /memo.txt

이번에는 /memo.txt가 없다는 오류가 나온다. 새 컨테이너는 같은 alpine 이미지에서 만들어졌지만, 이전 컨테이너의 쓰기 레이어를 물려받지 않는다.

이 원리를 모르면 데이터베이스 컨테이너를 다룰 때 바로 문제가 생긴다. 예를 들어 PostgreSQL이나 MySQL 컨테이너를 실행한 뒤 데이터를 넣었다고 하자. 볼륨 없이 컨테이너 내부 경로에만 데이터가 저장되어 있다면, 컨테이너를 삭제하고 다시 만들 때 데이터도 사라질 수 있다.

정리하면 다음과 같다.

  • 컨테이너 중지: 컨테이너는 남아 있으므로 내부 변경 사항도 남는다.

  • 컨테이너 삭제: 컨테이너 쓰기 레이어가 사라지므로 내부 변경 사항도 사라진다.

  • 새 컨테이너 생성: 같은 이미지로 만들더라도 이전 컨테이너의 데이터는 자동으로 따라오지 않는다.

그래서 컨테이너에서 중요한 데이터는 컨테이너 자체가 아니라 외부 저장 공간에 연결해야 한다.

컨테이너 데이터 관리와 볼륨AI 생성 이미지

Docker의 세 가지 마운트 방식

Docker에서 컨테이너 밖의 저장 공간을 컨테이너 안으로 연결하는 방법을 마운트라고 한다. 자주 쓰는 방식은 세 가지다.

방식저장 위치데이터 보존주 사용 목적
볼륨Docker가 관리하는 영역컨테이너 삭제 후에도 유지데이터베이스, 업로드 파일, 영구 데이터
바인드 마운트호스트의 특정 경로호스트 파일이 유지되는 한 유지로컬 소스 코드 공유, 설정 파일 연결
tmpfs 마운트호스트 메모리컨테이너 중지 후 사라짐임시 파일, 민감한 임시 데이터

볼륨

볼륨은 Docker가 직접 관리하는 저장 공간이다. 사용자는 볼륨 이름만 지정하면 되고, 실제 파일이 호스트의 어느 내부 경로에 저장되는지는 Docker가 관리한다.

볼륨은 데이터베이스처럼 오래 보존해야 하는 데이터에 적합하다. 컨테이너를 삭제하고 새로 만들어도 같은 볼륨을 다시 연결하면 기존 데이터를 계속 사용할 수 있다.

docker volume create mydata
docker volume ls
docker volume inspect mydata

볼륨은 직접 호스트 경로를 만지는 방식보다 안전하고 이식성이 좋다. 특히 팀원마다 프로젝트 경로가 다르거나, 운영 서버에서 컨테이너를 재생성하는 일이 잦다면 볼륨이 기본 선택지다.

바인드 마운트

바인드 마운트는 호스트의 특정 디렉터리나 파일을 컨테이너 안의 경로와 직접 연결한다.

예를 들어 내 컴퓨터의 ./app 디렉터리를 컨테이너의 /app에 연결하면, 컨테이너는 /app 안에서 호스트의 실제 파일을 보게 된다. 호스트에서 파일을 수정하면 컨테이너에서도 수정된 내용이 보인다.

이 방식은 개발 환경에서 특히 유용하다. 이미지를 매번 다시 빌드하지 않아도 로컬 소스 코드 변경 사항을 컨테이너 안의 개발 서버가 바로 읽을 수 있기 때문이다.

다만 운영 환경에서는 주의해야 한다. 바인드 마운트는 호스트의 실제 경로에 의존한다. 서버마다 경로가 다르면 컨테이너 실행 방식도 달라진다. 또한 컨테이너가 해당 경로에 쓸 수 있다면 호스트 파일도 변경할 수 있다.

tmpfs 마운트

tmpfs 마운트는 데이터를 디스크가 아니라 메모리에 저장한다. 컨테이너가 멈추거나 재시작되면 데이터는 사라진다. Docker 문서에서는 tmpfs를 보존할 필요가 없는 임시 데이터나 디스크에 남기고 싶지 않은 민감한 임시 데이터에 사용할 수 있다고 설명한다.2

예시는 다음과 같다.

docker run --rm \
  --mount type=tmpfs,destination=/tmp/cache \
  alpine sh -c 'echo temp > /tmp/cache/a.txt && cat /tmp/cache/a.txt'

tmpfs는 영구 저장소가 아니다. 데이터베이스 파일을 tmpfs에 두면 컨테이너가 사라질 때 데이터도 함께 사라진다. 성능이나 보안상의 이유로 임시 공간이 필요할 때만 사용하자.

데이터베이스 컨테이너에 볼륨 연결하기

이제 PostgreSQL 컨테이너에 Docker 볼륨을 연결해 보자. 목표는 간단하다.

  1. 볼륨을 연결한 PostgreSQL 컨테이너를 실행한다.

  2. 테이블과 데이터를 만든다.

  3. 컨테이너를 삭제한다.

  4. 같은 볼륨으로 새 컨테이너를 만든다.

  5. 데이터가 남아 있는지 확인한다.

먼저 볼륨을 만든다.

docker volume create pgdata

이제 PostgreSQL 컨테이너를 실행한다.

docker run -d --name pg-demo \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=appdb \
  -p 5432:5432 \
  --mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
  postgres:17

여기서 중요한 부분은 --mount 옵션이다.

--mount type=volume,source=pgdata,target=/var/lib/postgresql/data

뜻은 다음과 같다.

  • type=volume: Docker 볼륨을 사용한다.

  • source=pgdata: pgdata라는 볼륨을 연결한다.

  • target=/var/lib/postgresql/data: 컨테이너 안의 PostgreSQL 데이터 디렉터리에 연결한다.

PostgreSQL이 준비될 때까지 로그를 확인한다.

docker logs -f pg-demo

database system is ready to accept connections와 비슷한 문장이 보이면 Ctrl+C로 로그 보기를 빠져나온다. 컨테이너가 종료되는 것은 아니고 로그 보기만 끝난다.

이제 데이터베이스에 접속해 테이블을 만들고 데이터를 넣는다.

docker exec -it pg-demo psql -U postgres -d appdb

psql 프롬프트가 열리면 다음 SQL을 입력한다.

CREATE TABLE notes (
  id serial PRIMARY KEY,
  body text NOT NULL
);

INSERT INTO notes (body) VALUES ('volume keeps this row');

SELECT * FROM notes;

결과에 방금 넣은 행이 보이면 성공이다. 빠져나올 때는 다음을 입력한다.

q

이제 컨테이너를 삭제한다.

docker rm -f pg-demo

컨테이너는 사라졌지만 pgdata 볼륨은 남아 있다.

docker volume ls

같은 볼륨을 다시 연결해 PostgreSQL 컨테이너를 새로 만든다.

docker run -d --name pg-demo \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=appdb \
  -p 5432:5432 \
  --mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
  postgres:17

잠시 기다린 뒤 데이터를 조회한다.

docker exec -it pg-demo psql -U postgres -d appdb \
  -c "SELECT * FROM notes;"

이전에 넣었던 volume keeps this row가 보이면 볼륨이 제대로 동작한 것이다. 컨테이너는 새로 만들었지만 데이터는 컨테이너 밖의 pgdata 볼륨에 남아 있었기 때문이다.

실습을 완전히 정리하려면 다음 명령을 사용한다.

docker rm -f pg-demo
docker volume rm pgdata

단, 실제로 보존해야 하는 데이터가 들어 있는 볼륨은 삭제하면 안 된다. docker volume rm은 컨테이너가 아니라 저장소 자체를 지우는 명령이다.

로컬 소스 코드를 바인드 마운트하기

이번에는 바인드 마운트를 사용해 로컬 소스 코드를 컨테이너 안의 개발 서버에 연결해 보자. 이미지를 다시 빌드하지 않고 파일 수정 결과를 바로 확인하는 것이 목표다.

새 디렉터리를 만들고 이동한다.

mkdir bind-node-demo
cd bind-node-demo

package.json 파일을 만든다.

{
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "express": "^5.1.0"
  }
}

server.js 파일도 만든다.

import express from "express";

const app = express();
const port = 3000;

app.get("/", (req, res) => {
  res.send("Hello from bind mount");
});

app.listen(port, "0.0.0.0", () => {
  console.log(`listening on ${port}`);
});

이제 Node.js 컨테이너를 실행한다.

docker run --rm -it --name bind-node \
  -p 3000:3000 \
  --mount type=bind,source="$(pwd)",target=/app \
  -w /app \
  node:22-alpine \
  sh -c "npm install && npm run dev"

Windows PowerShell에서는 현재 경로 표현을 다음처럼 바꿔 사용할 수 있다.

--mount type=bind,source=${PWD},target=/app

명령의 핵심은 이 부분이다.

--mount type=bind,source="$(pwd)",target=/app

현재 호스트 디렉터리를 컨테이너의 /app에 연결한다. 그리고 -w /app으로 컨테이너의 작업 디렉터리를 /app으로 지정한다. 컨테이너 안에서 실행되는 npm install과 npm run dev는 모두 이 경로를 기준으로 동작한다.

브라우저에서 http://localhost:3000을 열면 다음 문장이 보인다.

Hello from bind mount

이제 로컬 편집기에서 server.js의 응답 문장을 바꿔 보자.

res.send("Changed on my host machine");

저장한 뒤 브라우저를 새로고침하면 변경된 문장이 보인다. 컨테이너 안의 파일을 수정한 것이 아니다. 호스트 파일을 수정했고, 그 파일이 바인드 마운트로 컨테이너 안에 그대로 보였기 때문에 개발 서버가 변경을 감지한 것이다.

이 방식은 개발할 때 편리하다. 하지만 배포용 이미지를 만들 때는 소스 코드를 이미지 안에 복사하는 방식이 더 일반적이다. 운영 서버에서 바인드 마운트에 의존하면 서버의 디렉터리 구조와 권한 설정에 영향을 많이 받기 때문이다.

어떤 저장 방식을 선택해야 할까

기준은 어렵지 않다.

데이터베이스 데이터처럼 컨테이너를 지워도 남아야 하는 데이터는 볼륨을 사용한다.

--mount type=volume,source=pgdata,target=/var/lib/postgresql/data

로컬에서 수정하는 소스 코드나 설정 파일을 컨테이너와 공유해야 한다면 바인드 마운트를 사용한다.

--mount type=bind,source="$(pwd)",target=/app

컨테이너가 실행되는 동안만 필요한 임시 파일은 tmpfs를 사용할 수 있다.

--mount type=tmpfs,destination=/tmp/cache

가장 중요한 원칙은 하나다. 컨테이너는 언제든 삭제하고 다시 만들 수 있어야 한다. 그러려면 컨테이너 자체와 데이터를 분리해야 한다. 애플리케이션 실행 환경은 이미지로 만들고, 오래 보존해야 하는 데이터는 볼륨에 둔다. 개발 중 자주 바뀌는 소스 코드는 바인드 마운트로 연결한다.

이 구분을 익히면 데이터베이스 컨테이너를 지웠다가 데이터를 잃는 실수를 피할 수 있다. 다음 장에서 네트워크를 배우면 웹 애플리케이션 컨테이너와 데이터베이스 컨테이너를 서로 연결하는 흐름까지 자연스럽게 이어진다.

참고

1Docker Storage docs

2Docker tmpfs mounts docs

책 목록

Docker와 Docker Compose 입문: 컨테이너 개발 환경 구축부터 운영 준비까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기