🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

컨테이너와 Docker 완벽 복습 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 19. · 5 Views

컨테이너와 Docker 완벽 복습 가이드

초급 개발자를 위한 컨테이너와 Docker 핵심 개념 정리입니다. 실무에서 바로 활용할 수 있는 명령어부터 효율적인 이미지 빌드 전략까지, 실전 중심으로 설명합니다.


목차

  1. 컨테이너란_무엇인가
  2. Docker_기본_명령어
  3. Dockerfile_구조
  4. 이미지_빌드하기
  5. 컨테이너_실행과_관리
  6. 멀티스테이지_빌드

1. 컨테이너란 무엇인가

신입 개발자 김개발 씨는 첫 프로젝트를 배포하면서 난감한 상황에 처했습니다. 로컬 환경에서는 완벽하게 돌아가던 애플리케이션이 서버에 올리니 "제 컴퓨터에서는 잘 됐는데요?"라는 말만 되풀이하게 되었습니다.

선배 개발자가 한마디 던집니다. "컨테이너를 쓰면 그런 문제 없어요."

컨테이너는 애플리케이션과 그 실행에 필요한 모든 환경을 하나로 묶어 어디서든 동일하게 실행할 수 있게 만든 독립적인 실행 환경입니다. 마치 이사할 때 짐을 박스에 담듯이, 코드와 라이브러리, 설정 파일을 하나의 패키지로 만들어 어떤 서버에서든 똑같이 실행됩니다.

이를 통해 개발, 테스트, 운영 환경의 차이로 인한 문제를 근본적으로 해결할 수 있습니다.

다음 코드를 살펴봅시다.

# 간단한 Python 웹 애플리케이션
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    # 컨테이너 안에서 실행되는 애플리케이션
    return "Hello from Container!"

if __name__ == '__main__':
    # 어떤 환경에서도 동일하게 실행됩니다
    app.run(host='0.0.0.0', port=5000)

김개발 씨는 입사 한 달 차 신입 개발자입니다. 열심히 개발한 첫 프로젝트를 드디어 서버에 배포하는 날, 예상치 못한 에러 메시지가 쏟아집니다.

"ModuleNotFoundError", "Version conflict" 같은 메시지들이 화면을 가득 채웁니다. "이상하네, 내 컴퓨터에서는 완벽하게 돌아갔는데..." 김개발 씨는 당황스러웠습니다.

로컬에서는 Python 3.9를 쓰고 있었는데, 서버는 3.7이었습니다. 게다가 필요한 라이브러리도 일부 설치되지 않았습니다.

선배 개발자 박시니어 씨가 다가와 화면을 봅니다. "아, 환경 차이 문제네요.

컨테이너를 사용하면 이런 일이 없어요." 그렇다면 컨테이너란 정확히 무엇일까요? 쉽게 비유하자면, 컨테이너는 마치 이사할 때 사용하는 이삿짐 박스와 같습니다.

박스 안에 물건뿐만 아니라 그 물건을 사용하는 데 필요한 모든 것을 함께 넣어두면, 어떤 집으로 이사를 가든 박스만 열면 바로 사용할 수 있습니다. 컨테이너도 마찬가지로 애플리케이션 코드와 함께 필요한 라이브러리, 설정 파일, 실행 환경을 모두 담아 패키징합니다.

컨테이너가 없던 시절에는 어땠을까요? 개발자들은 새로운 서버에 애플리케이션을 배포할 때마다 Python 버전을 맞추고, 필요한 라이브러리를 일일이 설치하고, 환경 변수를 설정해야 했습니다.

서버가 10대라면 같은 작업을 10번 반복해야 했습니다. 더 큰 문제는 각 서버마다 미묘한 차이가 생길 수 있다는 점이었습니다.

OS 버전이 다르거나, 이미 설치된 다른 애플리케이션과 충돌하는 경우도 빈번했습니다. 바로 이런 문제를 해결하기 위해 컨테이너 기술이 등장했습니다.

컨테이너를 사용하면 환경 일관성이 보장됩니다. 개발자의 노트북에서 돌아가는 것과 똑같은 환경이 운영 서버에서도 실행됩니다.

또한 빠른 배포도 가능합니다. 서버마다 환경을 구축할 필요 없이 컨테이너만 실행하면 됩니다.

무엇보다 격리성이라는 큰 이점이 있습니다. 여러 애플리케이션을 같은 서버에서 실행해도 서로 간섭하지 않습니다.

전통적인 가상 머신과는 어떻게 다를까요? 가상 머신은 전체 운영체제를 포함하기 때문에 무겁고 시작 시간이 오래 걸립니다.

하나의 가상 머신이 몇 GB를 차지하고, 부팅에 수십 초가 걸리기도 합니다. 반면 컨테이너는 호스트 OS의 커널을 공유하면서 프로세스만 격리합니다.

크기는 수백 MB 수준이고, 시작 시간은 1초 미만입니다. 위의 코드를 살펴보겠습니다.

간단한 Flask 웹 애플리케이션입니다. 이 코드를 컨테이너에 담으면, Flask 라이브러리가 설치되지 않은 서버에서도 문제없이 실행됩니다.

컨테이너 안에 Python 실행 환경과 Flask 라이브러리가 모두 포함되어 있기 때문입니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 쇼핑몰 서비스를 개발한다고 가정해봅시다. 프론트엔드 개발자는 React 애플리케이션을 컨테이너로 만들고, 백엔드 개발자는 Node.js API 서버를 컨테이너로 만듭니다.

데이터베이스도 컨테이너로 실행합니다. 각 컨테이너는 독립적으로 실행되면서도 네트워크로 연결되어 하나의 시스템으로 동작합니다.

Netflix, Uber 같은 글로벌 기업들이 수천 개의 컨테이너를 운영하며 서비스를 제공하고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 컨테이너 안에 데이터를 저장하는 것입니다. 컨테이너는 언제든 삭제되고 재생성될 수 있기 때문에, 컨테이너 내부의 데이터는 휘발성입니다.

중요한 데이터는 반드시 볼륨을 사용하여 외부에 저장해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 많은 기업이 컨테이너를 사용하는 거군요!" 컨테이너의 개념을 제대로 이해하면 개발과 배포의 패러다임이 완전히 바뀝니다.

더 이상 "내 컴퓨터에서는 잘 됐는데요"라는 말을 할 필요가 없어집니다.

실전 팁

💡 - 컨테이너는 가볍고 빠르게 시작되므로 마이크로서비스 아키텍처에 최적입니다

  • 중요한 데이터는 반드시 볼륨으로 관리하여 컨테이너가 삭제되어도 보존되도록 하세요
  • 개발 환경과 운영 환경을 완전히 동일하게 유지할 수 있어 배포 리스크가 크게 줄어듭니다

2. Docker 기본 명령어

컨테이너의 개념을 이해한 김개발 씨는 이제 실제로 사용해보고 싶어졌습니다. "그런데 컨테이너를 어떻게 만들고 실행하나요?" 박시니어 씨가 터미널을 열며 답합니다.

"Docker 명령어를 배워봅시다. 자주 쓰는 것만 알면 돼요."

Docker는 컨테이너를 만들고 관리하는 가장 널리 쓰이는 플랫폼입니다. Docker의 기본 명령어들은 컨테이너의 생명주기를 관리하는 핵심 도구입니다.

docker run으로 컨테이너를 실행하고, docker ps로 실행 중인 컨테이너를 확인하며, docker stop으로 중지하는 등 직관적인 명령어 체계를 가지고 있습니다.

다음 코드를 살펴봅시다.

# Docker 기본 명령어 모음

# 1. 이미지 다운로드
docker pull nginx:latest

# 2. 컨테이너 실행 (포트 매핑과 함께)
docker run -d -p 8080:80 --name my-nginx nginx:latest

# 3. 실행 중인 컨테이너 목록 확인
docker ps

# 4. 모든 컨테이너 목록 (중지된 것 포함)
docker ps -a

# 5. 컨테이너 중지
docker stop my-nginx

# 6. 컨테이너 삭제
docker rm my-nginx

# 7. 이미지 목록 확인
docker images

# 8. 컨테이너 로그 확인
docker logs my-nginx

김개발 씨는 이제 본격적으로 Docker를 사용해보기로 했습니다. 먼저 터미널을 열고 첫 명령어를 입력하려는데, 수많은 명령어 옵션에 압도당합니다.

"이걸 다 외워야 하나요?" 박시니어 씨가 웃으며 답합니다. "아니에요.

실무에서 자주 쓰는 명령어는 손에 꼽을 정도예요. 하나씩 같이 해봅시다." 그렇다면 가장 기본이 되는 Docker 명령어들은 무엇일까요?

쉽게 비유하자면, Docker 명령어는 마치 TV 리모컨과 같습니다. 리모컨에는 수십 개의 버튼이 있지만, 실제로 자주 쓰는 건 전원, 채널, 볼륨 정도입니다.

Docker도 마찬가지로 run, ps, stop, rm 같은 핵심 명령어만 알면 대부분의 작업을 할 수 있습니다. Docker를 처음 배우는 개발자들은 어떤 어려움을 겪을까요?

많은 초보자가 명령어의 옵션들 때문에 혼란스러워합니다. docker run 하나만 해도 -d, -p, --name, -v, -e 등 수많은 옵션이 있습니다.

어떤 옵션을 언제 써야 할지 몰라 매번 구글링을 하게 됩니다. 또 다른 문제는 컨테이너와 이미지의 차이를 헷갈려하는 것입니다.

바로 이런 혼란을 줄이기 위해 핵심 명령어부터 익히는 것이 중요합니다. docker pull은 이미지를 다운로드하는 명령어입니다.

마치 앱스토어에서 앱을 다운받는 것과 같습니다. nginx:latest는 nginx 웹 서버의 최신 버전 이미지를 의미합니다.

docker run은 다운받은 이미지로 컨테이너를 실행합니다. -d 옵션은 백그라운드에서 실행하라는 뜻이고, -p 8080:80은 호스트의 8080 포트를 컨테이너의 80 포트로 연결한다는 의미입니다.

docker ps는 현재 실행 중인 컨테이너 목록을 보여줍니다. 작업 관리자에서 실행 중인 프로그램을 보는 것과 비슷합니다.

-a 옵션을 추가하면 중지된 컨테이너까지 모두 표시됩니다. 위의 명령어들을 하나씩 실행해보겠습니다.

먼저 docker pull nginx:latest를 실행하면 Docker Hub에서 nginx 이미지를 다운로드합니다. 완료되면 docker run -d -p 8080:80 --name my-nginx nginx:latest로 컨테이너를 실행합니다.

--name 옵션으로 컨테이너에 알아보기 쉬운 이름을 지정할 수 있습니다. 이제 브라우저에서 localhost:8080에 접속하면 nginx 웰컴 페이지가 나타납니다.

docker ps를 실행하면 방금 실행한 my-nginx 컨테이너가 목록에 표시됩니다. CONTAINER ID, IMAGE, STATUS, PORTS 같은 정보를 확인할 수 있습니다.

docker logs my-nginx를 실행하면 컨테이너 내부에서 발생한 로그를 볼 수 있습니다. 디버깅할 때 매우 유용합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 데이터베이스를 로컬에 설치하지 않고 컨테이너로 실행할 수 있습니다.

docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=mysecret postgres 명령어 하나면 PostgreSQL 데이터베이스가 실행됩니다. 개발이 끝나면 docker stop과 docker rm으로 깔끔하게 제거할 수 있습니다.

로컬 환경을 더럽히지 않고 필요할 때만 사용할 수 있는 것입니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 컨테이너를 중지하지 않고 계속 쌓아두는 것입니다. docker ps -a로 확인해보면 수십 개의 중지된 컨테이너가 남아있는 경우가 많습니다.

이것들은 디스크 공간을 차지하므로 docker rm으로 정리하거나, docker run --rm 옵션을 사용하여 중지 시 자동으로 삭제되게 할 수 있습니다. 또 다른 실수는 포트 충돌입니다.

이미 8080 포트를 사용 중인데 같은 포트로 다른 컨테이너를 실행하려고 하면 에러가 발생합니다. docker ps로 현재 사용 중인 포트를 확인하는 습관을 들이면 좋습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 몇 가지 명령어를 직접 실행해본 김개발 씨는 신기해합니다.

"생각보다 간단하네요!" Docker 기본 명령어를 익히면 개발 환경 구축이 훨씬 쉬워집니다. 복잡한 설치 과정 없이 필요한 서비스를 즉시 실행하고 테스트할 수 있습니다.

실전 팁

💡 - docker run --rm 옵션을 사용하면 컨테이너 중지 시 자동으로 삭제되어 관리가 편합니다

  • docker exec -it 컨테이너명 /bin/bash로 실행 중인 컨테이너 내부에 접속할 수 있습니다
  • docker system prune으로 사용하지 않는 컨테이너, 이미지, 네트워크를 한 번에 정리할 수 있습니다

3. Dockerfile 구조

기본 명령어에 익숙해진 김개발 씨는 이제 자신의 애플리케이션을 컨테이너로 만들고 싶어졌습니다. "제가 작성한 Python 코드를 어떻게 컨테이너로 만들 수 있나요?" 박시니어 씨가 Dockerfile이라는 파일을 열어 보여줍니다.

"이 파일이 컨테이너 이미지를 만드는 설계도예요."

Dockerfile은 컨테이너 이미지를 자동으로 빌드하기 위한 명령어들을 담은 텍스트 파일입니다. FROM으로 베이스 이미지를 지정하고, COPY로 파일을 복사하며, RUN으로 명령을 실행하고, CMD로 컨테이너 시작 시 실행할 명령을 정의합니다.

마치 요리 레시피처럼 단계별로 이미지를 만드는 과정을 기술합니다.

다음 코드를 살펴봅시다.

# Python 웹 애플리케이션을 위한 Dockerfile

# 1. 베이스 이미지 선택
FROM python:3.9-slim

# 2. 작업 디렉토리 설정
WORKDIR /app

# 3. 의존성 파일 복사
COPY requirements.txt .

# 4. 의존성 설치
RUN pip install --no-cache-dir -r requirements.txt

# 5. 애플리케이션 코드 복사
COPY . .

# 6. 컨테이너 실행 시 노출할 포트
EXPOSE 5000

# 7. 컨테이너 시작 시 실행할 명령
CMD ["python", "app.py"]

김개발 씨는 자신이 만든 Flask 애플리케이션을 컨테이너로 만들고 싶습니다. 하지만 어디서부터 시작해야 할지 막막합니다.

"이미지는 어떻게 만드나요?" 박시니어 씨가 프로젝트 폴더에 Dockerfile이라는 새 파일을 만듭니다. "이 파일에 이미지를 만드는 과정을 단계별로 작성하면 돼요.

마치 요리 레시피를 작성하는 것과 같아요." 그렇다면 Dockerfile은 정확히 무엇일까요? 쉽게 비유하자면, Dockerfile은 마치 가구 조립 설명서와 같습니다.

IKEA 가구를 조립할 때 1단계부터 순서대로 따라가듯이, Docker도 Dockerfile의 명령어를 위에서 아래로 순서대로 실행하며 이미지를 만듭니다. 각 단계는 하나의 레이어가 되어 쌓이고, 최종적으로 완성된 이미지가 됩니다.

Dockerfile 없이 이미지를 만든다면 어떻게 될까요? 수동으로 컨테이너를 실행하고, 필요한 패키지를 설치하고, 파일을 복사하고, 설정을 바꾸고, docker commit으로 이미지를 저장해야 합니다.

이 과정을 팀원들에게 설명하려면 복잡한 문서를 작성해야 합니다. 누군가 실수하거나 단계를 빠뜨리면 다른 이미지가 만들어집니다.

재현성이 없고, 유지보수가 어렵습니다. 바로 이런 문제를 해결하기 위해 Dockerfile이 등장했습니다.

Dockerfile을 사용하면 재현 가능한 빌드가 보장됩니다. 같은 Dockerfile로는 누가 어디서 빌드하든 동일한 이미지가 만들어집니다.

또한 버전 관리도 가능합니다. Dockerfile을 Git에 커밋하면 이미지 빌드 과정의 변경 이력을 추적할 수 있습니다.

무엇보다 자동화가 가능해집니다. CI/CD 파이프라인에서 자동으로 이미지를 빌드하고 배포할 수 있습니다.

위의 Dockerfile을 한 줄씩 살펴보겠습니다. FROM python:3.9-slim은 베이스 이미지를 지정합니다.

처음부터 모든 것을 만드는 대신, 이미 Python이 설치된 이미지를 기반으로 시작합니다. slim 태그는 불필요한 패키지를 제거한 경량 버전입니다.

WORKDIR /app은 컨테이너 내부의 작업 디렉토리를 설정합니다. 이후의 모든 명령은 이 디렉토리에서 실행됩니다.

mkdir과 cd를 합친 것과 비슷합니다. **COPY requirements.txt .**는 로컬의 requirements.txt 파일을 컨테이너의 /app 디렉토리로 복사합니다.

점(.)은 현재 작업 디렉토리를 의미합니다. 왜 애플리케이션 코드보다 먼저 requirements.txt만 복사할까요?

이것은 레이어 캐싱을 활용하기 위한 최적화 기법입니다. RUN pip install --no-cache-dir -r requirements.txt는 Python 의존성을 설치합니다.

RUN 명령은 이미지 빌드 시점에 실행됩니다. --no-cache-dir 옵션은 pip 캐시를 저장하지 않아 이미지 크기를 줄입니다.

**COPY . .**는 나머지 애플리케이션 코드를 모두 복사합니다.

requirements.txt를 먼저 복사하고 의존성을 설치한 후에 코드를 복사하는 이유는, 코드가 자주 변경되어도 의존성 설치 레이어는 캐시를 재사용할 수 있기 때문입니다. EXPOSE 5000은 컨테이너가 5000번 포트를 사용한다는 것을 문서화합니다.

실제로 포트를 열지는 않고, 이미지를 사용하는 사람에게 정보를 제공하는 역할입니다. **CMD ["python", "app.py"]**는 컨테이너가 시작될 때 실행할 명령을 지정합니다.

RUN은 빌드 시점, CMD는 실행 시점에 동작한다는 차이가 있습니다. 실제 현업에서는 어떻게 활용할까요?

실제 프로젝트에서는 Dockerfile을 프로젝트 루트에 두고 Git으로 관리합니다. 코드가 변경될 때마다 Dockerfile도 함께 업데이트합니다.

예를 들어 새로운 라이브러리를 추가하면 requirements.txt를 수정하고, 환경 변수가 필요하면 ENV 명령어를 추가합니다. 많은 기업에서 Dockerfile을 코드 리뷰 대상에 포함시켜 보안과 최적화를 검증합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 .dockerignore 파일을 만들지 않는 것입니다.

COPY . .를 실행하면 node_modules, .git, pycache 같은 불필요한 파일까지 모두 복사됩니다.

.dockerignore 파일에 제외할 파일 목록을 작성하면 빌드 속도가 빨라지고 이미지 크기도 줄어듭니다. 또 다른 실수는 RUN 명령을 너무 많이 사용하는 것입니다.

RUN 하나마다 레이어가 생성되므로, 관련된 명령은 &&로 연결하여 하나의 RUN으로 합치는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

Dockerfile을 작성한 김개발 씨는 감탄합니다. "이렇게 간단한 텍스트 파일로 이미지를 만들 수 있다니 신기해요!" Dockerfile을 제대로 작성하면 팀 전체가 동일한 환경에서 개발할 수 있고, 배포도 일관되게 이루어집니다.

인프라를 코드로 관리하는 첫걸음입니다.

실전 팁

💡 - .dockerignore 파일을 반드시 만들어 불필요한 파일 복사를 방지하세요

  • 자주 변경되는 파일은 뒤에, 자주 변경되지 않는 파일은 앞에 배치하여 캐시를 최대한 활용하세요
  • RUN 명령을 연결할 때는 && \로 가독성을 높이면 좋습니다

4. 이미지 빌드하기

Dockerfile을 작성한 김개발 씨는 이제 실제로 이미지를 만들어볼 차례입니다. "이 Dockerfile로 어떻게 이미지를 만들죠?" 박시니어 씨가 터미널에 명령어를 입력합니다.

"docker build 명령어를 사용하면 돼요. 빌드 과정을 보면 Docker가 어떻게 이미지를 만드는지 이해할 수 있어요."

docker build 명령어는 Dockerfile을 읽어 단계별로 실행하며 컨테이너 이미지를 생성합니다. 각 명령어는 하나의 레이어를 만들고, 변경되지 않은 레이어는 캐시를 사용하여 빌드 속도를 높입니다.

-t 옵션으로 이미지에 태그를 지정하고, 빌드 컨텍스트를 통해 필요한 파일만 전달하여 효율적으로 빌드합니다.

다음 코드를 살펴봅시다.

# 이미지 빌드 명령어

# 1. 기본 빌드 (현재 디렉토리의 Dockerfile 사용)
docker build -t my-app:1.0 .

# 2. 다른 이름의 Dockerfile 사용
docker build -t my-app:1.0 -f Dockerfile.dev .

# 3. 빌드 인자 전달
docker build --build-arg VERSION=1.0.0 -t my-app:1.0 .

# 4. 캐시 없이 처음부터 빌드
docker build --no-cache -t my-app:1.0 .

# 5. 빌드된 이미지 확인
docker images

# 6. 빌드한 이미지로 컨테이너 실행
docker run -d -p 5000:5000 my-app:1.0

김개발 씨는 Dockerfile을 작성했지만, 아직 이미지는 만들어지지 않았습니다. "파일만 만들면 자동으로 이미지가 되는 건가요?" 박시니어 씨가 고개를 젓습니다.

"아니에요. Dockerfile은 설계도일 뿐이고, docker build 명령어로 실제 이미지를 만들어야 해요.

한번 같이 실행해봅시다." 그렇다면 docker build는 정확히 무엇을 하는 걸까요? 쉽게 비유하자면, docker build는 마치 3D 프린터와 같습니다.

설계 도면(Dockerfile)을 입력하면, 한 층씩 쌓아 올리며 최종 제품(이미지)을 만듭니다. 각 층이 잘 쌓였는지 확인하면서 진행하고, 문제가 있으면 해당 층에서 멈춥니다.

한 번 만든 층은 재사용할 수 있어 같은 부분을 다시 만들 필요가 없습니다. 빌드 과정을 자동화하지 않으면 어떻게 될까요?

개발자가 수동으로 컨테이너를 실행하고, 명령어를 하나씩 입력하고, 마지막에 docker commit으로 이미지를 저장해야 합니다. 이 과정에서 실수가 생기기 쉽고, 똑같이 재현하기도 어렵습니다.

팀원마다 조금씩 다른 이미지를 만들게 되고, "왜 제 이미지는 안 되는데 다른 사람 것은 되죠?"라는 질문이 끊임없이 나옵니다. 바로 이런 문제를 해결하기 위해 자동화된 빌드 프로세스가 필요합니다.

docker build를 사용하면 일관성 있는 빌드가 보장됩니다. 같은 Dockerfile과 소스 코드로는 항상 같은 이미지가 만들어집니다.

또한 레이어 캐싱으로 빌드 속도가 향상됩니다. 변경되지 않은 부분은 다시 빌드하지 않고 캐시를 사용합니다.

무엇보다 빌드 과정의 가시성이 확보됩니다. 각 단계의 출력을 보며 무엇이 실행되는지 정확히 알 수 있습니다.

위의 명령어들을 하나씩 실행해보겠습니다. **docker build -t my-app:1.0 .**에서 -t 옵션은 이미지에 태그를 지정합니다.

my-app은 이미지 이름이고, 1.0은 버전입니다. 마지막의 점(.)은 빌드 컨텍스트로, 현재 디렉토리의 모든 파일을 빌드에 사용하겠다는 의미입니다.

명령어를 실행하면 Docker는 Dockerfile의 각 명령어를 순서대로 처리합니다. Step 1/7 : FROM python:3.9-slim처럼 진행 상황을 표시하고, 각 단계가 완료되면 ---> Running in 해시값 같은 메시지를 출력합니다.

빌드가 끝나면 Successfully built 이미지ID와 Successfully tagged my-app:1.0 메시지가 나타납니다. docker images 명령어로 확인하면 방금 만든 my-app:1.0 이미지가 목록에 표시됩니다.

레이어 캐싱은 어떻게 작동할까요? 두 번째 빌드부터는 변경되지 않은 레이어에 대해 ---> Using cache 메시지가 나타납니다.

예를 들어 애플리케이션 코드만 수정하고 requirements.txt는 그대로라면, pip install 단계는 캐시를 사용하여 건너뜁니다. 덕분에 몇 분 걸리던 빌드가 몇 초로 단축됩니다.

--build-arg 옵션은 빌드 시점에 변수를 전달합니다. Dockerfile에서 ARG VERSION을 선언하고, 빌드할 때 --build-arg VERSION=1.0.0으로 값을 넘겨줄 수 있습니다.

환경에 따라 다른 값을 사용해야 할 때 유용합니다. --no-cache 옵션은 모든 캐시를 무시하고 처음부터 다시 빌드합니다.

외부 패키지의 최신 버전을 받아야 하거나, 캐시 문제로 빌드가 이상하게 될 때 사용합니다. 실제 현업에서는 어떻게 활용할까요?

실제 프로젝트에서는 개발, 스테이징, 프로덕션 환경별로 다른 이미지를 빌드하는 경우가 많습니다. 개발 환경에는 디버깅 도구를 포함하고, 프로덕션에는 최소한의 패키지만 포함하는 식입니다.

CI/CD 파이프라인에서는 코드가 커밋될 때마다 자동으로 이미지를 빌드하고 테스트합니다. 빌드 시간을 줄이기 위해 레이어 순서를 최적화하고, 멀티스테이지 빌드를 활용합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 빌드 컨텍스트를 너무 크게 잡는 것입니다.

현재 디렉토리에 수 GB의 파일이 있으면 빌드 시작 전에 모든 파일을 Docker 데몬으로 전송하느라 시간이 오래 걸립니다. .dockerignore 파일로 불필요한 파일을 제외하면 빌드 속도가 크게 개선됩니다.

또 다른 실수는 태그를 지정하지 않는 것입니다. 태그 없이 빌드하면 latest라는 기본 태그가 붙는데, 여러 버전을 관리할 때 혼란스러워집니다.

명시적으로 버전 번호를 태그로 사용하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

첫 빌드에 성공한 김개발 씨는 뿌듯해합니다. "제가 만든 애플리케이션이 이미지가 되다니, 이제 어디서든 실행할 수 있겠네요!" docker build를 제대로 활용하면 개발부터 배포까지의 과정이 자동화되고, 팀 전체가 동일한 환경을 공유할 수 있습니다.

실전 팁

💡 - 빌드 시간 단축을 위해 자주 변경되지 않는 레이어를 앞에 배치하세요

  • .dockerignore를 활용하여 빌드 컨텍스트 크기를 최소화하세요
  • 이미지 태그는 시맨틱 버저닝(1.0.0, 1.0.1)을 따르면 관리가 쉽습니다

5. 컨테이너 실행과 관리

이미지를 성공적으로 빌드한 김개발 씨는 이제 컨테이너를 실행하고 관리하는 방법을 배우고 싶어졌습니다. "이미지는 만들었는데, 실제로 어떻게 사용하나요?" 박시니어 씨가 답합니다.

"컨테이너를 실행하고, 로그를 확인하고, 필요하면 내부에 접속해서 디버깅하는 방법을 알려드릴게요."

컨테이너 실행과 관리는 docker run으로 시작하여 로그 확인, 내부 접속, 리소스 제한, 재시작 정책 설정 등을 포함합니다. 컨테이너는 프로세스처럼 시작, 중지, 재시작할 수 있으며, 볼륨을 마운트하여 데이터를 영구적으로 보존하고, 환경 변수로 설정을 주입할 수 있습니다.

다음 코드를 살펴봅시다.

# 컨테이너 실행과 관리 명령어

# 1. 기본 실행 (백그라운드, 포트 매핑, 이름 지정)
docker run -d -p 5000:5000 --name my-app my-app:1.0

# 2. 환경 변수 전달
docker run -d -e DATABASE_URL=postgres://db:5432 my-app:1.0

# 3. 볼륨 마운트 (데이터 영구 보존)
docker run -d -v /host/data:/app/data my-app:1.0

# 4. 컨테이너 로그 확인 (실시간 팔로우)
docker logs -f my-app

# 5. 실행 중인 컨테이너 내부 접속
docker exec -it my-app /bin/bash

# 6. 컨테이너 재시작
docker restart my-app

# 7. 리소스 제한 (CPU, 메모리)
docker run -d --memory="512m" --cpus="1.0" my-app:1.0

김개발 씨는 이미지를 만드는 데는 성공했지만, 막상 실행하려니 막막합니다. "단순히 docker run만 하면 되는 건가요?

데이터베이스 연결 정보는 어떻게 전달하죠?" 박시니어 씨가 웃으며 답합니다. "컨테이너를 제대로 실행하려면 여러 옵션을 알아야 해요.

하나씩 같이 배워봅시다." 그렇다면 컨테이너 실행과 관리란 정확히 무엇일까요? 쉽게 비유하자면, 컨테이너 실행은 마치 앱을 실행하는 것과 같습니다.

하지만 단순히 더블 클릭하는 것이 아니라, 어떤 포트로 열지, 어떤 데이터 폴더를 사용할지, 얼마나 메모리를 할당할지 등을 설정할 수 있습니다. 스마트폰 앱의 설정 메뉴를 열어 권한과 옵션을 조정하는 것과 비슷합니다.

컨테이너를 제대로 관리하지 않으면 어떤 문제가 생길까요? 많은 초보 개발자가 컨테이너를 실행만 하고 관리하지 않습니다.

에러가 발생했는데 로그를 확인하는 방법을 몰라 막막해합니다. 데이터베이스 컨테이너를 재시작했더니 데이터가 모두 사라져 당황합니다.

여러 컨테이너가 같은 포트를 사용하려다 충돌이 발생합니다. 메모리 제한 없이 실행했다가 서버 전체가 다운됩니다.

바로 이런 문제를 예방하기 위해 체계적인 컨테이너 관리가 필요합니다. 환경 변수로 설정을 주입하면 코드를 수정하지 않고도 다양한 환경에서 실행할 수 있습니다.

-e 옵션으로 DATABASE_URL, API_KEY 같은 값을 전달합니다. 보안이 중요한 정보는 --env-file 옵션으로 파일에서 읽어올 수도 있습니다.

볼륨 마운트는 컨테이너가 삭제되어도 데이터를 보존하는 핵심 메커니즘입니다. -v /host/data:/app/data처럼 호스트의 디렉토리를 컨테이너 내부 디렉토리에 연결합니다.

데이터베이스 파일, 업로드된 이미지, 로그 파일 등은 반드시 볼륨에 저장해야 합니다. 포트 매핑은 -p 옵션으로 설정합니다.

-p 8080:5000은 호스트의 8080 포트로 들어온 요청을 컨테이너의 5000 포트로 전달합니다. 같은 호스트에서 여러 애플리케이션을 실행할 때 포트를 다르게 지정하면 충돌을 피할 수 있습니다.

위의 명령어들을 실제로 사용해보겠습니다. docker run -d -p 5000:5000 --name my-app my-app:1.0을 실행하면 백그라운드에서 컨테이너가 시작됩니다.

-d는 detached 모드로, 터미널을 점유하지 않고 백그라운드에서 실행됩니다. --name으로 지정한 이름으로 컨테이너를 식별할 수 있습니다.

docker logs -f my-app은 실시간으로 로그를 확인합니다. -f 옵션은 tail -f처럼 새로운 로그가 추가되면 계속 출력합니다.

애플리케이션이 제대로 시작되는지, 에러는 없는지 확인할 때 매우 유용합니다. docker exec -it my-app /bin/bash는 실행 중인 컨테이너 내부에 접속합니다.

-it는 interactive terminal의 약자로, 터미널 세션을 열어줍니다. 컨테이너 안에서 파일을 확인하거나, 명령어를 실행하거나, 디버깅할 때 사용합니다.

리소스 제한은 운영 환경에서 매우 중요합니다. --memory="512m"는 컨테이너가 최대 512MB의 메모리만 사용하도록 제한합니다.

--cpus="1.0"은 CPU 1코어만 사용하도록 제한합니다. 이렇게 하면 한 컨테이너가 서버 전체 리소스를 독점하는 것을 방지할 수 있습니다.

재시작 정책은 --restart 옵션으로 설정합니다. --restart=always는 컨테이너가 중지되면 자동으로 재시작합니다.

--restart=on-failure는 에러로 종료된 경우만 재시작합니다. 프로덕션 환경에서는 재시작 정책을 반드시 설정해야 합니다.

실제 현업에서는 어떻게 활용할까요? 실제 서비스 운영에서는 여러 컨테이너를 조합하여 사용합니다.

예를 들어 웹 애플리케이션 컨테이너, 데이터베이스 컨테이너, Redis 컨테이너를 각각 실행하고 네트워크로 연결합니다. docker network create my-network로 사용자 정의 네트워크를 만들고, --network my-network 옵션으로 컨테이너를 연결하면 컨테이너 이름으로 통신할 수 있습니다.

모니터링도 중요합니다. docker stats 명령어로 실시간 리소스 사용량을 확인할 수 있습니다.

CPU, 메모리, 네트워크 I/O를 보며 병목 지점을 파악합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 중요한 데이터를 볼륨 없이 컨테이너 내부에 저장하는 것입니다. 컨테이너를 삭제하면 데이터도 함께 사라집니다.

데이터베이스, 파일 업로드, 로그 등은 반드시 볼륨으로 관리해야 합니다. 또 다른 실수는 환경 변수에 비밀번호를 직접 노출하는 것입니다.

docker ps나 docker inspect로 환경 변수를 볼 수 있기 때문에 보안에 취약합니다. Docker Secrets나 환경 변수 파일을 암호화하는 방법을 사용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 여러 옵션을 배운 김개발 씨는 자신감이 생겼습니다.

"이제 컨테이너를 제대로 관리할 수 있을 것 같아요!" 컨테이너 실행과 관리를 제대로 익히면 안정적인 서비스 운영이 가능해집니다. 문제가 생겨도 빠르게 원인을 파악하고 해결할 수 있습니다.

실전 팁

💡 - 프로덕션 환경에서는 반드시 --restart 정책을 설정하세요

  • 중요한 데이터는 Named Volume을 사용하여 관리하면 더 안전합니다
  • docker logs --tail 100으로 최근 100줄만 확인하면 로그가 많을 때 유용합니다

6. 멀티스테이지 빌드

컨테이너를 운영하던 김개발 씨는 이미지 크기가 너무 크다는 것을 발견했습니다. "1GB가 넘는 이미지를 매번 배포하니 시간이 너무 오래 걸려요." 박시니어 씨가 새로운 기법을 소개합니다.

"멀티스테이지 빌드를 사용하면 이미지 크기를 크게 줄일 수 있어요. 빌드에 필요한 도구와 실행에 필요한 파일을 분리하는 거죠."

멀티스테이지 빌드는 하나의 Dockerfile에서 여러 FROM 문을 사용하여 빌드 단계와 실행 단계를 분리하는 기법입니다. 빌드 단계에서 컴파일러, 개발 도구 등을 사용하고, 최종 단계에는 실행에 필요한 파일만 복사하여 이미지 크기를 극적으로 줄입니다.

Go, Java, Node.js 같은 컴파일 언어나 빌드 과정이 있는 프로젝트에 특히 효과적입니다.

다음 코드를 살펴봅시다.

# 멀티스테이지 빌드 Dockerfile (Node.js 예제)

# 1단계: 빌드 스테이지
FROM node:18 AS builder

WORKDIR /app

# 의존성 설치
COPY package*.json ./
RUN npm ci --only=production

# 소스 코드 복사 및 빌드
COPY . .
RUN npm run build

# 2단계: 실행 스테이지
FROM node:18-alpine

WORKDIR /app

# 빌드 스테이지에서 필요한 파일만 복사
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# 실행
EXPOSE 3000
CMD ["node", "dist/index.js"]

김개발 씨는 프로젝트가 커지면서 이미지 크기가 계속 증가하는 것을 발견했습니다. 처음에는 200MB였던 이미지가 어느새 1.2GB가 되었습니다.

배포할 때마다 업로드와 다운로드에 긴 시간이 걸립니다. "이미지가 왜 이렇게 큰 거죠?" 김개발 씨가 당황해하자, 박시니어 씨가 이미지를 분석해봅니다.

"빌드 도구, 개발 의존성, 소스 코드가 전부 이미지에 포함되어 있네요. 실행에는 필요 없는 것들인데 말이죠." 그렇다면 멀티스테이지 빌드란 정확히 무엇일까요?

쉽게 비유하자면, 멀티스테이지 빌드는 마치 건축 현장과 같습니다. 건물을 지을 때는 크레인, 비계, 각종 공구가 필요합니다.

하지만 건물이 완성되면 이런 것들은 모두 치웁니다. 입주자에게는 완성된 건물만 필요하기 때문입니다.

멀티스테이지 빌드도 마찬가지로 빌드 도구는 빌드 단계에서만 사용하고, 최종 이미지에는 실행 파일만 포함시킵니다. 단일 스테이지로 빌드하면 어떤 문제가 생길까요?

빌드에 필요한 컴파일러, 빌드 도구, 개발 의존성이 모두 최종 이미지에 포함됩니다. Node.js 프로젝트라면 node_modules의 devDependencies까지 모두 들어갑니다.

Python 프로젝트라면 pip, setuptools, gcc 같은 빌드 도구가 남습니다. 이미지가 불필요하게 커지고, 공격 표면도 넓어져 보안에도 취약해집니다.

바로 이런 문제를 해결하기 위해 멀티스테이지 빌드가 등장했습니다. 멀티스테이지 빌드를 사용하면 이미지 크기가 극적으로 줄어듭니다.

1GB가 넘던 이미지가 100MB 미만으로 줄어드는 경우도 흔합니다. 또한 보안이 강화됩니다.

불필요한 도구와 라이브러리가 없어 공격 벡터가 줄어듭니다. 무엇보다 배포 속도가 빨라집니다.

작은 이미지는 레지스트리에 빠르게 업로드되고, 서버에서도 빠르게 다운로드됩니다. 위의 Dockerfile을 단계별로 살펴보겠습니다.

첫 번째 FROM node:18 AS builder에서 AS builder는 이 스테이지에 이름을 붙입니다. 이 단계는 빌드 전용으로, 전체 Node.js 이미지를 사용합니다.

빌드에 필요한 모든 도구가 포함되어 있습니다. npm ci로 의존성을 설치하고, npm run build로 TypeScript를 컴파일하거나 Webpack으로 번들링합니다.

이 과정에서 devDependencies도 설치되고, 빌드 도구도 실행됩니다. 이 단계의 이미지는 매우 크지만, 최종 이미지에 포함되지 않으므로 문제없습니다.

두 번째 FROM node:18-alpine에서 새로운 베이스 이미지를 시작합니다. alpine은 경량 리눅스 배포판으로, 전체 Node.js 이미지보다 훨씬 작습니다.

이 단계가 최종 이미지가 됩니다. COPY --from=builder가 핵심입니다.

첫 번째 스테이지(builder)에서 빌드된 결과물만 복사합니다. dist 폴더의 컴파일된 파일과 운영 의존성만 가져오고, 소스 코드나 빌드 도구는 복사하지 않습니다.

Go 언어의 경우 더 극적인 효과를 볼 수 있습니다. 빌드 스테이지에서 Go 컴파일러로 바이너리를 만들고, 최종 스테이지는 scratch(완전히 빈 이미지)에서 시작하여 바이너리만 복사하면 됩니다.

수백 MB에서 수 MB로 줄어듭니다. 실제 현업에서는 어떻게 활용할까요?

마이크로서비스 아키텍처에서는 수십 개의 서비스가 각각 컨테이너로 실행됩니다. 각 이미지가 1GB라면 전체 시스템이 수십 GB가 되지만, 멀티스테이지 빌드로 각각 100MB로 줄이면 수 GB로 압축됩니다.

Kubernetes 클러스터에서 새 노드가 추가될 때 이미지를 다운로드하는 시간이 크게 단축됩니다. CI/CD 파이프라인에서도 효과적입니다.

작은 이미지는 레지스트리에 빠르게 푸시되고, 배포 서버에서도 빠르게 풀됩니다. 배포 시간이 5분에서 1분으로 줄어드는 경우도 있습니다.

캐싱 최적화도 가능합니다. 첫 번째 스테이지에서 의존성 설치와 소스 코드 복사를 분리하면, 소스 코드만 변경되어도 의존성 설치는 캐시를 사용합니다.

빌드 속도가 더욱 빨라집니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 운영에 필요한 파일을 복사하지 않는 것입니다. 예를 들어 Node.js 프로젝트에서 production 의존성을 복사하지 않으면 런타임 에러가 발생합니다.

어떤 파일이 실행에 필요한지 정확히 파악해야 합니다. 또 다른 실수는 alpine 이미지 사용 시 호환성 문제입니다.

alpine은 musl libc를 사용하는데, 일부 네이티브 모듈이 glibc에 의존하는 경우 동작하지 않습니다. 이런 경우 slim 이미지를 사용하거나 alpine에 필요한 라이브러리를 추가 설치해야 합니다.

디버깅도 어려워질 수 있습니다. 최종 이미지에 쉘이나 디버깅 도구가 없는 경우가 많습니다.

필요하다면 개발용 Dockerfile과 프로덕션용 Dockerfile을 분리하는 것도 방법입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

멀티스테이지 빌드를 적용한 김개발 씨는 감탄합니다. "이미지가 1.2GB에서 150MB로 줄었어요!

배포가 정말 빨라졌습니다!" 멀티스테이지 빌드를 제대로 활용하면 이미지 크기, 보안, 배포 속도 모두 개선됩니다. 프로덕션 환경에서는 필수적인 기법입니다.

실전 팁

💡 - 빌드 스테이지는 풍부한 도구가 있는 이미지를, 실행 스테이지는 경량 이미지를 사용하세요

  • --from=builder로 필요한 파일만 선택적으로 복사하여 불필요한 파일이 포함되지 않도록 하세요
  • 여러 스테이지를 사용하여 테스트, 린트, 빌드, 실행을 분리할 수도 있습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Docker#Container#Dockerfile#MultiStage#DevOps#AWS

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Helm 마이크로서비스 패키징 완벽 가이드

Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.

쿠버네티스 아키텍처 완벽 가이드

초급 개발자를 위한 쿠버네티스 아키텍처 설명서입니다. 클러스터 구조부터 Control Plane, Worker Node, 파드, 네트워킹까지 실무 관점에서 쉽게 풀어냅니다. 점프 투 자바 스타일로 술술 읽히는 이북 형식으로 작성되었습니다.

EFK 스택 로깅 완벽 가이드

마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.

Spring Boot 상품 서비스 구축 완벽 가이드

실무 RESTful API 설계부터 테스트, 배포까지 Spring Boot로 상품 서비스를 만드는 전 과정을 다룹니다. JPA 엔티티 설계, OpenAPI 문서화, Docker Compose 배포 전략을 초급 개발자도 쉽게 따라할 수 있도록 스토리텔링으로 풀어냅니다.

Docker로 컨테이너화 완벽 가이드

Spring Boot 애플리케이션을 Docker로 컨테이너화하는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 중심으로 설명합니다. Dockerfile 작성부터 멀티스테이지 빌드, 이미지 최적화, Spring Boot의 Buildpacks까지 다룹니다.