Portainer에서 Unused 이미지가 지워지지 않을 때: Docker 태그와 컨테이너 참조 확인
Portainer의 Images 화면에는 Unused라고 적혀 있는데 삭제를 누르면 image is referenced in multiple repositories라는 오류가 난다. 둘은 모순이 아니다. 첫 표시는 선택한 환경의 컨테이너가 그 이미지를 사용하고 있는지, 오류는 삭제하려는 이미지에 이름이 여럿 붙어 있는지에 관한 단서다. 없애고 싶은 게 예전 이름 하나라면 먼저 그 이름과 남길 이름이 같은 이미지를 가리키는지 확인하자. 이미지 ID를 강제로 삭제하는 것보다 범위가 좁다.
아래는 Portainer로 관리하는 Docker Standalone 환경의 로컬 이미지 기준이다. 레지스트리에서 원격 이미지를 삭제하거나 Kubernetes 노드의 캐시를 비우는 절차가 아니다. mlog-lab/moon:retired와 mlog-lab/moon:keep는 각각 지울 이름과 남길 이름을 설명하기 위한 예시다.
1. 같은 Docker 엔진에서 이름표를 센다
Portainer에서 해당 환경을 열고 Images의 이미지 ID와 태그를 확인한다. 터미널에서는 같은 Docker 엔진을 보고 있어야 한다. 로컬 PC에서 연 CLI와 Portainer Agent가 연결된 서버가 다르면 같은 이미지 이름도 서로 다른 결과를 낸다. 여러 서버를 관리한다면 Portainer와 Docker 소켓이 어느 서버에 연결되는지부터 확인한다.
docker context show
docker info --format 'engine={{.Name}}'
docker image ls --no-trunc
docker image inspect --format 'id={{.Id}} tags={{json .RepoTags}}' mlog-lab/moon:retired
docker image inspect --format 'id={{.Id}} tags={{json .RepoTags}}' mlog-lab/moon:keep
docker context show의 이름만 믿지 말고 Portainer의 대상 환경과 실제 엔진을 대조한다. 별도 --host나 --context로 연결한다면 이후 명령도 그 연결로 실행한다. 두 inspect 호출에 같은 전체 ID가 나오고 두 이름이 RepoTags에 있다면, 이름이 둘인 것이지 같은 크기의 이미지 두 벌이 있다는 뜻은 아니다. 반대로 남기려던 이름이 조회되지 않거나 ID가 다르면 여기서 멈춘다. 비슷한 태그나 생성 날짜가 복구용 사본이라는 보장은 없다.
Docker의 이미지 삭제 문서는 여러 이름이 붙은 이미지에서 저장소:태그를 지정하면 해당 이름을 해제한다고 설명한다. 같은 ID를 직접 삭제하면 여러 이름 참조 때문에 거부될 수 있다. 메시지에 must be forced가 보이더라도, 그 문구가 남겨야 할 태그까지 지우라는 판단을 대신해 주지는 않는다.
2. 중지된 컨테이너와 다음 배포를 각각 확인한다
실행 중인 컨테이너만 보고 결정하면 두 가지를 놓친다. 중지해 둔 컨테이너는 여전히 이미지를 참조할 수 있고, 다음 배포에서 사용할 태그는 현재 컨테이너가 하나도 참조하지 않을 수 있다.
docker ps -a --filter ancestor=mlog-lab/moon:retired --format '{{.ID}} {{.Names}} {{.Status}}'
-a는 중지된 컨테이너도 포함한다. ancestor 필터는 해당 이미지에서 파생된 이미지를 쓰는 컨테이너까지 찾는다. 결과는 삭제 목록이 아니라 더 확인할 후보다. 행이 나오면 아래 CONTAINER_ID에 그 행의 ID를 넣는다.
docker container inspect --format 'requested={{.Config.Image}} image={{.Image}} state={{.State.Status}}' CONTAINER_ID
requested는 컨테이너를 만들 때 지정한 이미지 이름이나 참조, image는 컨테이너에 기록된 이미지 ID다. 태그는 나중에 다른 이미지를 가리킬 수 있으므로 이름 문자열만으로 같은 내용이라고 결론짓지 않는다. 직접 참조하는 컨테이너가 있거나 결과를 해석할 수 없다면 이 단계에서 삭제를 보류한다. 컨테이너를 지우는 것은 별도 결정이다.
목록이 비어 있어도 한 가지가 남는다. 다음 재배포가 어떤 이름을 요청하는가? Compose라면 실제 배포와 같은 파일·환경·프로필을 지정해 프로젝트 디렉터리에서 아래 명령을 실행한다. Portainer Stack이나 CI가 별도 정의를 쓴다면 그 정의와 롤백 절차도 확인한다.
docker compose config --images
예를 들어 다음 배포가 moon:retired를 요청한다면, 같은 내용을 moon:keep로 남긴 것만으로 retired라는 이름을 찾을 수 있는 것은 아니다. 레지스트리에서 같은 내용을 다시 받을 수 있는지, 로컬 빌드라면 다시 만들 수 있는지 확인한다. 버전처럼 보이는 태그도 이동할 수 있다. 같은 빌드로 복구해야 한다면 기록한 다이제스트와 접근 권한까지 확인하고, 입증할 수 없는 복구용 이미지는 남겨 둔다.
3. 예전 이름 하나만 지우려면 태그 하나를 지정한다
다음 조건을 모두 확인한 경우의 예다. 같은 엔진에 두 이름이 있고 같은 ID를 가리킨다. keep는 계속 보존한다. retired를 기다리는 컨테이너·배포·롤백은 없다. 이때는 ID 대신 정확한 저장소와 태그를 사용한다.
# 조건을 확인한 테스트용 이름에만 적용
docker image rm mlog-lab/moon:retired
# 남기기로 한 이름의 ID를 변경 전 기록과 대조
docker image inspect --format '{{.Id}}' mlog-lab/moon:keep
# 방금 해제한 정확한 이름이 더 이상 조회되지 않는지 확인
docker image inspect mlog-lab/moon:retired
마지막 조회에서 그 이름을 찾을 수 없고 keep의 ID는 이전과 같아야 의도한 결과다. 지우려는 것이 마지막 태그라면 이 예와 다르다. 다른 참조나 충돌이 없으면 로컬 이미지 자체가 삭제될 수 있다. 다시 받을 수 없는 이미지라면 남기거나, 여유 있는 별도 저장 위치에 docker image save로 보관하고 테스트 환경에서 docker image load까지 확인한다. 이미지 보관 파일에는 서비스의 DB 볼륨이나 바인드 마운트 데이터가 들어있지 않다. 상태가 있는 서비스는 Ghost·MySQL 백업에서 실제 복원 가능성을 확인하는 방법처럼 데이터를 따로 검증한다.
4. 태그 하나 해제, 전체 정리, 용량 회수는 결과가 다르다
- 예전 이름 하나만 없애기: 보존할 다른 이름과 전체 ID를 확인한 뒤 해당
저장소:태그만 지정한다. - Portainer에서 dangling 이미지만 정리하기: Images → Prune의 기본 범위는
docker image prune에 해당한다. 특정 한 행을 지우는 동작이 아니다. - 모든 미사용 이미지 정리하기: Delete all unused images는 태그가 있어도 어떤 컨테이너도 사용하지 않는 이미지까지 포함한다(
docker image prune -a에 해당). 롤백용 이름도 대상이 될 수 있다. - 실제로 공간이 필요한지 판단하기: 정리 전후 같은 엔진에서
docker system df -v의 공유·고유 크기를 비교한다. 이름 개수나 화면의 크기를 더해 회수량을 예측하지 않는다.
화면의 범위는 Portainer의 Prune 공식 안내를 기준으로 했다. ID를 주고 강제 삭제하는 docker image rm -f도 한 이름을 고르는 것과 다르다. 어느 경우든 로컬 태그 해제가 원격 레지스트리의 삭제 요청이 되는 것은 아니다.
태그가 하나 줄었는데 디스크 여유 공간이 그대로여도 이상하지 않다. 다른 이름이 똑같은 내용을 사용하거나 다른 이미지가 레이어를 공유할 수 있다. 디스크 부족이 처음 문제였다면 Ubuntu 서버에서 Docker·로그 중 어느 쪽이 큰지 확인하는 순서로 돌아가자. Docker의 디스크 사용량 문서가 설명하는 SHARED SIZE와 UNIQUE SIZE는 이때 읽을 항목이지, 태그마다 돌려받을 용량을 표시한 열이 아니다.
직접 확인한 범위
2026-11-01, 운영 서버와 분리한 로컬 디렉터리에 텍스트 파일 한 개를 담은 OCI 이미지를 만들었다. regctl 0.11.7로 keep와 retired 두 태그를 붙이고 retired만 해제했다. 해제 전에는 두 이름이 같은 매니페스트 다이제스트를 반환했다. 해제 후에는 keep만 조회됐고, 그 매니페스트와 파일 레이어의 SHA-256 값은 전후가 같았다.
이 실험은 이름 하나가 사라져도 다른 이름이 가리키는 내용이 남는다는 점을 확인한다. 여기서 비교한 OCI 매니페스트 다이제스트는 위 명령의 Docker 로컬 이미지 ID와 구분해야 한다. Portainer 버튼, Docker Engine의 삭제 응답, 운영 서버의 회수 용량을 측정한 실험은 아니다. 운영에서의 판단은 보존할 이름 → 현재 컨테이너 → 미래 배포 → 정리 범위 순서로 확인하는 데서 시작한다.