포스트

[기초편] Docker

[기초편] Docker
시리즈 [Docker] 개념정리 및 실습 3편
  1. [기초편] Docker
  2. [기본편] Docker
  3. [심화편] Docker

Docker가 왜 필요할까?

“내 컴퓨터에서는 되는데요” 문제

개발을 하다 보면 한 번쯤 이런 상황을 겪게 된다.

로컬에서는 잘 됐는데, 서버에 올리니까 왜 안되지?

이런 문제는 진짜 흔한데, 막상 원인을 찾으려면 시간을 꽤 잡아먹는다. 나도 실제로 운영 환경과 내 로컬 환경의 MySQL 버전이 달라서 하루를 날린적이 있다. 이 문제의 대부분은 결국 환경 차이 때문이다.


환경이 다르면 결과도 달라진다

예를 들어 팀 상황이 이렇다고 해보자.

항목개발자 A 로컬개발자 B 로컬운영 서버
OSmacOS 14Windows 11Ubuntu 22.04
Java211711
MySQL8.08.05.7

코드는 똑같은데, 실행되는 환경이 다르면 결과가 달라지는건 당연하다.

  • 어떤 사람은 잘되고
  • 어떤 사람은 안되고
  • 서버에서는 또 다르게 터진다

그래서 나온 접근 방식

여기서 나온 생각이 이거다.

“그럼 실행 환경 자체를 통째로 묶어서 가져다 쓰면 되지 않을까?”

이걸 가능하게 해주는게 Docker 같은 컨테이너 기술이다.

  • 코드만 공유하는게 아니라
  • 실행 환경까지 같이 공유하는 방식

그래서 누가 어디서 실행하든 결과가 동일하게 나온다.


가상머신(VM) vs 컨테이너

“환경을 통째로 묶는다는 건 알겠는데, 기존에도 이런 방식이 없었나?”

사실 이미 비슷한 접근 방식이 있었다. 바로 가상머신(VM)이다.
VM도 Docker처럼 실행 환경을 통째로 만들어서 사용하는 기술이다.

VM 구조

1
2
3
4
5
6
7
8
9
┌──────────────────────────────────┐
│           Host OS                │
├──────────────────────────────────┤
│          Hypervisor              │
├──────────┬──────────┬────────────┤
│  Guest   │  Guest   │  Guest     │
│   OS     │   OS     │   OS       │
│  (App A) │  (App B) │  (App C)   │
└──────────┴──────────┴────────────┘

VM은 하이퍼바이저 위에 OS 전체를 올려서 격리된 환경을 만든다.
VM은 격리는 완벽하지만 문제가 있다.

  • Guest OS를 통째로 올리기 때문에 수 GB의 용량이 필요하다.
  • 부팅하는 데 수 분이 걸린다.
  • 하나의 서버에 올릴 수 있는 VM 수가 제한적이다.

반면 Docker가 사용하는 컨테이너는 다른 방식으로 접근한다.

컨테이너는 쉽게 말하면 “OS를 새로 띄우지 않고, 프로세스만 격리해서 실행하는 방식”이다.

Docker 구조

1
2
3
4
5
6
7
8
┌──────────────────────────────────┐
│           Host OS                │
├──────────────────────────────────┤
│        Docker Engine             │
├──────────┬──────────┬────────────┤
│Container │Container │Container   │
│  (App A) │  (App B) │  (App C)   │
└──────────┴──────────┴────────────┘

컨테이너는 Guest OS를 따로 두지 않는다. 대신 Host OS의 커널을 공유하면서 프로세스 수준의 격리를 제공한다.

비교 항목가상머신(VM)컨테이너
격리 수준OS 수준 (강함)프로세스 수준
시작 시간수 분수 초 이내
이미지 크기수 GB수 MB ~ 수백 MB
리소스 사용높음낮음
이식성낮음높음

VM이 더 안전하긴 한데, 대신 많이 무겁다.
컨테이너는 격리를 조금 덜어낸 대신 훨씬 가볍고 빠르다.
그리고 실제로 써보면, 이정도 격리로도 대부분 문제 없다.
그래서 요즘은 컨테이너를 더 많이 쓴다.


Docker의 특징

1. 이식성

Docker 이미지는 애플리케이션 실행에 필요한 모든 것(코드, 런타임, 라이브러리, 환경 변수)을 담고있다.
이 이미지만 있으면 어떤 환경에서든 동일하게 실행된다.

1
2
개발자 노트북 → Docker 이미지 → 테스트 서버 → 운영 서버
                  (동일한 이미지로 어디서든 동일하게 동작)

2. 격리

각 컨테이너는 독립된 환경에서 실행된다.
하나의 서버에서 Java 11을 쓰는 서비스와 Java 21을 쓰는 서비스를 충돌 없이 동시에 실행할 수 있다.

1
2
3
4
5
[컨테이너 A]          [컨테이너 B]
 Java 11               Java 21
 MySQL 5.7             MySQL 8.0
 Port 8080             Port 8081
      ↕ 격리된 환경, 서로 영향 없음 ↕

3. 재현성

Dockerfile이라는 설정 파일 하나로 환경을 코드로 관리한다.
이 파일만 있으면 언제든, 누구든 동일한 환경을 재현할 수 있다.

1
2
3
4
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY build/libs/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

이 세 가지 특성 덕분에 Docker는 “내 컴퓨터에서는 되는데요” 문제를 근본적으로 해결한다.


Docker를 제대로 쓰려면 알아야 하는 핵심 개념 3가지

Docker가 좋은건 알겠는데, 막상 써보려고 하면 이런 생각이 든다.

“그래서 Image? Container? Registry? 이건 또 뭐야..”

처음 보면 용어가 좀 헷갈리는데, 사실 구조는 생각보다 단순하다.

Image: 컨테이너 설계도

Image는 한마디로 이거다.

“실행에 필요한걸 미리 다 만들어놓은 완성된 패키지”

보통 애플리케이션을 실행하려면

  • Java 설치
  • 라이브러리 맞추고
  • 환경 변수 설정하고

이걸 다 해줘야 한다.

Image는 이 과정을 한 번에 끝내놓은 결과물이다.

  • 어떤 OS를 쓸지
  • 어떤 라이브러리를 설치할지
  • 어떤 애플리케이션을 실행할지 이런 정보들이 전부 들어 있다.

Image는 위에서 봤던 Dockerfile로 만든다.

1
Dockerfile -> Image -> COntainer

참고로 Image는 한 번 만들면 바뀌지 않는다.(Read-Only)

Container: 실제로 실행된 상태

Container는 Image를 실행한 결과다.

“설계도를 실제로 돌린 상태”

Image가 준비된 상태라면, Container는 그걸 실제로 실행 중인 프로세스다. 그리고 하나의 Image로 여러 개를 띄울 수 있다.

1
Image 1개 -> Container 여러 개

Registry: Image 저장소

Registry는 Image를 저장하고 공유하는 곳이다.

“빌드한 Image를 올려두고, 어디서든 가져다 쓸 수 있는 저장소”

Maven Repository에서 의존성을 가져오는 것처럼, Registry에서 Image를 가져다 쓴다.

가장 많이 쓰는 건 Docker Hub로, 공식 Image들이 다 여기 있다.

1
2
docker pull mysql:8.0      # Docker Hub에서 mysql 이미지 가져오기
docker pull openjdk:21     # Docker Hub에서 openjdk 이미지 가져오기

정리하면 이렇다.

1
2
3
4
5
Dockerfile  →  docker build  →  Image  →  docker push  →  Registry
                                  ↓
                             docker run
                                  ↓
                             Container

자주 쓰는 명령어 실습

가장 쉽게 시작할 수 있는 MySQL 컨테이너를 띄워보면서 명령어를 익혀보자.

MySQL 컨테이너 띄우기

1
2
3
4
5
6
docker run -d \
  --name my-mysql \
  -e MYSQL_ROOT_PASSWORD=1234 \
  -e MYSQL_DATABASE=testdb \
  -p 3306:3306 \
  mysql:8.0

옵션이 좀 있는데, 하나씩 보면 어렵지 않다.

옵션설명
-d백그라운드로 실행 (detach)
--name my-mysql컨테이너 이름 지정
-e환경 변수 설정
-p 3306:3306호스트 포트:컨테이너 포트 연결
mysql:8.0사용할 이미지 이름과 태그

-p 3306:3306 이 부분이 헷갈릴 수 있는데, 컨테이너는 기본적으로 외부와 격리되어 있다.
포트를 열어줘야 내 로컬에서 localhost:3306 으로 접근할 수 있다.

포트 매핑이 내부적으로 어떻게 동작하는지 궁금하다면 -> 포트 매핑 동작 원리

컨테이너 상태 확인

1
2
3
4
5
# 실행 중인 컨테이너 목록
docker ps

# 전체 컨테이너 목록 (종료된 것 포함)
docker ps -a

출력 결과가 이렇게 나오면 정상이다.

1
2
CONTAINER ID   IMAGE       COMMAND   CREATED         STATUS         PORTS                    NAMES
a1b2c3d4e5f6   mysql:8.0   ...       5 seconds ago   Up 4 seconds   0.0.0.0:3306->3306/tcp   my-mysql

컨테이너 안으로 들어가기

1
docker exec -it my-mysql bash

-it 는 인터랙티브 모드로, 컨테이너 안의 터미널을 직접 사용할 수 있게 해준다. 들어가서 mysql 접속도 해볼 수 있다.

1
mysql -u root -p  # 비밀번호: 1234

로그 확인

1
2
3
4
docker logs my-mysql

# 실시간으로 로그 보기
docker logs -f my-mysql

컨테이너 정지 / 삭제

1
2
3
4
5
6
7
8
# 컨테이너 정지
docker stop my-mysql

# 컨테이너 삭제
docker rm my-mysql

# 한 번에 정지 + 삭제
docker rm -f my-mysql

Image 관련 명령어

1
2
3
4
5
# 로컬에 있는 이미지 목록
docker images

# 이미지 삭제
docker rmi mysql:8.0

Dockerfile로 Spring Boot 앱 이미지 만들기

MySQL을 그냥 가져다 쓴 것처럼, 내가 만든 Spring Boot 앱도 Image로 만들 수 있다.
이 때 사용하는 게 Dockerfile이다.

Spring Boot 프로젝트 준비

먼저 jar 파일을 빌드한다.

1
./gradlew bootJar

build/libs/ 아래에 .jar 파일이 생긴다.

Dockerfile 작성

프로젝트 루트에 Dockerfile을 만든다.

1
2
3
4
5
6
7
8
9
FROM openjdk:21-jdk-slim

WORKDIR /app

COPY build/libs/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

각 명령어가 하는 일을 보면 이렇다.

명령어설명
FROM베이스 이미지 지정. openjdk:21이 깔린 환경에서 시작
WORKDIR컨테이너 안의 작업 디렉토리 설정
COPY로컬 파일을 컨테이너 안으로 복사
EXPOSE컨테이너가 사용할 포트 명시 (문서용)
ENTRYPOINT컨테이너 시작 시 실행할 명령어

Image 빌드

1
docker build -t my-spring-app:1.0 .
  • -t my-spring-app:1.0 : 이미지 이름과 태그 지정
  • . : 현재 디렉토리의 Dockerfile 사용

빌드가 완료되면 이미지 목록에서 확인할 수 있다.

1
2
3
docker images
# REPOSITORY       TAG   IMAGE ID       CREATED          SIZE
# my-spring-app    1.0   abc123def456   10 seconds ago   450MB

Spring Boot 컨테이너 실행

1
2
3
4
docker run -d \
  --name my-app \
  -p 8080:8080 \
  my-spring-app:1.0

localhost:8080 으로 접근하면 앱이 실행되고 있는 것을 확인할 수 있다.

1
2
# 로그로 정상 구동 확인
docker logs -f my-app

레이어 캐시: Dockerfile 작성 시 알아두면 좋은 것

Dockerfile을 작성할 때 딱 한 가지만 더 알아두면 좋다.

Docker는 Image를 빌드할 때 각 명령어 단위로 레이어를 만들고 캐시해둔다.
그래서 다음 빌드 때 변경이 없는 레이어는 캐시를 그대로 재사용한다.

문제는 중간에 있는 레이어가 바뀌면, 그 이후 레이어는 전부 캐시가 무효화된다는 점이다.

비효율적인 Dockerfile

1
2
3
4
5
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY . .                        # 소스 전체 복사 (코드 한 줄만 바뀌어도 이 이하 전부 재실행)
RUN ./gradlew bootJar
ENTRYPOINT ["java", "-jar", "build/libs/app.jar"]

소스 코드 한 줄만 고쳐도 COPY . . 부터 전부 다시 실행된다.

개선된 Dockerfile

현실적으로 Spring Boot 앱은 Dockerfile 안에서 빌드하지 않고, 미리 빌드한 jar를 COPY하는 방식을 더 많이 쓴다.

1
2
3
4
5
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY build/libs/*.jar app.jar   # 완성된 jar만 복사
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

이렇게 하면 jar 파일이 바뀐 경우에만 레이어가 무효화되고, 그 전 단계는 캐시를 재사용한다.


정리

Docker 기초에서 핵심만 뽑으면 이렇다.

개념한 줄 요약
Image실행 환경을 담은 패키지. 한 번 만들면 불변(Read-Only)
ContainerImage를 실행한 프로세스
RegistryImage를 저장/공유하는 저장소 (Docker Hub)
DockerfileImage를 만드는 설정 파일

다음 편에서는 여러 컨테이너를 한 번에 관리하는 Docker Compose와, 컨테이너끼리 통신하는 네트워크 개념을 다룰 예정이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.