Huny's Dev Blog
huny.dev

Docker Compose secrets: 등록한 키가 앱에 전달되지 않을 때 확인할 3단계

비밀 원본·서비스별 부여·앱의 파일 읽기를 분리해 Compose secrets를 점검한다. _FILE 관례와 파일 권한, 빌드·런타임 경계, 키 교체 절차를 정리하고 Python 파일 함수 6개 검사를 확인한다.

Hun Jang
Hun Jang Nov 8, 2026

필요한 서비스에만 비밀 파일을 전달하는 구조를 표현한 생성형 개념 이미지. 실제 Docker 화면은 아니다.

설정에 비밀값을 등록했는데 애플리케이션은 여전히 “키가 없다”고 한다. 이때 값을 다시 복사하기 전에 세 가지를 나눠 확인하는 편이 낫다. 비밀의 원본이 준비됐는지, 실행할 서비스에 전달됐는지, 앱이 전달 방식에 맞게 읽는지다.

Docker Compose의 secrets는 이 차이를 보여주는 좋은 예다. 선언은 되어 있어도 서비스에 부여하지 않으면 파일이 없고, 파일이 있어도 앱이 환경변수만 읽으면 사용할 수 없다.

이 글은 로컬 Docker Compose에서 Linux 컨테이너를 실행하는 경우를 다룬다. Swarm의 비밀 관리나 클라우드 보관함과 동일한 기능으로 가정하지 않는다. Docker 명령은 공식 문서에 근거한 독자 실행용 예제이며, 이 글 작성 환경에서는 실행하지 않았다.

1. 원본 선언과 서비스 접근 부여는 별개다

최상위 secrets는 무엇을 전달할지 정의한다. 각 서비스의 secrets는 그중 무엇을 사용할지 정한다. 짧은 문법으로 부여한 비밀은 컨테이너 안의 /run/secrets/<이름>에 읽기 전용 파일로 제공된다. 선언만으로 모든 서비스에 배포되는 것은 아니다. Docker Compose secrets 안내

다음은 비밀이 필요한 app과 필요 없는 observer를 비교하는 compose.yaml이다. 외부 서비스와 통신하지 않는 더미 실습이며 포트를 공개하지 않는다.

빈 실습 폴더에서 아래처럼 합성값을 만든다. 실제 운영 키를 예제에 넣지 않는다. 첫 실행에는 이미지 다운로드가 필요할 수 있다.

config --quiet는 구성 검증 단계다. 앱이 실제로 비밀을 읽거나 인증에 성공했다는 뜻은 아니다. 이미지 태그는 예제의 가독성을 위한 것이므로 재현성이 필요한 배포에서는 검토한 이미지 digest와 Compose 버전도 기록한다.

원본을 environment: HOST_TOKEN으로 지정하는 방법도 있다. 이는 Compose를 실행하는 환경의 HOST_TOKEN을 비밀의 원본으로 읽는다는 의미다. 컨테이너 안에 HOST_TOKEN이라는 환경변수를 자동 생성한다는 뜻이 아니다. 이 원본 방식은 Docker Compose용이며 docker stack deploy에서는 지원하지 않는다. 최상위 secrets 문서

2. _FILE은 앱이 구현해야 하는 약속이다

위 예제의 APP_TOKEN_FILE에는 비밀값이 아니라 경로가 들어 있다. Compose가 파일을 읽어 APP_TOKEN으로 바꿔주지는 않는다. _FILE은 MySQL 등 일부 이미지가 지원하는 관례이므로, 사용하는 이미지의 문서나 시작 스크립트를 확인해야 한다. Docker의 _FILE 설명

직접 만든 Python 앱이라면 파일을 읽는 처리를 명시적으로 구현할 수 있다.

앱의 시작 지점에서는 다음처럼 연결한다. 이 Python 코드는 앞의 Alpine 실습 컨테이너에 자동으로 설치되거나 실행되는 코드가 아니다.

여기서는 원본 바이트를 그대로 보존한다. 개행 제거와 문자 디코딩은 해당 자격증명의 규격에 맞게 별도로 결정해야 한다. 모든 비밀에 무조건 strip을 적용하면 의미 있는 공백까지 바꿀 수 있다. 파일을 읽었다고 키의 유효성이나 서비스 접근 권한까지 확인된 것도 아니다.

이 함수는 Python 3.12.14에서 합성값과 임시 파일만으로 여섯 가지를 검사했다. 경로 누락, 파일 없음, 빈 파일, 디렉터리 입력을 거부했고, 개행을 포함한 바이트를 보존했으며 표준 출력에 값을 쓰지 않았다. 이는 파일 읽기 함수의 검사이며 Compose 마운트나 운영 인증을 검증한 결과는 아니다.

3. 값을 출력하지 말고 전달 경계를 검사한다

앞의 두 서비스가 실행 중이라면 다음 명령으로 내용을 노출하지 않고 확인할 수 있다.

성공 문구가 없으면 명령의 종료 상태와 오류를 함께 확인한다. 파일이 없거나 읽을 수 없다는 실패를 정상 출력으로 덮지 않는다. 이 예제의 기본 root 실행에서 읽힌다는 사실만으로 실제 비루트 앱에서도 읽힌다고 판단해서는 안 된다.

특히 파일 기반 secret에 uid, gid, mode를 적어놓고 권한이 강제됐다고 믿으면 안 된다. 확인한 최신 공식 문서는 이 속성들이 environment 원본에서는 지원되지만 file 원본에서는 bind mount 구현 때문에 무시된다고 설명한다. 호스트 파일 권한과 앱의 실제 UID를 함께 확인하고, 설치된 Compose 버전에서 동작을 검증한다. 서비스 secrets 권한 문서

파일 전달은 암호화 보관함을 대신하지 않는다

로컬 파일 기반 secrets를 사용해도 호스트 원본 파일은 남는다. 컨테이너 환경에 비밀값을 직접 넣는 일을 줄일 수 있지만, 파일 자체를 안전하게 보관하는 책임이 사라지지는 않는다.

  • secrets 디렉터리를 Git 추적 대상에서 제외한다. 이미 커밋했다면 ignore 추가만으로 과거 노출이 해결되지 않는다.
  • 빌드 컨텍스트 안에 둔다면 .dockerignore에서도 제외한다.
  • 앱 로그, 오류 보고, 디버그 출력에 값을 남기지 않는다.
  • 앱이 _FILE을 지원하더라도 내부적으로 값을 환경변수로 옮기는지는 구현에 따라 다르다.
  • 읽을 권한을 가진 앱이나 호스트 관리자에게 비밀이 절대 노출되지 않는 구조라고 설명하지 않는다.

서비스별 부여도 신뢰하지 않는 Compose 파일을 안전하게 실행해주는 장치는 아니다. Compose는 입력 파일의 권한·볼륨 요청을 신뢰해 적용하므로, 다른 호스트 경로를 마운트하거나 권한을 확대하는 설정도 검토해야 한다. Compose 신뢰 모델

빌드와 실행, 교체와 적용을 구분한다

이미지를 빌드할 때만 필요한 저장소 토큰은 별도 문제다. services.app.build.secrets와 Dockerfile의 RUN --mount=type=secret을 연결하는 빌드용 전달은 services.app.secrets로 지정하는 런타임 전달과 다르다. 빌드 단계에 제공했다고 실행 중인 앱도 받는 것은 아니다. 또한 빌드 명령이 비밀을 일반 파일에 복사하거나 로그로 출력하면 마운트를 사용해도 유출될 수 있다. Build secrets · Compose build.secrets

비밀을 교체할 때도 파일 저장만으로 끝내지 않는다. 앱이 시작할 때 한 번만 읽는지, 다시 읽는지, 기존 연결에 인증 상태가 남아 있는지를 확인한다. 변경 반영을 명확히 하려면 영향을 받는 서비스를 계획된 시점에 재생성하고 새 자격증명으로 동작하는지 검증할 수 있다.

이 명령은 app 컨테이너를 중단하고 다시 만들므로 서비스 영향이 있다. restart는 바뀐 Compose 설정을 적용하는 명령이 아니다. 운영 키 회전에서는 제공자 측 키 생성·폐기, 새 연결 검증, 실패 시 복구 순서를 따로 준비한다. DB 초기화용 비밀번호 파일을 바꾸는 일도 이미 생성된 DB 계정의 비밀번호 변경과는 구분해야 한다. compose up · compose restart

실습이 끝나면 해당 실습 폴더에서 docker compose down으로 두 컨테이너를 정리한다. 이 명령은 호스트의 비밀 원본 파일까지 삭제하지 않는다.

문제를 좁힐 때는 원본 → 서비스 부여 → 실제 파일 → 앱의 읽기 → 인증 결과 순서로 확인하면 된다. 키가 안 보인다는 이유로 환경 전체를 출력하거나 비밀값을 여러 위치에 복사하는 것보다, 어느 경계에서 전달이 끊겼는지 먼저 찾는 편이 안전하다.

확인 기준은 2026년 11월 8일 한국시간의 공식 문서다. 로컬 파일 함수만 실행 검사했고 Docker Compose, 비루트 권한, 실제 키 회전과 외부 API 인증은 실행 검증하지 않았다.

추천 글