소개
이번 랩에서는 컨테이너를 실행하기 위해 Docker 명령어를 사용했던 랩 1 에서 얻은 지식을 바탕으로 진행합니다. Dockerfile 로부터 사용자 정의 Docker Image 를 생성할 것입니다. Image 를 빌드한 후, 중앙 레지스트리에 푸시하여 다른 환경에 배포하기 위해 풀 (pull) 할 수 있도록 합니다. 또한, Image 레이어와 Docker 가 "copy-on-write" 및 union file system 을 통합하여 Image 를 효율적으로 저장하고 컨테이너를 실행하는 방법에 대해 간략하게 설명합니다.
이번 랩에서는 몇 가지 Docker 명령어를 사용할 것입니다. 사용 가능한 모든 명령어에 대한 전체 문서는 공식 문서를 참조하십시오.
Python 앱 생성 (Docker 미사용)
다음 명령을 실행하여 간단한 파이썬 프로그램이 포함된 app.py라는 파일을 생성합니다. (전체 코드 블록을 복사하여 붙여넣기)
cd ~/project
echo 'from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello world!"
if __name__ == "__main__":
app.run(host="0.0.0.0")' > app.py
이것은 flask 를 사용하여 포트 5000(5000 은 flask 의 기본 포트) 에서 http 웹 서버를 노출하는 간단한 파이썬 앱입니다. 파이썬이나 flask 에 익숙하지 않더라도 걱정하지 마십시오. 이러한 개념은 모든 언어로 작성된 애플리케이션에 적용될 수 있습니다.
선택 사항: 파이썬과 pip 가 설치되어 있다면 이 앱을 로컬에서 실행할 수 있습니다. 그렇지 않은 경우 다음 단계로 진행하십시오.
$ python3 --version
$ pip3 --version
$ pip3 install flask
$ python3 app.py
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
http://0.0.0.0:5000/를 사용하여 새 브라우저 탭에서 앱을 엽니다.

Docker Image 생성 및 빌드
이제 로컬에 파이썬이 설치되어 있지 않다면 어떻게 해야 할까요? 걱정하지 마세요! 필요하지 않으니까요. 컨테이너를 사용하는 것의 장점 중 하나는 호스트 머신에 파이썬이 설치되어 있지 않아도 컨테이너 내에서 파이썬을 빌드할 수 있다는 것입니다.
다음 명령을 실행하여 Dockerfile을 생성합니다. (전체 코드 블록을 복사하여 붙여넣기)
echo 'FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py' > Dockerfile
Dockerfile 은 Docker Image 를 빌드하는 데 필요한 지침을 나열합니다. 위의 파일을 줄별로 살펴보겠습니다.
FROM python:3.8-alpine
이것은 Dockerfile 의 시작점입니다. 모든 Dockerfile 은 위에 레이어를 구축할 시작 Image 인 FROM 라인으로 시작해야 합니다.
이 경우, 애플리케이션을 실행하는 데 필요한 파이썬과 pip 버전이 이미 포함되어 있으므로 python:3.8-alpine 기본 레이어를 선택합니다 ( Dockerfile for python3.8/alpine3.12 참조).
alpine 버전은 Alpine Linux 배포판을 사용한다는 의미입니다. 이는 다른 많은 Linux 버전보다 훨씬 작으며, 크기가 약 8MB 이고, 디스크에 최소 설치 시 약 130MB 정도입니다. Image 가 작을수록 다운로드 (배포) 속도가 훨씬 빠르며, 공격 표면이 작기 때문에 보안 측면에서도 장점이 있습니다. Alpine Linux는 musl 과 BusyBox 를 기반으로 하는 Linux 배포판입니다.
여기서는 파이썬 Image 에 "3.8-alpine" 태그를 사용하고 있습니다. Docker Hub에서 공식 파이썬 Image 에 사용 가능한 태그를 살펴보십시오. 상위 Image 의 변경 사항을 제어하기 위해 상위 Image 를 상속할 때는 특정 태그를 사용하는 것이 가장 좋습니다. 태그가 지정되지 않으면 "latest" 태그가 적용되며, 이는 Image 의 최신 버전을 가리키는 동적 포인터 역할을 합니다.
보안상의 이유로, Docker Image 를 구축하는 레이어를 이해하는 것이 매우 중요합니다. 따라서 docker hub에서 찾을 수 있는 "공식" Image 또는 docker-store 에서 찾을 수 있는 비 커뮤니티 Image 만 사용하는 것이 좋습니다. 이러한 Image 는 특정 보안 요구 사항을 충족하도록 검증되었으며, 사용자가 따를 수 있는 매우 훌륭한 문서를 갖추고 있습니다. 이 파이썬 기본 Image 및 사용할 수 있는 다른 모든 Image 에 대한 자세한 정보는 docker hub에서 확인할 수 있습니다.
더 복잡한 애플리케이션의 경우, 체인 상위에 있는 FROM Image 를 사용해야 할 수 있습니다. 예를 들어, 파이썬 앱의 상위 Dockerfile은 FROM alpine으로 시작한 다음 Image 에 대한 일련의 CMD 및 RUN 명령을 지정합니다. 더 세밀한 제어가 필요한 경우, FROM alpine(또는 다른 배포판) 으로 시작하여 해당 단계를 직접 실행할 수 있습니다. 하지만 처음 시작할 때는 요구 사항에 가장 잘 맞는 공식 Image 를 사용하는 것이 좋습니다.
RUN pip install flask
RUN 명령은 패키지 설치, 파일 편집 또는 파일 권한 변경과 같이 애플리케이션에 대한 Image 를 설정하는 데 필요한 명령을 실행합니다. 이 경우 flask 를 설치하고 있습니다. RUN 명령은 빌드 시 실행되며 Image 의 레이어에 추가됩니다.
CMD ["python","app.py"]
CMD는 컨테이너를 시작할 때 실행되는 명령입니다. 여기서는 CMD를 사용하여 파이썬 앱을 실행하고 있습니다.
Dockerfile 당 하나의 CMD만 있을 수 있습니다. 둘 이상의 CMD를 지정하면 마지막 CMD가 적용됩니다. 상위 python:3.8-alpine 도 CMD(CMD python3) 를 지정합니다. 공식 python:alpine Image 에 대한 Dockerfile 은 여기에서 확인할 수 있습니다.
호스트에 파이썬을 설치하지 않고도 공식 파이썬 Image 를 직접 사용하여 파이썬 스크립트를 실행할 수 있습니다. 하지만 오늘은 애플리케이션을 포함하는 사용자 정의 Image 를 생성하여 애플리케이션으로 Image 를 빌드하고 다른 환경으로 배포할 수 있도록 합니다.
COPY app.py /app.py
이것은 로컬 디렉토리 (여기서 docker image build를 실행합니다) 의 app.py 를 Image 의 새 레이어로 복사합니다. 이 지침은 Dockerfile 의 마지막 줄입니다. 소스 코드를 Image 로 복사하는 등 자주 변경되는 레이어는 Docker 레이어 캐시를 최대한 활용하기 위해 파일 하단에 배치해야 합니다. 이렇게 하면 그렇지 않으면 캐시될 수 있는 레이어를 다시 빌드하지 않아도 됩니다. 예를 들어, FROM 지침에 변경 사항이 있으면 이 Image 의 모든 후속 레이어에 대한 캐시가 무효화됩니다. 이 랩의 뒷부분에서 이를 시연할 것입니다.
CMD ["python","app.py"] 라인 뒤에 배치하는 것은 직관적이지 않을 수 있습니다. CMD 라인은 컨테이너가 시작될 때만 실행되므로 여기서는 file not found 오류가 발생하지 않습니다.
자, 아주 간단한 Dockerfile 이 완성되었습니다. Dockerfile 에 넣을 수 있는 전체 명령 목록은 여기에서 확인할 수 있습니다. 이제 Dockerfile 을 정의했으므로 이를 사용하여 사용자 정의 Docker Image 를 빌드해 보겠습니다.
Docker Image 를 빌드합니다.
-t를 전달하여 Image 의 이름을 python-hello-world로 지정합니다.
docker image build -t python-hello-world .
Image 목록에 Image 가 표시되는지 확인합니다.
docker image ls
참고 기본 Image python:3.8-alpine도 목록에 있습니다.
history 명령을 실행하여 Image 및 해당 레이어의 기록을 표시할 수 있습니다.
docker history python-hello-world
docker history python:3.8-alpine
Docker Image 실행
이제 Image 를 빌드했으므로 실행하여 작동하는지 확인할 수 있습니다.
Docker Image 실행
docker run -p 5001:5000 -d python-hello-world
-p 플래그는 컨테이너 내에서 실행되는 포트를 호스트에 매핑합니다. 이 경우, 컨테이너 내부의 포트 5000 에서 실행되는 파이썬 앱을 호스트의 포트 5001 에 매핑하고 있습니다. 호스트의 다른 애플리케이션에서 포트 5001 을 이미 사용하고 있는 경우 5001 을 5002 와 같은 다른 값으로 바꿔야 할 수 있습니다.
터미널 창에서 PORTS 탭으로 이동하여 링크를 클릭하여 새 브라우저 탭에서 앱을 엽니다.

터미널에서 curl localhost:5001을 실행하면 hello world!가 반환됩니다.
컨테이너의 로그 출력을 확인합니다.
애플리케이션의 로그를 보려면 docker container logs 명령을 사용할 수 있습니다. 기본적으로 docker container logs는 애플리케이션에서 표준 출력으로 전송된 내용을 출력합니다. 실행 중인 컨테이너의 ID 를 찾으려면 docker container ls를 사용하십시오.
labex:project/ $ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52df977e5541 python-hello-world "python app.py" 2 minutes ago Up 2 minutes 0.0.0.0:5001->5000/tcp, :::5001->5000/tcp heuristic_lamport
labex:project/ $ docker container logs 52df977e5541
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.2:5000
Press CTRL+C to quit
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET /favicon.ico HTTP/1.1" 404 -
Dockerfile 은 애플리케이션에 대한 재현 가능한 빌드를 생성하는 방법입니다. 일반적인 워크플로우는 CI/CD 자동화가 빌드 프로세스의 일부로 docker image build를 실행하도록 하는 것입니다. Image 가 빌드되면 중앙 레지스트리로 전송되어 해당 애플리케이션의 인스턴스를 실행해야 하는 모든 환경 (예: 테스트 환경) 에서 액세스할 수 있습니다. 다음 단계에서는 사용자 정의 Image 를 공개 Docker 레지스트리인 Docker Hub 에 푸시하여 다른 개발자 및 운영자가 사용할 수 있도록 합니다.
중앙 레지스트리에 푸시
Docker Hub로 이동하여 아직 계정이 없다면 계정을 만듭니다. 또는 https://quay.io를 사용할 수도 있습니다.
이 랩에서는 Docker Hub 를 중앙 레지스트리로 사용합니다. Docker Hub 는 공개적으로 사용 가능한 Image 를 저장하는 무료 서비스이며, 유료로 개인 Image 를 저장할 수도 있습니다. Docker Hub 웹사이트로 이동하여 무료 계정을 만듭니다.
Docker 를 많이 사용하는 대부분의 조직은 자체 레지스트리를 내부적으로 설정합니다. 간소화를 위해 Docker Hub 를 사용하지만, 다음 개념은 모든 레지스트리에 적용됩니다.
로그인
터미널에서 docker login을 입력하거나, podman 을 사용하는 경우 podman login을 입력하여 Image 레지스트리 계정에 로그인할 수 있습니다.
labex:project/ $ export DOCKERHUB_USERNAME=<your_docker_username>
labex:project/ $ docker login docker.io -u $DOCKERHUB_USERNAME
Password:
WARNING! Your password will be stored unencrypted in /home/labex/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
사용자 이름으로 Image 태그 지정
Docker Hub 명명 규칙은 [dockerhub username]/[image name]으로 Image 에 태그를 지정하는 것입니다. 이를 위해 이전에 생성한 Image python-hello-world에 해당 형식에 맞게 태그를 지정합니다.
docker tag python-hello-world $DOCKERHUB_USERNAME/python-hello-world
Image 를 레지스트리에 푸시
적절하게 태그가 지정된 Image 가 있으면 docker push 명령을 사용하여 Image 를 Docker Hub 레지스트리에 푸시할 수 있습니다.
docker push $DOCKERHUB_USERNAME/python-hello-world
브라우저에서 Docker Hub 에서 Image 확인
Docker Hub로 이동하여 프로필로 이동하여 새로 업로드된 Image 를 https://hub.docker.com/repository/docker/<dockerhub-username>/python-hello-world에서 확인합니다.
이제 Image 가 Docker Hub 에 있으므로 다른 개발자와 운영자는 docker pull 명령을 사용하여 Image 를 다른 환경에 배포할 수 있습니다.
참고: Docker Image 에는 Image 내에서 애플리케이션을 실행하는 데 필요한 모든 종속성이 포함되어 있습니다. 이는 모든 배포 환경에 설치된 종속성에 의존할 때 환경 드리프트 (버전 차이) 를 더 이상 처리할 필요가 없기 때문에 유용합니다. 또한 이러한 환경을 프로비저닝하기 위한 추가 단계를 거칠 필요가 없습니다. 단 한 단계: Docker 를 설치하면 됩니다.
변경 배포
"hello world!" 애플리케이션은 과대평가되었으므로, 대신 "Hello Beautiful World!"라고 표시되도록 앱을 업데이트해 보겠습니다.
app.py 업데이트
app.py에서 문자열 "Hello World"를 "Hello Beautiful World!"로 바꿉니다. 다음 명령으로 파일을 업데이트할 수 있습니다. (전체 코드 블록을 복사하여 붙여넣기)
echo 'from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello beautiful world!"
if __name__ == "__main__":
app.run(host="0.0.0.0")' > app.py
Image 다시 빌드 및 푸시
이제 앱이 업데이트되었으므로, 앱을 다시 빌드하고 Docker Hub 레지스트리에 푸시하기 위해 위의 단계를 반복해야 합니다.
먼저 다시 빌드합니다. 이번에는 빌드 명령에 Docker Hub 사용자 이름을 사용합니다.
docker image build -t $DOCKERHUB_USERNAME/python-hello-world .
1-3 단계에 "Using cache"가 표시되는 것을 확인합니다. Docker Image 의 이러한 레이어는 이미 빌드되었으며 docker image build는 이러한 레이어를 다시 빌드하는 대신 캐시에서 사용합니다.
docker push $DOCKERHUB_USERNAME/python-hello-world
레이어를 푸시하기 위한 캐싱 메커니즘도 있습니다. Docker Hub 에는 이전 푸시에서 모든 레이어가 이미 존재하므로 변경된 레이어 하나만 푸시합니다.
레이어를 변경하면 그 위에 빌드된 모든 레이어를 다시 빌드해야 합니다. Dockerfile 의 각 줄은 이전 줄에서 생성된 레이어 위에 빌드되는 새 레이어를 빌드합니다. 이것이 Dockerfile 의 줄 순서가 중요한 이유입니다. 가장 자주 변경될 가능성이 있는 레이어 (COPY app.py /app.py) 가 Dockerfile 의 마지막 줄이 되도록 Dockerfile 을 최적화했습니다. 일반적으로 애플리케이션의 경우 코드가 가장 빈번하게 변경됩니다. 이러한 최적화는 자동화가 가능한 한 빨리 실행되기를 원하는 CI/CD 프로세스에 특히 중요합니다.
Image 레이어 이해
Docker 의 주요 설계 속성 중 하나는 유니온 파일 시스템 (union file system) 을 사용하는 것입니다.
이전에 생성한 Dockerfile을 생각해 보겠습니다.
FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py
이러한 각 줄은 레이어입니다. 각 레이어에는 이전 레이어의 델타, 차이 또는 변경 사항만 포함되어 있습니다. 이러한 레이어를 단일 실행 컨테이너로 함께 넣기 위해 Docker 는 유니온 파일 시스템을 사용하여 레이어를 단일 보기로 투명하게 오버레이합니다.
Image 의 각 레이어는 실행 중인 컨테이너를 위해 생성된 최상위 레이어를 제외하고 읽기 전용입니다. 읽기/쓰기 컨테이너 레이어는 "copy-on-write"를 구현합니다. 즉, 하위 Image 레이어에 저장된 파일은 해당 파일에 대한 편집이 수행될 때만 읽기/쓰기 컨테이너 레이어로 가져옵니다. 그런 다음 해당 변경 사항은 실행 중인 컨테이너 레이어에 저장됩니다. "copy-on-write" 기능은 매우 빠르며 거의 모든 경우 성능에 눈에 띄는 영향을 미치지 않습니다. docker diff 명령을 사용하여 어떤 파일이 컨테이너 수준으로 가져왔는지 검사할 수 있습니다. docker diff 사용 방법에 대한 자세한 내용은 여기에서 확인할 수 있습니다.

Image 레이어는 읽기 전용이므로 Image 및 실행 중인 컨테이너에서 공유할 수 있습니다. 예를 들어, 유사한 기본 레이어를 가진 자체 Dockerfile 로 새 Python 앱을 생성하면 첫 번째 Python 앱과 공통으로 가지고 있는 모든 레이어를 공유합니다.
FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app2.py"]
COPY app2.py /app2.py

동일한 Image 에서 여러 컨테이너를 시작할 때 레이어 공유를 경험할 수도 있습니다. 컨테이너가 동일한 읽기 전용 레이어를 사용하므로 컨테이너 시작이 매우 빠르고 호스트에 매우 적은 공간을 차지한다고 상상할 수 있습니다.
이 Dockerfile 과 이 랩에서 이전에 생성한 Dockerfile 에 중복된 줄이 있는 것을 알 수 있습니다. 이는 매우 사소한 예이지만, 두 Dockerfile 의 공통 줄을 "base" Dockerfile 로 가져와서 FROM 명령을 사용하여 각 자식 Dockerfile 로 가리킬 수 있습니다.
Image 레이어링은 빌드 및 푸시를 위한 Docker 캐싱 메커니즘을 활성화합니다. 예를 들어, 마지막 docker push의 출력은 Image 의 일부 레이어가 이미 Docker Hub 에 존재함을 보여줍니다.
$ docker push $DOCKERHUB_USERNAME/python-hello-world
레이어를 자세히 살펴보려면 생성한 Python Image 의 docker image history 명령을 사용할 수 있습니다.
$ docker image history python-hello-world
각 줄은 Image 의 레이어를 나타냅니다. 상위 줄이 생성한 Dockerfile 과 일치하고 아래 줄이 상위 Python Image 에서 가져온다는 것을 알 수 있습니다. "<missing>" 태그에 대해 걱정하지 마십시오. 이것들은 여전히 정상적인 레이어입니다. Docker 시스템에서 ID 가 부여되지 않았을 뿐입니다.
정리
이 랩을 완료하면 호스트에 많은 수의 실행 중인 컨테이너가 생성됩니다. 이를 정리해 보겠습니다.
실행 중인 각 컨테이너에 대해 docker container stop [container id]를 실행합니다.
먼저 docker container ls를 사용하여 실행 중인 컨테이너 목록을 가져옵니다.
$ docker container ls
그런 다음 목록의 각 컨테이너에 대해 명령을 실행합니다.
$ docker container stop <container_id>
중지된 컨테이너 제거
docker system prune은 시스템을 정리하는 데 매우 유용한 명령입니다. 중지된 모든 컨테이너, 사용하지 않는 볼륨 및 네트워크, 그리고 댕글링 (dangling) Image 를 제거합니다.
$ docker system prune
WARNING! This will remove:
- all stopped containers
- all volumes not used by at least one container
- all networks not used by at least one container
- all dangling images
Are you sure you want to continue? [y/N] y
Deleted Containers:
0b2ba61df37fb4038d9ae5d145740c63c2c211ae2729fc27dc01b82b5aaafa26
Total reclaimed space: 300.3kB
요약
이 랩에서는 사용자 정의 Docker 컨테이너를 생성하여 가치를 추가하기 시작했습니다.
핵심 내용:
- Dockerfile 은 애플리케이션에 대한 재현 가능한 빌드를 생성하고 애플리케이션을 Docker 와 CI/CD 파이프라인에 통합하는 방법입니다.
- Docker Image 는 중앙 레지스트리를 통해 모든 환경에서 사용할 수 있습니다. Docker Hub 는 레지스트리의 한 예이지만, 직접 제어하는 서버에 자체 레지스트리를 배포할 수 있습니다.
- Docker Image 는 Image 내에서 애플리케이션을 실행하는 데 필요한 모든 종속성을 포함합니다. 이는 모든 환경에 배포할 때 설치되는 종속성에 의존할 때 더 이상 환경 드리프트 (버전 차이) 를 처리할 필요가 없기 때문에 유용합니다.
- Docker 는 Image 레이어를 재사용하기 위해 유니온 파일 시스템 (union file system) 과 "copy on write"를 사용합니다. 이는 Image 저장 공간을 줄이고 컨테이너 시작 성능을 크게 향상시킵니다.
- Image 레이어는 Docker 빌드 및 푸시 시스템에 의해 캐시됩니다. 원하는 시스템에 이미 있는 Image 레이어를 다시 빌드하거나 다시 푸시할 필요가 없습니다.
- Dockerfile 의 각 줄은 새 레이어를 생성하며, 레이어 캐시로 인해 더 자주 변경되는 줄 (예: Image 에 소스 코드 추가) 은 파일 하단에 나열되어야 합니다.