Docker
💡 애플리케이션을 쉽게 만들고, 테스트하고, 배포할 수 있게 도와주는 소프트웨어 플랫폼으로 애플리케이션을 컨테이너라는 가볍고 이식성 있는 패키지로 실행할 수 있다.
주요 특징
- 컨테이너화: 애플리케이션과 필요한 모든 것을 하나의 패키지로 묶어 어디서든 실행할 수 있다.
- 경량: Docker는 운영 체제의 커널을 공유하므로, 가상 머신보다 훨씬 가볍고 빠르게 실행된다.
- 이식성: 다양한 환경에서 동일한 실행 환경을 보장한다. Docker 컨테이너는 어디서든 동일하게 실행된다. 예를 들어, 개발자의 로컬 컴퓨터에서 테스트 서버, 운영 서버까지 동일하게 동작한다.
- 확장성: Docker를 사용하면 여러 개의 컨테이너를 효율적으로 관리하고 쉽게 확장할 수 있다.
주요 용어 및 개념
- 이미지(Image): 애플리케이션과 모든 실행에 필요한 파일을 포함한 읽기 전용 정적 템플릿
- 객체 지향적으로 비유하면 클래스라 할 수 있다.
- 이미지에는 코드, 런타임, 라이브러리, 환경 변수, 구성 파일 등이 포함된다.
- 컨테이너(Container): 이미지를 실행하여 동작하는 애플리케이션 인스턴스
- 객체 지향적으로 비유하면 객체라 할 수 있다.
- 하나의 이미지로 여러 개의 컨테이너를 독립적으로 생성할 수 있다.
- 컨테이너는 격리된 공간에서 실행되기 때문에 각각의 컨테이너가 다른 컨테이너에 영향을 받지 않는다.
- 도커 파일(Dockerfile): Docker 이미지를 생성하기 위한 명령어가 담긴 스크립트 파일, 이미지를 빌드하는 데 필요한 명령어들을 포함한다.
- 도커 허브(Docker Hub): 이미지를 저장하고 공유하는 중앙 저장소
- 자신이 만든 이미지를 도커 허브에 push하거나, 다른 사람이 만든 이미지를 pull해서 가져와 쓸 수 있다.
- 볼륨(Volume): 컨테이너 데이터를 지속적으로 저장하는 메커니즘
- 도커 컨테이너 내부의 데이터(디렉토리, 파일 등)를 로컬에 연동하여 저장할 수 있다. 도커 컨테이너가 삭제되어도 로컬에 저장된 데이터는 유지된다.
- 네트워크(network): 컨테이너 간의 통신을 관리하는 방식
- Default는 브리지 네트워크 (명시하지 않았을 때)
Bridge Network (브리지 네트워크)
- 기본적으로 Docker가 컨테이너를 실행할 때 사용하는 네트워크로 단일 호스트에서 여러 컨테이너를 연결할 때 사용된다.
- 동일한 브리지 네트워크에 연결된 컨테이너들은 서로 통신할 수 있다.
- 외부 네트워크와는 NAT(Network Address Translation)를 통해 통신한다.
💡 NAT(Network Address Translation)
내부 네트워크의 여러 장치가 하나의 공용 IP 주소를 통해 외부 네트워크와 통신할 수 있도록 IP 주소를 변환하는 기술
docker network create my-bridge-network
docker run -d --name container1 --network my-bridge-network nginx
docker run -d --name container2 --network my-bridge-network nginx
Host Network (호스트 네트워크)
- 컨테이너가 호스트의 네트워크 스택을 직접 사용한다.
- 네트워크 격리가 없기 때문에 성능상 이점이 있지만, 보안 및 네트워크 충돌 위험이 있다.
- 일반적으로 성능이 중요한 애플리케이션에 사용된다.
docker run -d --network host nginx
Overlay Network (오버레이 네트워크)
- 여러 Docker 호스트에 걸쳐 있는 컨테이너를 연결할 때 사용한다.
- Swarm 모드나 Kubernetes 같은 오케스트레이션 도구와 함께 사용된다.
- 데이터 센터 또는 클라우드 환경에서 분산 시스템을 구축할 때
💡 스웜(Swarm) 모드
Docker 컨테이너의 오케스트레이션과 클러스터링을 지원하여 여러 호스트에서 컨테이너를 관리하고 배포할 수 있는 기능
Docker vs Virtual Machine

도커(Docker)는 애플리케이션만 실행하고, 운영 체제의 커널을 공유하므로 가상 머신보다 훨씬 빠르게 시작할 수 있다.
+ 도커 컨테이너는 어디서든 동일하게 실행된다.
하지만, 도커 컨테이너는 동일한 운영 체제 커널을 공유하기 때문에 가상 머신보다 보안 수준이 낮다.
도커는 리눅스 커널을 사용하여 동작하므로, 리눅스 OS에서 가장 잘 동작한다.
즉, 윈도우나 Mac 같은 운영체제에서는 호환성 문제가 일부 있을 수 있다.
리눅스 커널을 에뮬레이션 하는 방식으로 작동해야 하기 때문에 어느 정도 성능 저하가 있을 수 있다.
가상 머신(VM)은 하이퍼바이저를 통해 물리적 하드웨어 위에 가상화된 운영 체제를 실행하는 기술이다.
하이퍼바이저는 여러 운영 체제를 동시에 실행할 수 있도록 물리적 하드웨어를 가상화하는 소프트웨어이다.
도커의 단점을 장점으로 가진다고 할 수 있다.
호스트의 OS를 공유하는 도커와 달리 가상 머신으로 같은 컴퓨터에서 윈도우, 리눅스, 맥OS를 동시에 실행할 수 있다.
또한 도커의 장점을 단점으로 가진다.
오버헤드가 크고, 부팅 시간이 느리다. 가상 머신은 전체 운영 체제를 실행해야 하기 때문에, 많은 메모리(RAM)와 CPU 자원을 소비한다. 이는 컴퓨터의 성능을 저하시킬 수 있다.
항상 도커가 옳다고는 할 수 없다. 그렇다면 언제 도커를 사용해야 할까?
일관된 개발 환경이 필요할 때
- 개발, 테스트, 운영 환경이 다를 때 발생하는 문제를 피하고자 할 때 Docker를 사용한다. Docker를 사용하면 모든 환경에서 동일한 컨테이너를 실행할 수 있어, 환경 차이로 인한 문제를 줄일 수 있다.
애플리케이션을 빠르게 배포하고 싶을 때
- Docker를 사용하면 애플리케이션을 빠르고 쉽게 배포할 수 있다. Docker 이미지를 빌드하고 이를 컨테이너로 실행하면, 필요한 모든 구성 요소가 포함되어 있어 별도의 설치 과정 없이 바로 실행할 수 있다.
MSA를 도입할 때
- Docker는 마이크로서비스 아키텍처와 잘 맞는다. 각 서비스가 독립적으로 배포되고 실행될 수 있어, 여러 개의 컨테이너를 통해 다양한 서비스를 쉽게 관리할 수 있다.
CI/CD 파이프라인을 구축할 때
- Docker는 코드를 변경할 때마다 자동으로 빌드, 테스트, 배포할 수 있도록 설정할 수 있어, 개발 주기를 단축하고 배포의 신뢰성을 높일 수 있다.
리소스 효율성을 높이고 싶을 때
- Docker 컨테이너는 운영 체제의 커널을 공유하므로 가상 머신보다 적은 리소스를 사용한다. 즉, 더 많은 애플리케이션을 동일한 하드웨어에서 실행할 수 있다.
애플리케이션 격리가 필요할 때
- 여러 애플리케이션을 독립적으로 실행하고자 할 때 Docker를 사용하면 각 컨테이너가 서로 격리되어 실행된다. 이를 통해 애플리케이션 간의 충돌을 방지하고 보안을 강화할 수 있다.
쉽게 스케일링하고 싶을 때
- Docker를 사용하면 컨테이너 기반의 애플리케이션을 쉽게 확장할 수 있다. 필요한 만큼 컨테이너를 추가하여 수평 확장이 가능하며, 오케스트레이션 도구와 결합하여 오토 스케일링도 가능하다.
쿠버네티스(Kubernetes)와 함께 사용하고자 할 때
- 쿠버네티스는 컨테이너 오케스트레이션 도구로, 다수의 Docker 컨테이너를 관리하고 자동 배포, 확장, 운영을 지원한다. Docker 컨테이너를 쿠버네티스 클러스터에 배포하면, 애플리케이션의 가용성과 확장성을 높일 수 있다.
자주 쓰이는 명령어 모음
이미지 관련 명령어
docker build -t [이미지명:버전] .
# Dockerfile을 기반으로 이미지를 생성한다.
# -t 옵션을 사용하여 이미지의 이름과 태그를 입력 할 수 있다.
# . 은 Dockerfile의 위치
docker pull [이미지명]
# 도커 허브에서 해당 이미지를 가져옴
docker images
# 로컬에 저장된 도커 이미지 목록 보기
docker rmi [이미지명:버전]
# 도커 이미지 삭제
컨테이너 관련 명령어
컨테이너 아이디(container_id)는 모두 작성할 필요없다. 식별 가능한 자릿수까지만 입력해도 된다.
docker run -d -p 8080:80 myapp:latest # myapp:latest 이미지를 사용하여 컨테이너를 실행
# -d: 백그라운드에서 실행
# -p: 호스트의 8080 포트를 컨테이너의 80 포트에 매핑
docker exec -it [container_id] /bin/bash # 컨테이너 내부 접속
# -i (interactive): 컨테이너의 표준 입력(STDIN)을 연다. 컨테이너 내부에서 사용자 입력을 받을 수 있다.
# -t (tty): 가상 터미널을 할당, 컨테이너 내부에서 터미널을 사용할 수 있다.
docker ps # 현재 실행중인 도커 컨테이너 확인
docker ps -a # 모든 컨테이너 목록 확인
docker ps -al # 마지막으로 실행된 컨테이너를 먼저 나열
docker start [container_id] # 컨테이너 시작
docker stop [container_id] # 컨테이너 중지
docker rm [-f] [container_id] # 컨테이너 삭제
도커 네트워크 및 볼륨 관련 명령어
docker network create mynetwork # 도커 네트워크 생성
docker network ls # 도커 네트워크 목록 확인
docker network rm mynetwork # 도커 네트워크 삭제
docker volume create myvolume # 도커 볼륨 생성
docker volume ls # 도커 볼륨 목록 확인
docker volume rm myvolume # 볼륨 삭제
도커 리소스 정리 명령어
docker container prune
docker volume prune
docker image prune
docker network prune
docker system prune -a
도커 컨테이너 실행 예시
# postgres 컨테이너
docker run -d --name postgres-sample \
-p 5433:5432 \
-e POSTGRES_USER=admin1 \
-e POSTGRES_PASSWORD=admin2 \
-e PGDATA=/var/lib/postgresql/data/pgdata \
-v ${로컬_바인딩_폴더}:/var/lib/postgresql/data:z \
postgres
## M1 mac 기준 mysql 컨테이너
docker run --platform linux/amd64 \
--name mysql-exam -d \
-p 3307:3306 \
-e MYSQL_ROOT_PASSWORD=1234 \
-e MYSQL_DATABASE=exam \
mysql:8.0.35
\ 뒤에 공백이 존재하면 안된다.
-e → 환경변수를 설정해주는 옵션
:z → SELinux(Secure Enhanced Linux) 환경에서 사용되는 옵션으로 해당 볼륨이 여러 컨테이너에서 공유될 수 있음을 나타낸다. 컨테이너가 해당 디렉토리에 읽기 및 쓰기 권한을 갖도록 한다.
-p →호스트 포트(로컬):도커 포트
💡 호스트의 포트는 한정적이나, 도커 내부의 포트는 여러 개의 어플리케이션이 각각의 컨테이너로 존재할 때, 컨테이너가 분리되어 있기 때문에 겹쳐도 상관 없다.

Docker compose
두 개의 스프링 부트 프로젝트(com.service.a, com.service.b)를 만든다.


com.service.a > application.properties
spring.application.name=service-a
server.port=8080
service.b.url=http://service-b:8080
com.service.a에서 OpenFeign 라이브러리를 사용하여 com.service.b의 api를 호출할 것이다.
com.service.b > application.properties
spring.application.name=service-b
server.port=8080
각각의 컨테이너로 독립적으로 실행될 것이라 포트는 8080으로 겹쳐도 상관없다.
com.service.a > AApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@SpringBootApplication
public class AApplication {
public static void main(String[] args) {
SpringApplication.run(AApplication.class, args);
}
}
com.service.a > AController.java
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class AController {
private final BServiceClient bServiceClient;
@GetMapping("/hi")
public String hello() {
String hello = bServiceClient.test();
return "sevice-a: hi ###### service-b: " + hello;
}
}
com.service.a > BServiceClient.java
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "service-b", url = "${service.b.url}")
public interface BServiceClient {
@GetMapping("/test")
public String test();
}
com.service.b > BController.java
package com.service.b;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BController {
@GetMapping("/test")
public String test() {
return "B service test";
}
}
도커 파일 작성(com.service.a와 com.service.b 각각 하위에 작성)
FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
도커 이미지 생성
하기 전에 ./gradlew clean bootJar 로 빌드 먼저 하자. 혹은 ./gradlew clean build
docker build -t img-service-b . # .은 Dockerfile의 위치
docker build -t img-service-a .
도커 컨테이너는 같은 네트워크에 있으면 도커 컨테이너의 이름으로 호출할 수 있다.
docker network create my-network
도커 컨테이너 생성
docker run -d --name service-b \
--network my-network \
-p 18081:8080 \
img-service-b
docker run -d --name service-a \
--network my-network \
-p 18080:8080 \
img-service-a
지금까지는 각각의 패키지 안에 도커 파일을 만들어서 실행했다면, 이를 루트 디렉토리에 docker-compose.yml 파일을 작성해서 모든 컨테이너를 한 번에 관리할 수 있다.
docker-compose.yml
version: '3.8'
services:
service-a:
image: img-service-a
ports:
- "18080:8080"
environment:
- SERVICE_B_URL=http://service-b:8080
depends_on:
- service-b
service-b:
image: img-service-b
ports:
- "18081:8080"
networks:
default:
driver: bridge
💡 docker compose를 사용하여 서비스를 실행하면, 기본적으로 새 브리지 네트워크를 생성하여 각 서비스 컨테이너를 해당 네트워크로 엮어준다. 이 네트워크는 docker-compose.yml 파일에 정의된 모든 서비스가 서로 통신 가능하게 한다.

도커 컴포즈 명령어
# 변경을 감지해 새로운 이미지 빌드 후 실행
docker compose up --build -d # 옛날 버전은 docker-compose로 사용해야 한다.
docker compose down
가끔식 docker compose build를 수행하지 않으면 변경사항이 반영되지 않은 채 compose up이 되는 경우가 있었다. 따라서 원하는 결과가 나오지 않는다면 빌드 후 도커 컴포즈 명령어를 수행하자.
(컴퓨터공학 종합 설계2 할 때 이거 때문에 삽질 좀 했음)

두 애플리케이션을 실행하고 http://localhost:18080/hi로 접속하여 메시지를 확인한다.