Firefish는 손쉬운 설치 스크립트도 지원하지만 그렇게 설치하면 관리하기 힘들어지므로 Docker, 구체적으로는 Docker Compose를 사용해서 설치하겠습니다.
먼저 Docker와 Docker Compose가 무엇인지 알아봐야겠죠. 이번 글에서는 Docker에 대해 이해하고 설치해보겠습니다.
나중에 알았는데, 설치 스크립트에서도 docker를 사용하는 옵션을 설정할 수 있다네요. 하지만 그렇게 설치하면 뭐가 뭔지 파악할 수 있는 소중한 기회를 놓치게 되니까 이 기회에 알아보자구요.
Docker: 프로그램에 운영체제와 의존성까지 한번에
요즘에는 프로그램을 직접 설치하는 대신 docker라는 것을 많이 사용합니다.
docker는
- 프로그램과 그에 필요한 운영체제를 포함한 모든 것을 하나로 묶는 기술
과 - 이렇게 묶어놓은 프로그램을 간편하게 실행하는 방법/기술
을 말합니다. 몹시 편리합니다. 그런데 개념이 좀 어렵나요? 조금 더 자세히 설명해볼게요.
프로그램이 돌아가려면 다른 프로그램이 필요하다
어떤 프로그램이 하나 실행되려면, 그 프로그램이 필요로 하는 다른 프로그램과, 운영체제에서 제공하는 기능 등이 필요합니다.
기억하시나요? 프로그램 하나를 설치하면 다른 뭔가도 같이 설치하라고 창이 뜨는 경우들이 종종 있죠. 다 필요해서 설치하는 것입니다. 이번에 설치하려고 시도하고 있는 Firefish도 여러 다른 프로그램과 운영체제의 기능 등을 필요로 하죠.
옛날의 해결방식: 가상의 컴퓨터를 만들자 ("가상화")
이렇게 컴퓨터에 프로그램을 이것저것 설치하면 관리가 안 된다라는 문제가 발생합니다. A 프로그램은 B 프로그램의 1.2 버전을 요구하고, C 프로그램은 B 프로그램의 1.4 버전을 요구한다던가... 이렇게 시간이 흐르면 대체 뭐가 필요하더라? 라는 사태까지 발전하게 됩니다. :머리아파:
(TMI: 이것은 지극히 현실적인 문제입니다. 프로그램을 열심히 개발해놨는데, 고객 환경에서 다른 필요한 프로그램이 버전이 안 맞아서, 혹은 필요한 프로그램이 없어서 내가 개발한 프로그램이 안 돌아간다고 생각해보세요. (으악!))
그래서 "docker"라는 기술이 나오기 전에는 개발자들은 가상의 컴퓨터를 만드는 방법으로 해결했어요.
- 프로그램 A를 위한 가상의 컴퓨터를 생성합니다.
- 가상의 컴퓨터에 운영체제(특정 버전)을 설치합니다.
- 운영체제가 설치되면, 거기에 이것저것 프로그램 A가 필요로 하는 다른 프로그램을 설치해요.
- 그리고 마지막으로 프로그램 A를 설치하면, 완벽하쥬?
이제 이 "가상의 컴퓨터"를 파일로 저장해놓고, 나중에 프로그램 A를 실행하고 싶으면 저장해놨던 파일에서 또다른 가상의 컴퓨터를 만들어서 거기에서 프로그램 A를 실행하면 됩니다. 컴퓨터를 통으로 파일로 저장해놨으니, 필요한 프로그램들을 또 설치하거나 따로 관리할 필요가 없겠죠?
컴퓨터를 통으로 파일에다가 저장해놨다가, 필요할 때 저장해놨던 파일에서 새 컴퓨터를 만들어서 실행하는 이런 방법을 "가상화"라고 부릅니다. (TMI: 가상화 기술 중에서도"전(full)가상화"라고 불러요.)
Linux의 격리 기능을 사용해서 가상화와 비슷한 효과를 내자
아마 눈치채셨겠지만, 컴퓨터를 통으로 가상화하는 기술은 효율이 매우 떨어집니다. 느리다는 거죠.
여기에서 똑똑한 Linux 개발자분들이 혜성같이 등장하게 됩니다. "굳이 가상으로 컴퓨터를 만들 필요 없이, 필요한 프로그램들만 다 깔려있으면 되는 거 아냐? 격리만 할 수 있으면 되잖아"
그렇습니다. 우리의 목적은 "필요한 프로그램들이 모두 설치되어 있는 환경"을 만드는 것입니다. 이를 위해 "컴퓨터를 통채로 가상화한다" 라는 수단을 사용했구요. 그렇다면, 컴퓨터를 통채로 가상화하는 대신, 격리된 환경을 만들 수 있다면 그만 아닐까요? 거기에 필요한 프로그램을 모두 설치해놓으면 되니까요.
Linux(리눅스) 운영체제의 코어 부분(커널)의 기능들을 활용하면, 컴퓨터를 통으로 가상화하는 것보다 더 가볍고 빠르게 동작하는 격리된 환경을 만들 수 있어요. 이 기능들을 활용하면
- (1) 격리된 환경을 만들고,
- (2) 만들어진 환경에 필요한 프로그램을 모두 설치해놓은 뒤
- (3) 이 격리된 환경을파일로 저장해놨다가
- (4) 필요할 때 저장해놨던 환경을 불러와서 다시 실행할 수 있는
Docker라는 기술을 만들 수 있어요. Docker가 바로 이런거에요. "필요한 프로그램을 모두 미리 설치해놓는다"는 목적은 달성했지만, 컴퓨터 전체를 가상으로 만드는 대신 Linux 운영체제에서 제공하는 격리 기능을 사용한 것이죠.
대체 Linux에 무슨 기능이 있길래 가상의 환경을 만들어서 저장했다가 불러온다던가 하는 짓을 할 수 있는 걸까요? 우리에게는 중요하지 않지만, 이해에는 도움이 될 테니 일단 몇 가지만 알아봅시다. (사실 저도 정확히는 몰라요!)
- chroot: 특정 폴더를 정해서 이 폴더가 내 컴퓨터 전체인 것처럼 보이게 만듭니다. 특정 폴더 안에 갇힌 거 같은 효과를 낼 수 있어요. 어차피 그거밖에 안 보이니까요.
- cgroups: CPU나 메모리 같은 것의 사용량을 제한할 수 있어요.
- namespace: 이 컴퓨터에서 실행되고 있는 프로그램 중, 일부 프로그램만 보이게 만들 수 있어요.
- overlayfs: 파일들이 바뀌면, 바뀐 내용만 단계별로 저장해놨다가, 겹겹히 쌓아서 최종본만 보여줄 수 있는 파일 관리 체계에요.
위 기능들을 사용하면, 분명 컴퓨터를 통으로 가상화하지는 않았는데, 별도의 격리된 환경을 만들 수 있게 되죠. 안에 있는 프로그램은 자신이 격리되었는지조차 모를거에요!
(TMI: Windows나 macOS 에서도 Docker를 쓸 수 있지만, 위에서 설명한 것처럼 Docker는 리눅스 운영체제의 기능을 사용해요. 그래서 Windows나 macOS에서는 기존처럼 가상의 컴퓨터를 만든 뒤, 그 안에서 Docker를 실행해요. 느리겠죠? 맞아요!)
격리된 환경을 만들고, 저장하고, 불러오기
(Dockerfile → 이미지 → 컨테이너)
격리된 환경을 만들어서 프로그램을 미리 설치해놓고 실행하는 기술을 "Docker"(도커) 라고 부른다는 것을 알았으니, 이제 관련된 중요한 개념을 이해하고 넘어갑시다.
딱 세 가지입니다.
- Dockerfile: 격리된 환경을 만드는 레시피 (명령어 목록)
- (도커) 이미지: 격리된 환경을 파일로 저장해둔 것
- (도커) 컨테이너: 저장된 파일에서 생성한 격리된 환경
"Dockerfile"에 적혀있는 대로 격리된 환경을 만들어서
"이미지"로 저장하자
원래 문제가 되었던 건 환경이 꼬이는 문제였죠. 내 프로그램에 필요한 다른 프로그램이 무엇인지 알 수 없게 된다던가, 버전을 알 수 없게 된다던가... 운영체제의 기능 중 필요한 게 뭐였는지도 모르겠고...
그래서, 아예
- 격리된 환경을 만들고 프로그램을 세팅하는 등의 명령어들을 텍스트 파일에 기록해놓고,
- 그 파일에 적혀있는 대로 순서대로 실행해서 자동으로 격리된 환경을 만들기로 합니다.
파일에다가 뭘 설치하는지를 적어두면 다시 똑같은 걸 만들기도 용이하겠죠?
이렇게, 격리된 환경을 만들고 구성하는 레시피가 들어있는 파일이 Dockerfile(도커파일)입니다. "docker build" 명령어를 실행하면 (1) 격리된 환경을 만들고 세팅한 뒤 (2) 세팅된 환경을 파일에 저장합니다. 파일로 저장된 격리 환경을 "이미지"(Image) 라고 부릅니다. (TMI: 이설명은 세세한 부분이 실제와는 쪼끔 다른데, 지금 저희한테는 중요하지 않죠. 궁금하시면 나중에 따로 찾아보세요.)
우리는 Firefish 프로젝트가 제공하는 도커 이미지를 사용할 거지만, 이해를 돕기 위해 Dockerfile이 어떻게 생겼나 몇 줄만 살펴봅시다. 보통 소스코드의 최상위 경로에 Dockerfile이라는 파일이 있고, firefish도 예외는 아니에요. (참고: 클릭해서 원본 파일 보기)
Firefish의 Dockerfile은 (현재 시점에서는) 두 부분으로 나뉘어져있는데요,
- (1) 프로그램을 빌드(소스코드를 보고 컴퓨터가 실행할 수 있는 형태의 프로그램을 만들어냄)하기 위한 격리된 환경을 하나 만들어서 임시로 사용한 뒤,
- (2) 완성된 프로그램을 가져다가 프로그램 실행용으로 두 번째 환경을 만들어서 이걸 이미지로 저장해요.
저희는 두 번째거만 살짝 볼 거에요. 직접 만들지는 않을거니까요.
FROM alpine:3.18 # 어떤 환경에서 계속 이어서 만들까요? WORKDIR /firefish # 현재 경로 지정
"FROM" 에서 시작지점으로 삼을, 기존에 만들어놨던 격리된 환경을 선택해요. 리눅스 운영체제의 변종 중 "alpine" 의 3.18 버전을 사용하네요. alpine 리눅스는 용량이 5메가밖에 안 해서 격리된 환경을 만드는 데 자주 쓰여요.
"alpine" 이라는 이름의 환경은 누가 만들었고, 어디에 저장되어 있냐구요? 인터넷에 있습니다. 여기에 있어요. (살펴보기: https://hub.docker.com/_/alpine ) 주소가 생략되면 앞에 docker.io 가 있다고 생각합니다. 이런 게 이런 기술을 처음 만든 사람들의 특권이죠. 만약, docker의 공식 저장소 (docker hub) 가 아닌 다른 사이트에 저장된 환경(이미지)를 사용한다면, 앞에 주소가 붙으면 됩니다. Github에 저장되어 있는 ghcr.io/유저이름/이미지이름 처럼요.
"WORKDIR" 로 현재 경로를 지정해요.
# Install runtime dependencies RUN apk add --no-cache --no-progress tini ffmpeg vips-dev zip unzip nodejs-current
RUN 으로 만들어진 환경 내에서 명령을 실행할 수 있습니다. apk add는 alpine 리눅스의 프로그램을 설치하는 명령어에요. 이전에 Ubuntu용 명령어인 apt install 도 봤었죠? 굉장히 많은 것들을 설치하고 있습니다.
중간 부분은 생략하고 끝 부분을 볼까요.
ENV NODE_ENV=production # 실제 서비스용 환경으로 설정 VOLUME "/firefish/files" # 격리된 환경 바깥의 경로를 이 경로로 내부에서 사용
필요한 환경 설정을 좀 해 준 뒤,
(volume 부분은 나중에 실행할 때 지정할 수 있어요)
ENTRYPOINT [ "/sbin/tini", "--" ] # tini 라는 프로그램을 실행하는데, CMD [ "pnpm", "run", "migrateandstart" ] # 추가로 pnpm 이라는 프로그램도 실행할 거에요.
격리된 환경이 만들어지면 바로 실행할 명령어들이 적혀있습니다. 실행되는 명령어는 "/sbin/tini -- pnpm run migrateandstart" 라고 하네요. (사실 저도 ENTRYPOINT랑 CMD가 헷갈려요. 비슷한 거라고 생각해주세요.)
"컨테이너": 저장된 파일에서 만든 격리된 환경
그냥 격리된 환경을 만들 뿐인데 거기에 들어있는 프로그램을 실행까지 하겠다구요? 무슨 소리죠? 굳이 그럴 필요가 있나요?
우리의 원래 목적을 떠올려봅시다. 프로그램 A를 실행하고 싶은데, 프로그램 A가 필요한 환경을 고정해놓기 힘들다는 거였잖아요. 맞아요. 원래 목적은 프로그램을 실행하는 것이었습니다.
그래서, "docker run 저장된_이미지" 명령어를 실행하면
- (1) 저장해놨던 이미지에서 실제로 격리된 환경을 만듭니다.
- (2) 위에서 새로 만든 환경에서, 미리 적어둔 프로그램 A를 바로 실행할거에요.
격리된 환경을 만들어서 거기에서 지정해둔 프로그램을 실행할 것입니다.
저장해놨던 "이미지"(image) 에서 docker run 명령어로 만들어낸 격리된 환경을 "컨테이너"(container) 라고 부릅니다. 컨테이너라는 격리된 환경이 프로그램을 실행하고 있는 거죠.
한 번 직접 해볼까요.
(아마 ARM 버전은 없을거고, docker가 설치되어 있어야 하기도 하니 이번엔 구경만 해주세요.)
실행 예시: rancher/cowsay 이미지를 실행함
사용자@우리집:~$ sudo docker run rancher/cowsay "하하" Unable to find image 'rancher/cowsay:latest' locally latest: Pulling from rancher/cowsay cbdbe7a5bc2a: Already exists dd05e66d8cea: Pull complete 34d5e986f175: Pull complete 13eefd6dff68: Pull complete Digest: sha256:5dab61268bc18daf56febb5a856b618961cd806dbc49a22a636128ca26f0bd94 Status: Downloaded newer image for rancher/cowsay:latest ________ < 하하 > -------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || 사용자@우리집:~$
리눅스에는 소가 말하는 모양을 화면에 출력하는 cowsay 라는 프로그램이 있는데, 이걸 실행하기 위한 이미지(저장된 격리 환경)를 누가 만들어놨더라구요. 실행해봤는데, 아니나다를까 소가 말을 하는 게 잘 출력되는 것 같습니다. 이미지가 내 컴퓨터에 없어서 다운받는 모습도 보이시죠?
Docker를 설치하자
(TMI: 사실 이런 방법은 이미 표준화가 되어서, docker가 아니어도 이런 짓을 할 수 있기는 해요. 하지만 이번에는 가장 대중적인 docker를 사용하는 방법을 설명할 거에요.)
이제 Docker가 무엇인지 이해했으니, 쓰려면 설치를 해야겠죠?
접속한 원격 컴퓨터에서 계속 하겠습니다.
간단하게 "sudo apt install docker.io" 만 해도 되는데, 보통은 최신버전이 권장되니까 이 방법 대신에 공식 홈페이지에 나와있는 방법으로 설치하겠습니다.
공식 가이드 https://docs.docker.com/engine/install/ubuntu/
시간이 지남에 따라 설치 방법이 바뀔수도 있으니, 혹시 문제가 있다면 공식 가이드를 보고 진행해주세요.
공식 가이드에서는 설치하는 방법을 4가지 안내하고 있습니다.
- docker desktop 설치. 저희는 GUI를 쓰고 있지 않으므로 해당하지 않습니다.
- 수동 설치. 관리하기 번거롭습니다.
- 설치 스크립트 사용. 테스트용 / 개발용으로만 쓰라네요.
- docker 의 apt 저장소 추가. 이 방법을 쓸 겁니다.
이전에 nginx를 설치했을 때, "sudo apt install nginx"라는 명령을 사용했었죠? 이 때 사용한 프로그램 설치 관리자인 "apt" 는 사전에 등록된 저장소에서 프로그램을 다운받습니다. 그 저장소에 docker 회사가 관리하는 저장소를 추가해서 거기에서 다운받는 방법입니다.
설치하기
문서에서 시키는 대로 따라해봅시다. 사실 그대로 실행하는 거에요. 자세한 내용이 궁금하다면 ChatGPT에게 물어봅시다.
(* apt 와 apt-get 은 거의 동일한 명령어입니다.)
저장소 추가하기
# 저장소에 어떤 프로그램이 있는지를 업데이트합니다. sudo apt-get update # 다른 저장소를 추가하기 위해선, 암호와 관련된 프로그램이 필요합니다. sudo apt-get install ca-certificates curl gnupg
docker의 저장소를 추가하기 전에, 필요한 프로그램을 설치합니다.
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg
docker의 저장소의 서명을 다운받아 등록합니다. 엉뚱한 곳에서 다운받지 않기 위한 조치입니다.
echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
docker의 저장소를 apt의 저장소 목록에 추가합니다.
sudo apt-get update # sudo apt update 도 괜찮아요
저장소를 추가했으니, 어떤 프로그램이 있는지 업데이트해놓자구요. 저장소를 추가해놓고 정작 update로 내용을 확인을 안 해보면 설치할 수 없겠죠?
설치하기
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
이제 설치하면 돼요. 이전과 똑같이 sudo apt install 프로그램1 프로그램2 프로그램3... 을 실행하면 돼요. 설치할 게 좀 많네요!
sudo docker run hello-world
설치되었는지 확인하기 위해 hello-word 라는 이미지를 실행(*)해봅시다.
(*저장해놨던 격리된 환경 "이미지" 에서 새로운 격리된 환경 "컨테이너"를 만들어서 사전에 지정된 프로그램 실행)
아까 docker run 으로 cowsay를 실행했었죠? 마찬가지입니다.
이번에는 sudo로 관리자권한을 주고 실행하고 있는데, 이게 불편하다면 현재 사용자를 "docker" 그룹에 추가하면 됩니다. 보안 면에서 권장할만한 사항이 아니므로 "혹시나"의 측면에서 하지 않겠습니다. (sudo usermod -aG docker 사용자명 이면 충분하겠죠?)
실행 예시
ch@firefish-server:~$ sudo docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 70f5ac315c5a: Pull complete Digest: sha256:dcba6daec718f547568c562956fa47e1b03673dd010fe6ee58ca806767031d1c Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (arm64v8) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/ ch@firefish-server:~$
docker 관리하기
다음장에서는 "docker compose" 라는 docker를 사용하는 프로그램으로 firefish를 설치하고 실행해볼 것입니다.
그렇지만 그 전에 docker의 기본적인 관리법을 알아보고 넘어갑시다.
다른 모든 프로그램과 마찬가지로, 끝에 --help 를 붙이면 도움말을 볼 수 있습니다.
다시 한 번 개념을 보고 갑시다.
- Dockerfile: 이미지를 만드는 레시피
- 이미지 (Image): 파일로 저장해둔 격리된 환경
- 컨테이너 (Container): 이미지에서 새로 생성한 격리된 환경, 사전에 지정한 프로그램을 실행함
이미지 관리
이 컴퓨터에 저장되어 있는 이미지 목록을 보려면 sudo docker image ls 를 입력합니다. ls는 list를 줄인 것으로, 목록을 표시하는 명령어네요.
sudo docker image ls
실행 예시
ch@firefish-server:~$ sudo docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest b038788ddb22 3 months ago 9.14kB
아... hello-world를 실행했던 게 남아있네요. 지워버립시다.
image rm (remove를 줄였음) 이미지_ID 를 실행하시면 됩니다. 앞의 영어 이름이 아니라 뒤의 ID임에 주의합시다.
sudo docker iamge rm b038788ddb22
실행 예시
ch@firefish-server:~$ sudo docker image rm b038788ddb22 Error response from daemon: conflict: unable to delete b038788ddb22 (must be forced) - image is being used by stopped container 8c9169d98f16
삭제가 안 되네요. 이 이미지를 쓰고 있는 컨테이너가 있다는군요.
컨테이너 관리
실행중인 컨테이너가 뭐가 있나 볼까요. docker 명령어에 이어 추가 명령어로 "ps" 혹은 "container ps" 를 붙여서 실행하시면 됩니다. ps는 프로세스의 상태(process status) = 현재 실행중인 프로그램들 을 줄인 말일거에요.
sudo docker container ps # 혹은 sudo docker ps
실행 예시
ch@firefish-server:~$ sudo docker container ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
어찌된 일일까요? 아무것도 없네요. 실행이 끝난 컨테이너가 남아있는 건 아닐까요?
도움말을 봅시다.
sudo docker ps --help
실행 예시
ch@firefish-server:~$ sudo docker ps --help Usage: docker ps [OPTIONS] List containers Aliases: docker container ls, docker container list, docker container ps, docker ps Options: -a, --all Show all containers (default shows just running) # 생략
기본적으로 실행중인 컨테이너만 보여주는 거였군요. --all 을 붙이면 되겠네요. (그리고 docker ps와 docker ls는 같은 명령어였습니다.)
sudo docker ps --all
실행 예시
ch@firefish-server:~$ sudo docker ps --all CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8c9169d98f16 hello-world "/hello" 41 hours ago Exited (0) 41 hours ago unruffled_dirac
아, 저기 있네요. 지우는 건 아까와 똑같습니다. sudo docker (container, 생략 가능) rm 8c9169d98f16 겠죠? 사전에 지정했거나 임의로 만들어진 "NAMES" 부분을 사용해도 됩니다. 하지만 이번엔 그냥 전부 청소를 해주는 명령어를 써볼거에요.
docker system prune 으로 모든 걸 정리
docker 명령어중에는 "prune" 이라는 게 있는데, 안 쓰는 걸 정리하는 명령어입니다.
- docker image prune - 안 쓰는 이미지를 정리 (삭제)
- docker container prune - 안 쓰는 컨테이너를 정리 (삭제)
- docker system prune - 안 쓰는 이미지, 컨테이너, 네트워크 등을 정리 (삭제)
- 아마 더 있을거에요
가장 편리하고 위험한 게 docker system prune 입니다. 필요한 컨테이너들이 모두 실행중이라면 보통 문제가 되지는 않지만요.
한 번 실행해볼까요.
sudo docker system prune
실행 예시
ch@firefish-server:~$ sudo docker system prune WARNING! This will remove: - all stopped containers - all networks not used by at least one container - all dangling images - all dangling build cache Are you sure you want to continue? [y/N] y Deleted Containers: 8c9169d98f169c0735cbf9096ab824bfa01f19854b38e771bb2a2863e6bbb3f1 Total reclaimed space: 0B ch@firefish-server:~$
컨테이너가 삭제되었습니다. 이미지까지 지워지길 바랬는데 그러진 않네요. 이유는 모르겠고 일단 지워둡시다.
실행 예시
ch@firefish-server:~$ docker image rm b038788ddb22 Untagged: hello-world:latest Untagged: hello-world@sha256:dcba6daec718f547568c562956fa47e1b03673dd010fe6ee58ca806767031d1c Deleted: sha256:b038788ddb222cb7d6025b411759e4f5abe9910486c8f98534ead97befd77dd7 Deleted: sha256:a7866053acacfefb68912a8916b67d6847c12b51949c6b8a5580c6609c08ae45
이제 되는군요.