무엇을 배우나요?
- Docker
- Docker를 통해 애플리케이션을 컨테이너로 만들어서 일관되게 실행할 수 있습니다
- Dockerfile을 작성하고, Docker 이미지를 만들고 배포할 수 있습니다.
- Docker-compose
- Docker-compose로 여러 개의 컨테이너를 쉽게 관리할 수 있습니다
- 서비스 간의 연결 및 데이터 관리를 배우게 됩니다
- CI/CD 파이프라인 구축
- CI/CD의 기본 개념을 이해하고, 자동으로 빌드, 테스트, 배포하는 과정을 배울 수 있습니다.
- GitLab을 사용하여 CI/CD 파이프라인을 설정할 수 있습니다.
- AWS Elastic Container Service 사용
- AWS ECS를 통해 컨테이너 애플리케이션을 배포하고 관리할 수 있습니다
🗨️ Docker
Docker 개요
Docker란?
- Docker
- Docker는 애플리케이션을 쉽게 만들고, 테스트하고, 배포할 수 있게 도와주는 소프트웨어 플랫폼입니다.
- 애플리케이션을 컨테이너라는 가볍고 이식성 있는 패키지로 실행할 수 있습니다.
- Docker 이미지는 애플리케이션을 실행하는 데 필요한 모든 것(코드, 런타임, 시스템 도구, 시스템 라이브러리 등)을 포함합니다.
- 주요 특징
- 컨테이너화: 애플리케이션과 필요한 모든 것을 하나의 패키지로 묶어 어디서든 실행할 수 있습니다.
- 경량: Docker는 운영 체제의 커널을 공유하므로, 가상 머신보다 훨씬 가볍고 빠르게 실행됩니다.
- 이식성: Docker 컨테이너는 어디서든 동일하게 실행됩니다. 예를 들어, 개발자의 컴퓨터에서 테스트 서버, 운영 서버까지 동일하게 동작합니다.
- 확장성: Docker를 사용하면 여러 개의 컨테이너를 효율적으로 관리하고 쉽게 확장할 수 있습니다.
도커 이전에는 서버 환경마다 자바, 그레이들, 데이터베이스 등의 버전과 환경에 맞게 직접 설치, 설정해야 했으나, 도커 덕분에 복잡한 의존성 관리 및 설치 과정이 없어지고, 커맨드 몇 줄로 모든 것이 해결되어 관리와 배포가 매우 간편해졌습니다.
도커를 사용하면 서로 다른 환경이나 버전 문제가 사라지고, 의존성 관리 및 배포, 확장에 따른 실수도 크게 줄일 수 있습니다.
💡 주요 키워드
- 키워드 정리
- 이미지: 애플리케이션과 모든 실행에 필요한 파일을 포함한 읽기 전용 템플릿
- 컨테이너: 이미지를 실행하여 동작하는 애플리케이션 인스턴스
- Dockerfile: 이미지를 생성하기 위한 명령어가 담긴 스크립트 파일
- Docker Hub: 이미지를 저장하고 공유하는 중앙 저장소
- 볼륨: 컨테이너 데이터를 지속적으로 저장하는 메커니즘
- 네트워크: 컨테이너 간의 통신을 관리하는 방식
- 이미지 (Image)
- Docker 이미지는 애플리케이션과 그 실행에 필요한 모든 것을 포함하는 읽기 전용 템플릿입니다. 이미지에는 코드, 런타임, 라이브러리, 환경 변수, 구성 파일 등이 포함됩니다.
- 이미지는 컨테이너를 생성하기 위한 청사진 역할을 합니다.
- 컨테이너 (Container)
- 컨테이너는 Docker 이미지를 실행한 상태입니다. 이미지가 정적인 템플릿이라면, 컨테이너는 실제로 애플리케이션이 실행되는 동적인 환경입니다.
- 컨테이너는 격리된 공간에서 애플리케이션을 실행하며, 필요한 모든 의존성을 포함합니다.
- 하나의 시스템에서 여러 개의 컨테이너를 독립적으로 실행할 수 있습니다.
- Dockerfile
- Dockerfile은 Docker 이미지를 생성하기 위한 스크립트입니다. 이 파일에는 이미지를 빌드하는 데 필요한 명령어들이 포함되어 있습니다.
- Dockerfile을 사용하면 이미지 생성 과정을 자동화하고 일관되게 만들 수 있습니다.
- Docker Hub
- Docker Hub는 Docker 이미지를 공유하고 저장하는 중앙 저장소입니다. 사용자는 Docker Hub에서 다양한 공개 이미지를 다운로드하거나 자신만의 이미지를 업로드할 수 있습니다.
- 볼륨 (Volume)
- 볼륨은 컨테이너의 데이터를 지속적으로 저장할 수 있는 메커니즘입니다. 컨테이너가 삭제되더라도 볼륨에 저장된 데이터는 유지됩니다.
- 볼륨을 사용하면 데이터를 컨테이너와 독립적으로 관리할 수 있습니다.
- 네트워크 (Network)
- Docker 네트워크는 컨테이너 간의 통신을 관리하는 방식입니다. Docker는 여러 가지 네트워크 드라이버를 제공하여 다양한 네트워크 설정을 지원합니다.
- 기본적으로 모든 컨테이너는 브리지 네트워크를 통해 통신할 수 있습니다.
📎네트워크 종류
브리지 네트워크만 잘 알아두기!
- Bridge Network (브리지 네트워크)
- 기본적으로 Docker가 컨테이너를 실행할 때 사용하는 네트워크입니다.
- 동일한 브리지 네트워크에 연결된 컨테이너들은 서로 통신할 수 있습니다.
- 외부 네트워크와는 NAT (내부 네트워크의 여러 장치가 하나의 공용 IP 주소를 통해 외부 네트워크와 통신할 수 있도록 IP 주소를 변환하는 기술) 를 통해 통신합니다.
- 일반적으로 단일 호스트에서 여러 컨테이너를 연결할 때 사용됩니다.
- 명시하지 않으면 모두 브리지 네트워크에서 실행됩니다
# Docker에서 사용자 정의 브리지 네트워크 생성
# 같은 네트워크에 연결된 컨테이너끼리 이름(container name)으로 통신 가능
docker network create my-bridge-network
# nginx 이미지를 사용해서 container1 이라는 이름의 컨테이너를 백그라운드(-d)로 실행
# nginx는 가볍고 많이 사용하는 웹 서버 프로그램
# --network 옵션으로 위에서 만든 my-bridge-network에 연결
docker run -d --name container1 --network my-bridge-network nginx
# nginx 이미지를 사용해서 container2 컨테이너 실행
# container1과 같은 브리지 네트워크에 연결되므로 서로 통신 가능
# 예: container1 내부에서 ping container2 가능
docker run -d --name container2 --network my-bridge-network nginx
- Host Network (호스트 네트워크)
- 컨테이너가 호스트의 네트워크 스택을 직접 사용합니다.
- 네트워크 격리가 없기 때문에 성능상 이점이 있지만, 보안 및 네트워크 충돌 위험이 있습니다.
- 일반적으로 성능이 중요한 애플리케이션에 사용됩니다.
= 컨테이너가 호스트 컴퓨터의 네트워크를 그대로 같이 사용
= 빠르지만 서로 포트 충돌이나 보안 문제가 생길 수 있음
예) 내 PC의 8080 포트를 컨테이너도 그대로 사용
docker run -d --network host nginx
- Overlay Network (오버레이 네트워크)
- 여러 Docker 호스트에 걸쳐 있는 컨테이너를 연결할 때 사용됩니다.
- Swarm 모드(Docker 컨테이너의 오케스트레이션과 클러스터링을 지원하여 여러 호스트에서 컨테이너를 관리하고 배포할 수 있는 기능)나 Kubernetes 같은 오케스트레이션 도구와 함께 사용됩니다.
- 데이터 센터 또는 클라우드 환경에서 분산 시스템을 구축할 때 유용합니다.
= 서로 다른 서버(호스트)에 있는 Docker 컨테이너끼리 연결하는 네트워크
= 여러 대 서버를 하나처럼 연결할 때 사용
예) 서울 서버의 컨테이너 ↔ 부산 서버의 컨테이너 통신
Docker와 가상 머신의 비교
- Docker의 장점과 단점
- 장점
- 빠른 시작 시간과 낮은 오버헤드: Docker 컨테이너는 애플리케이션만 실행하고, 운영 체제의 핵심 부분은 공유하므로, 가상 머신보다 훨씬 빠르게 시작할 수 있습니다.
- 높은 이식성과 확장성: Docker 컨테이너는 한 번 만들면 어디서든지 동일하게 실행됩니다. 예를 들어, 개발 환경, 테스트 환경, 실제 운영 환경 모두에서 같은 방식으로 동작합니다. 여러 컨테이너를 쉽게 추가하고 관리할 수 있어, 필요에 따라 애플리케이션을 확장하기 쉽습니다.
- 단점
- 보안 격리가 가상 머신보다 약함: Docker 컨테이너는 동일한 운영 체제 커널을 공유하기 때문에, 가상 머신보다 보안 격리 수준이 낮습니다. 하나의 컨테이너에서 보안 문제가 발생하면, 같은 커널을 공유하는 다른 컨테이너에도 영향을 줄 가능성이 있습니다.
- 운영 체제 종속성 존재: Docker는 리눅스 커널을 사용하여 작동하므로, 리눅스 운영 체제에서 가장 잘 동작합니다. 윈도우나 맥 같은 다른 운영 체제에서는 호환성 문제가 있을 수 있고, 리눅스 커널을 에뮬레이션하는 방식으로 작동해야 하기 때문에 성능이 저하될 수 있습니다.
- 장점
- 가상 머신(VM)의 장점과 단점
- 정의
- 가상 머신(VM)은 하이퍼바이저를 통해 물리적 하드웨어 위에 가상화된 운영 체제를 실행하는 기술입니다. 하이퍼바이저는 여러 운영 체제를 동시에 실행할 수 있도록 물리적 하드웨어를 가상화하는 소프트웨어입니다.
- 장점
- 격리된 환경 제공: 각 가상 머신은 완전히 독립된 운영 체제를 실행하므로, 하나의 가상 머신에서 문제가 발생해도 다른 가상 머신이나 호스트 시스템에 영향을 주지 않습니다.
- 다양한 운영 체제 실행 가능: 가상 머신을 사용하면 한 물리적 서버에서 여러 종류의 운영 체제를 동시에 실행할 수 있습니다. 예를 들어, 같은 컴퓨터에서 윈도우, 리눅스, 맥OS를 동시에 실행할 수 있습니다. 이를 통해 개발자나 테스트 팀은 다양한 환경을 쉽게 구축하고 사용할 수 있습니다.
- 단점
- 오버헤드가 크고, 느린 부팅 시간: 가상 머신은 전체 운영 체제를 실행해야 하기 때문에, 많은 메모리(RAM)와 CPU 자원을 소비합니다. 가상 머신을 부팅할 때 운영 체제를 처음부터 시작해야 해서 시간이 오래 걸립니다. 이는 실제 하드웨어에서 컴퓨터를 켜는 것과 비슷합니다.
- 높은 리소스 소비: 가상 머신은 각기 독립된 운영 체제를 포함하므로, 여러 가상 머신을 실행하면 컴퓨터 자원을 많이 소모하게 됩니다. 예를 들어, 두 개의 가상 머신을 실행하면, 두 운영 체제를 모두 실행해야 하므로 메모리와 CPU 사용량이 크게 증가합니다. 이는 컴퓨터의 성능을 저하시킬 수 있습니다.
- 정의
Docker는 언제 사용할까?
- 일관된 개발 환경이 필요할 때
- 개발, 테스트, 운영 환경이 다를 때 발생하는 문제를 피하고자 할 때 Docker를 사용하면 좋습니다. Docker를 사용하면 모든 환경에서 동일한 컨테이너를 실행할 수 있어, 환경 차이로 인한 문제를 줄일 수 있습니다
- 애플리케이션을 빠르게 배포하고 싶을 때
- Docker를 사용하면 애플리케이션을 빠르고 쉽게 배포할 수 있습니다. Docker 이미지를 빌드하고 이를 컨테이너로 실행하면, 필요한 모든 구성 요소가 포함되어 있어 별도의 설치 과정 없이 바로 실행할 수 있습니다.
- 마이크로서비스 아키텍처를 도입할 때
- Docker는 마이크로서비스 아키텍처와 잘 맞습니다. 각 서비스가 독립적으로 배포되고 실행될 수 있어, 여러 개의 컨테이너를 통해 다양한 서비스를 쉽게 관리할 수 있습니다
- CI/CD 파이프라인을 구축할 때
- Docker는 CI/CD 파이프라인에 적합합니다. 코드를 변경할 때마다 자동으로 빌드, 테스트, 배포할 수 있도록 설정할 수 있어, 개발 주기를 단축하고 배포의 신뢰성을 높일 수 있습니다.
- 리소스 효율성을 높이고 싶을 때
- Docker 컨테이너는 가상 머신보다 적은 리소스를 사용합니다. 운영 체제의 커널을 공유하므로, 더 많은 애플리케이션을 동일한 하드웨어에서 실행할 수 있습니다.
- 애플리케이션 격리가 필요할 때:
- 여러 애플리케이션을 독립적으로 실행하고자 할 때 Docker를 사용하면 각 컨테이너가 서로 격리되어 실행됩니다. 이를 통해 애플리케이션 간의 충돌을 방지하고 보안을 강화할 수 있습니다.
- 쉽게 스케일링하고 싶을 때:
- Docker를 사용하면 컨테이너 기반의 애플리케이션을 쉽게 확장할 수 있습니다. 필요한 만큼 컨테이너를 추가하여 수평 확장이 가능하며, 오케스트레이션 도구와 결합하여 자동 확장도 가능합니다.
- 쿠버네티스(Kubernetes)와 함께 사용하고자 할 때
- Docker는 쿠버네티스와 함께 사용하기에 매우 적합합니다. 쿠버네티스는 컨테이너 오케스트레이션 도구로, 다수의 Docker 컨테이너를 관리하고 자동 배포, 확장, 운영을 지원합니다. Docker 컨테이너를 쿠버네티스 클러스터에 배포하면, 애플리케이션의 가용성과 확장성을 높일 수 있습니다.
Docker 명령어
📎이미지 관련 명령어
- 이미지 빌드 (Build)
- 현재 디렉토리의 Dockerfile을 기반으로 myapp이라는 이름의 이미지를 생성합니다. -t 옵션을 사용하여 이미지의 이름과 태그를 입력할 수 있습니다.
docker build -t myapp:latest .
- 이미지 가져오기
- 도커 허브에서 해당 이미지를 가져옴
docker pull postgres
- 이미지 목록 보기 (List Images)
- 현재 로컬에 저장된 Docker 이미지를 목록으로 표시합니다.
docker images
- 이미지 삭제
- myapp:latest 이미지를 로컬 저장소에서 삭제합니다.
docker rmi myapp:latest
📎 Docker 컨테이너 관련 명령어
📌 컨테이너 아이디는 모두 작성할 필요없이 식별 가능한 자릿수까지만 입력하세요
🐳 도커 포트 매핑 (Port Mapping) 핵심 요약
- 도커의 포트 매핑은 호스트(내 컴퓨터)와 컨테이너 내부를 연결하는 과정입니다.
- 컨테이너 포트 (8080): 실제 서버(Spring Boot 등)가 실행되고 있는 독립적인 내부 위치
- 호스트 포트 (18080): 외부(사용자)에서 해당 컨테이너로 접속하기 위해 열어두는 입구
docker run -p 호스트포트:컨테이너포트 예시: docker run -p 18080:8080
- 외부에서 localhost:18080으로 요청을 보내면, 도커가 이를 컨테이너 내부의 8080 포트로 전달해 주는 원리입니다.
💡 꼭 알아야 할 '포트 중복' 규칙
- 컨테이너는 서로 완전히 격리된 독립 공간입니다. 따라서 내부(컨테이너) 포트는 여러 컨테이너가 똑같이 8080을 사용해도 전혀 문제가 없습니다.
- 하지만 호스트 포트는 하나의 컴퓨터에서 공유되는 입구이므로 절대 중복해서 사용할 수 없습니다.
- ⭕ 정상 (호스트 포트 다름): 18080:8080 / 18081:8080
- ❌ 불가 (호스트 포트 충돌): 18080:8080 / 18080:8080
- 컨테이너 실행 (Run)
- myapp:latest 이미지를 사용하여 컨테이너를 실행합니다. -d 옵션은 백그라운드에서 실행되도록 하고, -p 옵션은 호스트의 8080 포트를 컨테이너의 80 포트에 매핑합니다.
- -d (detached mode): 컨테이너를 백그라운드에서 실행합니다. 이 옵션을 사용하면 터미널을 컨테이너에 붙잡히지 않고, 컨테이너가 백그라운드에서 계속 실행됩니다.
docker run -d -p 8080:80 myapp:latest
- 컨테이너 내부 접속
- -i (interactive): 컨테이너의 표준 입력(STDIN)을 열어둡니다. 이 옵션을 사용하면 컨테이너 내부에서 사용자 입력을 받을 수 있습니다.
- -t (tty): 가상 터미널을 할당합니다. 이 옵션을 사용하면 컨테이너 내부에서 터미널을 사용할 수 있습니다.
암기하기!
docker exec -it 컨테이너_아이디 /bin/bash
# docker → 도커 명령어 실행
# exec → 실행 중인 컨테이너 안에서 명령 실행
# -it → 터미널 입력 가능(-i) + 터미널 화면 연결(-t)
# 컨테이너ID → 접속할 대상 컨테이너
# /bin/bash → 컨테이너 내부에서 bash 쉘 실행 (리눅스 터미널 열기)
- 실행 중인 컨테이너 목록 보기 (List Running Containers)
- 현재 실행 중인 컨테이너의 목록을 표시합니다.
docker ps
- 모든 컨테이너 목록 보기 (List All Containers)
docker ps -a # 중지된 컨테이너를 포함한 모든 컨테이너의 목록을 표시합니다.
docker ps -al # 마지막으로 실행된 컨테이너를 가장 먼저 나열.
- 컨테이너 중지 (Stop)
- 지정된 container_id를 가진 컨테이너를 중지합니다.
docker stop container_id
- 컨테이너 시작 (Start)
- 중지된 컨테이너를 다시 시작합니다.
docker start container_id
- 컨테이너 삭제 (Remove Container)
- 지정된 container_id를 가진 컨테이너를 삭제합니다.
docker rm 컨테이너_아이디
📎 Docker 네트워크 및 볼륨 관련 명령어
- 네트워크 생성 (Create Network)
- mynetwork이라는 이름의 네트워크를 생성합니다.
docker network create mynetwork
- 네트워크 목록 보기 (List Networks)
- 현재 설정된 Docker 네트워크의 목록을 표시합니다.
docker network ls
- 네트워크 삭제 (Remove Network)
- mynetwork이라는 이름의 네트워크를 삭제합니다.
docker nework rm mynetwork
- 볼륨 생성 (Create Volume)
- myvolume이라는 이름의 볼륨을 생성합니다.
docker volume create myvolume
- 볼륨 목록 보기 (List Volumes)
- 현재 설정된 Docker 볼륨의 목록을 표시합니다.
docker volume ls
- 볼륨 삭제 (Remove Volume)
- myvolume이라는 이름의 볼륨을 삭제합니다.
docker volume rm myvolume
간단한 실습
📌 도커를 통해 PostgreSQL 컨테이너 2개를 실행해보겠습니다.
이미지 받기
docker pull postgres
# docker pull → Docker Hub에서 이미지 다운로드
# postgres → PostgreSQL 공식 이미지 이름
# 이미지(Image) → 프로그램 + 실행환경을 묶어둔 실행 템플릿
컨테이너 실행하기
- :z 옵션은 SELinux(Secure Enhanced Linux) 환경에서 사용되는 파일 시스템 옵션입니다.
- 이 옵션은 Docker 컨테이너가 호스트 파일 시스템의 특정 디렉토리에 접근할 수 있도록 SELinux 컨텍스트를 설정합니다. SELinux는 Linux 시스템의 보안을 강화하기 위해 파일 및 프로세스에 대한 권한을 세밀하게 제어합니다.
- :z 옵션은 해당 볼륨이 여러 컨테이너에서 공유될 수 있음을 나타내며, 이를 통해 컨테이너가 해당 디렉토리에 읽기 및 쓰기 권한을 갖도록 합니다.
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
# 새로운 컨테이너를 백그라운드(-d)로 실행하고, 컨테이너 이름 지정
docker run -d --name postgres-sample \
# 호스트 5433 ↔ 컨테이너 내부 5432 포트 연결 (외부는 5433으로 접속)
-p 5433:5432 \
# 환경변수(-e): PostgreSQL 계정(username) 설정
-e POSTGRES_USER=admin1 \
# 환경변수(-e): PostgreSQL 비밀번호 설정
-e POSTGRES_PASSWORD=admin2 \
# 환경변수(-e): PostgreSQL 데이터가 저장되는 세부 위치 지정
-e PGDATA=/var/lib/postgresql/data/pgdata \
# 볼륨 마운트(-v): 로컬-컨테이너 폴더 연결하여 데이터 유지. (:z는 SELinux 권한 허용)
-v ${로컬_바인딩_폴더}:/var/lib/postgresql/data:z \
# 실행할 PostgreSQL 공식 이미지 지정
postgres
💾 볼륨 마운트(Volume Mount)와 :z 옵션의 이해
- 볼륨 마운트 (-v)란? 도커 컨테이너는 종료되거나 삭제되면 그 안의 데이터도 모두 날아갑니다. 데이터베이스(DB)의 데이터가 날아가면 안 되기 때문에, 내 컴퓨터(호스트)의 특정 폴더와 컨테이너 내부의 폴더를 서로 연결(동기화)하는 작업입니다. 이렇게 하면 컨테이너를 지워도 데이터는 내 컴퓨터에 안전하게 보존됩니다.
- SELinux와 :z 옵션이란? SELinux는 리눅스(특히 CentOS, RHEL 등)의 강력한 보안 시스템입니다. 이 시스템은 기본적으로 컨테이너가 내 컴퓨터의 폴더에 접근하는 것을 차단합니다. 마운트 경로 맨 끝에 :z를 붙여주면, 도커가 해당 폴더를 정상적으로 읽고 쓸 수 있도록 SELinux의 접근 권한을 알맞게 허용(레이블 조정)해 주는 역할을 합니다. 즉, 권한 거부(Permission Denied) 에러를 막기 위한 필수 옵션입니다.
만약 맥에서 실행시 Permission denied 관련 에러가 난다면 다음의 내용을 참고하세요
- Rancher desktop을 사용할 경우
- Rancher desktop 윈도우에서 설정을 들어갑니다.
- Virtual Machine > Volumes 로 들어가 Mount Type을 9p 로 변경합니다.
- Security Model을 Mapped-xattr 로 변경합니다.
- Docker desktop의 경우
- Docker desktop 윈도우에서 설정에 들어갑니다.
- General 탭에서 virtoFS을 oxsfs(Legacy)로 수정합니다.
🗨️ Docker Compose
Docker Compose 개요
Docker Compose란?
- 정의:
- Docker Compose는 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구입니다.
- docker-compose.yml 파일 하나로 애플리케이션의 서비스, 네트워크, 볼륨 등을 정의할 수 있습니다.
- 여러 Docker 컨테이너를 한 번에 설정하고 실행하는 도구
- DB, 서버, Redis 같은 여러 컨테이너를 한 파일로 관리하고 한 번에 실행하는 도구\
Docker Compose 설치
- Docker 20.10부터는 Docker Compose가 기본적으로 설치됩니다. 따라서 별도로 설치할 필요가 없습니다.
- 이전 버전은 중간에 “-” 가 필요하지만 기본 설치 부터는 “ ”으로 대체 되었습니다.
docker-compose
docker compose
Docker Compose 파일 구조
- docker-compose.yml:
- Docker Compose 파일은 YAML 형식으로 작성되며, 애플리케이션의 서비스, 네트워크, 볼륨 등을 정의합니다.
version: '3'
services:
web:
image: nginx
ports:
- "8080:80"
app:
build: .
ports:
- "8081:8080"
depends_on:
- db
db:
image: postgres
environment:
POSTGRES_PASSWORD: example
- version: Docker Compose 파일의 버전을 지정합니다.
- services: 애플리케이션의 각 서비스를 정의합니다.
- web, app, db: 각각의 서비스 이름입니다.
- image: 서비스를 실행할 Docker 이미지를 지정합니다.
- build: Dockerfile이 있는 디렉토리 경로를 지정하여 이미지를 빌드합니다.
- ports: 호스트와 컨테이너 간의 포트를 매핑합니다.
- depends_on: 다른 서비스가 먼저 실행되어야 하는 순서를 지정합니다.
- environment: 컨테이너의 환경 변수를 설정합니다.
Docker Compose 사용법
docker-compose.yml 파일 작성
- 예제 docker-compose.yml 파일:
version: '3'
services:
web:
image: nginx
ports:
- "8080:80"
app:
build: .
ports:
- "8081:8080"
depends_on:
- db
db:
image: postgres
environment:
POSTGRES_PASSWORD: example
Docker Compose 명령어
- docker compose up:
- docker-compose.yml 파일에 정의된 서비스를 빌드하고 시작합니다. 만약 이미 빌드된 이미지가 있다면 이를 사용합니다.
- 백그라운드에서 실행하려면 d 옵션을 추가합니다.
docker compose up -d
docker compose -f /path/to/your/project/docker-compose.yml up
- docker compose down:
- 실행 중인 모든 서비스를 중지하고 컨테이너, 네트워크, 볼륨 등을 정리합니다.
docker compose down
- docker compose build:
- docker-compose.yml 파일에 정의된 서비스를 빌드합니다.
docker compose build
- docker compose ps:
- 현재 실행 중인 서비스를 확인합니다.
docker compose ps
- docker compose logs:
- 실행 중인 서비스의 로그를 확인합니다.
docker compose logs
이러한 방식으로 Docker Compose를 사용하면 여러 컨테이너로 구성된 애플리케이션을 쉽게 관리하고 배포할 수 있습니다. docker-compose.yml 파일 하나로 모든 서비스의 설정을 관리할 수 있어 일관된 환경을 제공할 수 있습니다.
참고: Docker Compose는 Docker 버전 20.10부터 기본적으로 포함되어 있으므로 별도의 설치가 필요하지 않습니다 (Docker) (Docker Documentation).
실습 예제
- 1) docker-compose.yml 파일 작성
version: '3'
services:
web:
image: nginx
ports:
- "8080:80"
app:
build: .
ports:
- "8081:8080"
depends_on:
- db
db:
image: postgres
environment:
POSTGRES_PASSWORD: example
- 2) Dockerfile 작성
- app 서비스의 이미지를 빌드하기 위한 Dockerfile을 작성합니다.
# 1. 빈 컨테이너에 'Java 17'이 미리 설치된 리눅스 환경(템플릿)을 가져옵니다.
# 우리가 직접 리눅스를 깔고 Java를 설치할 필요 없이, 세팅이 끝난 버전을 사용하는 것입니다.
FROM eclipse-temurin:17-jre
# 2. 내 컴퓨터에서 빌드된 완성본(target/myapp.jar)을 컨테이너 내부의 '/app' 폴더로 복사합니다.
COPY target/myapp.jar /app/myapp.jar
# 3. 지금부터 터미널 작업 위치를 컨테이너 안의 '/app' 폴더로 이동합니다. (리눅스의 'cd /app'과 같음)
WORKDIR /app
# 4. 컨테이너가 켜질 때 자동으로 실행할 명령어를 지정합니다.
# 터미널에 'java -jar myapp.jar'를 입력하여 Spring Boot 서버를 켜는 것과 똑같은 동작입니다.
ENTRYPOINT ["java", "-jar", "myapp.jar"]
FROM openjdk:11-jre-slim
강의에서 사용하는 openjdk:11은 deprecated 되었습니다.
아래와 같이 FROM eclipse-temurin:17-jre로 진행해주세요~!
(주의)
- Java 17 이상에서 실행 가능
- Spring Boot 3 이상에서 사용
- 3) Docker Compose로 서비스 시작
- docker-compose.yml 파일이 있는 디렉토리에서 다음 명령어를 실행하여 서비스를 시작합니다.
docker compose up -d
🗨️ 애플리케이션 생성
- 스프링 프로젝트 2개 생성 (https://start.spring.io/)


🔗 Feign Client & EnableFeignClients 핵심 정리
- Feign은 다른 서버의 API를 호출할 때 HTTP 요청 코드를 직접 작성하지 않고 메서드처럼 호출하기 위한 방식이다.
- 사용할 때는 먼저 인터페이스를 만들고, 여기에 FeignClient 애노테이션을 붙여 어떤 서버를 호출할지 지정한다.
- 인터페이스 안의 메서드에는 호출할 API 경로를 매핑해주면, 해당 메서드가 실제 HTTP 요청과 연결된다.
- 이렇게 정의한 인터페이스는 그대로는 동작하지 않기 때문에, 애플리케이션 시작 클래스에 EnableFeignClients를 붙여 Feign을 활성화해야 한다.
- EnableFeignClients는 FeignClient가 붙은 인터페이스를 찾아서 Spring이 사용할 수 있는 객체로 만들어주는 역할을 한다.
- 이후에는 해당 인터페이스를 주입받아 일반 메서드처럼 호출하면 내부적으로 HTTP 통신이 실행된다.
- 정리하면 FeignClient는 “어떤 API를 호출할지 정의”, EnableFeignClients는 “그걸 실제로 동작하게 만드는 설정”이다.
🌊 마이크로서비스 간 통신 (Feign) 전체 흐름 요약
- 상황: Service A가 Service B의 데이터를 가져와서 사용자에게 보여줘야 하는 상황
- 사용자 요청 진입: 외부(브라우저 등)에서 Service A의 컨트롤러(@GetMapping("/hi"))로 요청이 들어옵니다.
- 메서드 호출 (Feign 작동): Service A는 직접 복잡한 통신 코드를 짜는 대신, 주입받은 BServiceClient 인터페이스의 getHello() 메서드를 그냥 툭 호출합니다.
- 스프링의 자동 통신 (마법의 순간): 스프링 프레임워크가 Feign 인터페이스에 적힌 설정(@GetMapping("/hello")와 타겟 URL)을 보고, 알아서 HTTP GET 요청을 만들어 Service B로 발사합니다.
- 타겟 서버 응답: Service B의 컨트롤러가 해당 요청(/hello)을 받아 처리한 후 "hello"라는 결과값을 Service A로 돌려줍니다.
- 최종 결과 반환: Service A는 받아온 "hello" 데이터를 가공하여 최종적으로 사용자에게 응답을 내려줍니다.
- 보내는 쪽(Service A의 Feign 인터페이스)과 받는 쪽(Service B의 컨트롤러)의 HTTP 메서드와 URL 경로(예: @GetMapping("/hello"))는 무조건 똑같아야 합니다. 하나라도 다르면 404 에러가 발생하며 통신이 끊어집니다.
- 보내는 쪽 (Service A의 FeignClient):
- @GetMapping("/hello")
👉 "나 타겟 서버의 /hello라는 주소로 GET 요청을 쏠게!"
- @GetMapping("/hello")
- 받는 쪽 (Service B의 Controller):
- @GetMapping("/hello")
👉 "나 /hello라는 주소로 들어오는 GET 요청을 받아서 기다릴게!"
- @GetMapping("/hello")
- 보내는 쪽 (Service A의 FeignClient):
service-a (요청을 보내는 서버 - Port: 18080)
AApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
// [핵심] 프로젝트 내의 @FeignClient 어노테이션을 찾아 통신 구현체를 생성하도록 활성화합니다.
@EnableFeignClients
@SpringBootApplication
public class AApplication {
public static void main(String[] args) {
SpringApplication.run(AApplication.class, args);
}
}
AController.java
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class AController {
// [핵심] 스프링이 자동 생성해 준 FeignClient 구현체를 의존성 주입(DI) 받습니다.
private final BServiceClient bServiceClient;
@GetMapping("/hi")
public String hello() {
// [핵심] RestTemplate 같은 복잡한 HTTP 세팅 없이, 일반 메서드 호출하듯 타겟 서버의 API를 깔끔하게 실행합니다.
String hello = bServiceClient.getHello();
return "sevice-a: hi ###### service-b: " + hello;
}
}
- 사용자 요청 진입: 외부(브라우저 등)에서 Service A의 컨트롤러(@GetMapping("/hi"))로 요청이 들어옵니다.
- 메서드 호출 (Feign 작동): Service A는 직접 복잡한 통신 코드를 짜는 대신, 주입받은 BServiceClient 인터페이스의 getHello() 메서드를 그냥 툭 호출합니다.
- 최종 결과 반환: Service A는 받아온 "hello" 데이터를 가공하여 최종적으로 사용자에게 응답을 내려줍니다.
BServiceClient.java
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
// [핵심] 타겟 서버(Service B)와 통신하기 위한 명세서(인터페이스)입니다.
// url 속성은 application.properties에 설정된 주소를 동적으로 읽어옵니다.
@FeignClient(name = "service-b", url = "${service.b.url}")
public interface BServiceClient {
// [핵심] 구현 코드 없이 선언만 해두면, 타겟 서버의 'GET /hello' API를 호출하는 HTTP 통신 코드를 스프링이 자동 생성합니다.
@GetMapping("/hello")
public String getHello();
}
- 스프링의 자동 통신 (마법의 순간): 스프링 프레임워크가 Feign 인터페이스에 적힌 설정(@GetMapping("/hello")와 타겟 URL)을 보고, 알아서 HTTP GET 요청을 만들어 Service B로 발사합니다.
application.properties
spring.application.name=service-a
server.port=18080
# [핵심] FeignClient 인터페이스가 HTTP 요청을 보낼 타겟 서버(Service B)의 실제 주소입니다.
service.b.url=http://localhost:18081
service-b (요청을 받는 타겟 서버 - Port: 18081)
BController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BController {
// [핵심] Service A의 FeignClient(@GetMapping("/hello"))가 실제로 호출하게 되는 최종 목적지 API입니다.
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
- 타겟 서버 응답: Service B의 컨트롤러가 해당 요청(/hello)을 받아 처리한 후 "hello"라는 결과값을 Service A로 돌려줍니다.
application.properties
spring.application.name=service-b
# Service A와 포트가 겹치지 않도록 18081 포트에서 실행합니다.
server.port=18081
RUN
- 두 애플리케이션을 실행하고 “http://localhost:18080/hi”로 접속하여 메시지를 확인합니다.

🗨️ Docker 사용
application.properties 수정
📌 포트를 모두 아래와 같이 8080 으로 변경합니다.
- Why?! 도커의 외부 포트를 조절하여 애플리케이션을 실행할 것이기 때문입니다.
- → 호스트에서는 서로 다른 포트를 매핑하여 여러 서비스를 동시에 실행하기 위함입니다.
- service-a > application.properties
spring.application.name=service-a
server.port=8080
service.b.url=http://service-b:8080
- service-b > application.properties
spring.application.name=service-b
server.port=8080
Dockerfile 생성
- service-a > Dockerfile
# 1. 빈 컨테이너에 'Java 17'이 미리 설치된 리눅스 환경(템플릿)을 가져옵니다.
# 우리가 직접 리눅스를 깔고 Java를 설치할 필요 없이, 세팅이 끝난 버전을 사용하는 것입니다.
FROM eclipse-temurin:17-jre
# 2. 컨테이너 내부에 '/tmp'라는 임시 데이터 저장소(볼륨)를 만듭니다.
# Spring Boot가 실행될 때 내부적으로 톰캣(Tomcat) 서버를 쓰는데, 이 톰캣이 임시 파일을 저장할 공간이 필요하기 때문입니다.
VOLUME /tmp
# 3. 도커 이미지를 구울(Build) 때 사용할 변수(ARG)를 선언합니다.
# 'JAR_FILE'이라는 변수에 Gradle이 만들어낸 결과물(jar 파일)의 경로를 담아둡니다.
ARG JAR_FILE=build/libs/*.jar
# 4. 내 컴퓨터의 ${JAR_FILE} 경로에 있는 파일을 컨테이너 내부의 'app.jar'라는 깔끔한 이름으로 복사합니다.
# 버전이 붙어 복잡한 원본 파일명(예: myapp-1.0.1.jar)을 다루기 쉽게 'app.jar' 하나로 통일해 버리는 과정입니다.
COPY ${JAR_FILE} app.jar
# 5. 컨테이너가 켜질 때 자동으로 실행할 명령어를 지정합니다.
# 터미널에 'java -jar /app.jar'를 입력하여 Spring Boot 서버를 켜는 것과 똑같은 동작입니다.
ENTRYPOINT ["java","-jar","/app.jar"]
- jar는 Spring Boot 프로젝트를 하나의 실행 파일로 묶어놓은 것이다. 즉, 코드 + 라이브러리 + 설정이 전부 들어있는 완성된 프로그램 파일이다. 그래서 jar만 있으면 다른 PC에서도 바로 실행 가능하다.
- Java 실행 환경 준비 (FROM)
- 내가 만든 프로그램(jar) 넣기 (COPY)
- 그 프로그램 실행 (ENTRYPOINT)
💾 도커 데이터 관리 핵심 비교: VOLUME vs -v
- VOLUME (Dockerfile 내부 작성 / 익명 볼륨)
- 핵심 목적: 성능 최적화 및 임시 데이터 처리
- 특징: 컨테이너 내부의 파일 시스템은 구조상 읽고 쓰는 속도가 느립니다. 따라서 지워져도 무방한 임시 파일(예: Spring Boot의 Tomcat 작업 파일)을 다룰 때, 호스트 디스크의 성능을 빌려 빠르게 읽고 쓰기 위해 생성하는 '일회성 임시 작업장'입니다.
- 수명: 컨테이너가 삭제되면 그 역할이 끝나며, 데이터도 함께 폐기되는 것이 원칙입니다.
- -v 옵션 (실행 명령어 / 바인드 마운트)
- 핵심 목적: 데이터 영구 보존 및 안전 보장
- 특징: 호스트 컴퓨터(또는 AWS 서버)의 명확한 특정 경로와 컨테이너를 강력하게 연결합니다. 데이터베이스의 고객 정보나 서버 로그처럼, 어떤 상황에서도 절대 유실되면 안 되는 '핵심 데이터'를 안전하게 격리하여 저장하는 용도입니다.
- 수명: 컨테이너가 종료되거나 완전히 삭제되어도, 연결된 호스트의 파일은 영구적으로 안전하게 유지됩니다.
- service-b > Dockerfile
FROM eclipse-temurin:17-jre
VOLUME /tmp
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
도커 네트워크 생성
- 도커끼리 컨테이너 이름으로 호출하기 위해선 기본 브리지 네트워크가 아닌 사용자 정의 네트워크에서 진행해야 합니다.
docker network create my-network
Service-b 실행하기
📌 service-b의 프로젝트 루트 폴더에서 진행합니다 (Dockerfile의 위치)
- 프로젝트 빌드
# (빌드: 개발한 코드를 실행 가능한 파일로 만드는 과정 / Gradle: 이를 도와주는 자동화 도구)
# ./gradlew : 프로젝트에 내장된 Gradle(Wrapper)을 실행 (따로 설치할 필요 없이 바로 사용)
# clean : 이전 빌드 과정에서 생긴 찌꺼기(build 폴더)를 깨끗하게 삭제 (초기화)
# bootJar : Spring Boot 서버를 띄울 수 있는 최종 실행 파일(.jar)을 생성
./gradlew clean bootJar
- 이미지 생성
- 이미지 이름을 img-service-b 로 만듭니다. (마지막의 . 은 도커파일의 위치)
# 현재 경로의 Dockerfile을 읽어 'img-service-b'라는 이름의 실행용 이미지를 구워냅니다.
# -t (tag) : 생성될 도커 이미지의 이름(태그)을 지정
# . (dot) : Dockerfile이 위치한 경로 지정 (현재 디렉토리를 의미, 필수 입력)
docker build -t img-service-b .
- 컨테이너 생성
- 컨테이너의 이름을 service-b, 외부 포트는 18081 로 지정합니다. 내부 포트가 8080 임을 확인합니다.
# 생성된 이미지로 'service-b' 컨테이너를 실행하며, 네트워크와 외부 접속 포트를 설정합니다.
docker run -d --name service-b \
--network my-network \
-p 18081:8080 \
img-service-b
# -d : 터미널을 멈추지 않고 백그라운드에서 실행 (Detached)
# --name service-b : 컨테이너의 식별 이름을 지정
# --network my-network : 컨테이너 간 통신을 위해 지정된 도커 가상 네트워크에 연결
# -p 18081:8080 : 포트 매핑 (외부 호스트 포트 18081 ↔ 내부 컨테이너 포트 8080)
# img-service-b : 실행할 베이스 이미지의 이름
- 컨테이너 확인
- 컨테이너 리스트 명령어로 컨테이너가 실행되어있는지 확인합니다. (확인하는 습관을 만들기!)
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ffdea753159b img-service-b "java -jar /app.jar …" 6 seconds ago Up 2 seconds 0.0.0.0:18081->8080/tcp, :::18081->8080/tcp
- http://localhost:18081/hello 로 접속하여 service-b가 호출되는지 확인합니다.

Service-a 실행하기
📌 service-a의 프로젝트 루트 폴더에서 진행합니다 (Dockerfile의 위치)
- 프로젝트 빌드
./gradlew clean bootJar
- 이미지 생성
- 이미지 이름을 img-service-a 로 만듭니다. (마지막의 . 은 도커파일의 위치)
docker build -t img-service-a .
- 컨테이너 생성
- 컨테이너의 이름을 service-a, 외부 포트는 18080 로 지정합니다. 내부 포트가 8080 임을 확인합니다.
- service-b를 호출하는 url을 확인합니다. 컨테이너의 이름 및 내부 포트로 호출하는 것을 볼 수 있습니다.
docker run -d --name service-a \
--network my-network \
-p 18080:8080 \
-e SERVICE_B_URL=http://service-b:8080 \
img-service-a
- 컨테이너 확인
- 컨테이너 리스트 명령어로 컨테이너가 실행되어있는지 확인합니다. (확인하는 습관을 만들기!)
(base) ➜ com.service.a docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9fea304cd49e img-service-a "java -jar /app.jar" 5 seconds ago Up 2 seconds 0.0.0.0:18080->8080/tcp, :::18080->8080/tcp
- http://localhost:18081/hello 로 접속하여 service-a가 service-b를 호출 하는지 확인합니다.

🗨️ Docker Compose 사용
📌 도커를 사용하면 컨테이너 두개를 띄우는것도 매우 헷갈립니다. 컴포즈를 사용하면 이러한 상황을 피할 수 있습니다.
- 기존 도커 컨테이너를 모두 삭제합니다. (이미지가 아닙니다)
docker rm -f 서비스_A_컨테이너_아이디
docker rm -f 서비스_B_컨테이너_아이디
Docker compose 파일 생성
- service-a 프로젝트와 service-b 프로젝트의 상위 폴더에서 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
version: '3.8' # Docker Compose 파일의 문법 버전 지정
services: # 한 번에 실행할 컨테이너(서비스) 목록 정의
service-a: # 도커 내부 네트워크에서 '도메인 이름'처럼 사용됨
image: img-service-a # 컨테이너를 생성할 대상 이미지
ports:
- "18080:8080" # 포트 매핑 (외부 호스트 18080 ↔ 내부 컨테이너 8080)
environment:
# [핵심] 도커 내부망 통신이므로, B의 호스트 포트(18081)가 아닌 '컨테이너 이름'과 '내부 포트(8080)'를 사용!
- SERVICE_B_URL=http://service-b:8080
depends_on:
- service-b # 실행 순서 제어: service-b를 먼저 켠 후 service-a를 실행
service-b:
image: img-service-b
ports:
- "18081:8080" # 외부에서 B에 직접 접근할 땐 18081 포트 사용
networks: # 컨테이너들이 서로 통신할 가상 네트워크 설정
default:
driver: bridge # 기본 격리망 구축 (이 파일 내의 서비스들끼리만 안전하게 통신)
컨테이너의 내부 IP는 매번 바뀌기 때문에, 도커 컴포즈에 적힌 서비스 이름(service-b) 자체를 변하지 않는 도메인 주소(naver.com처럼)로 사용하여 서버 간 통신을 안전하게 보장하는 것이다
- 실행
- 도커 컴포즈를 실행하고 http://localhost:18080/hi 에 접속하여 확인합니다.
docker compose up -d

- 네트워크는 어떻게 된 걸까?
- 네트워크 생성 확인
- Docker Compose는 자동으로 네트워크를 생성하며, 이 네트워크는 docker-compose.yml 파일이 있는 디렉토리의 이름을 기반으로 합니다. 네트워크의 이름은 다음과 같은 형식으로 생성됩니다
- “directoryname_default”
- docker network ls 를 사용하여 네트워크가 생성되었는지 확인해봅니다.
- 네트워크 생성 확인

'Back-End > Spring' 카테고리의 다른 글
| 대규모 스트림 처리 (0) | 2026.05.14 |
|---|---|
| 프로젝트 관리 심화: 챕터 2 (CI/CD) (0) | 2026.05.14 |
| MSA (Microservice Architecture) (0) | 2026.04.14 |
| Spring 숙련: 챕터 2 (0) | 2026.04.08 |
| Spring 숙련: 챕터 1 (0) | 2026.04.07 |