🥞 BE
home

[BOAZ] Docker compose 이론 및 실습

Docker-compose

Docker-Compose란 무엇인가?

Docker compose란 여러 개의 컨테이너로부터 이루어진 서비스를 구축 실행하는 순서를 자동으로 하여 관리를 간단히 하는 기능이다. Docker compose에서는 compose 파일을 준비하여 커맨드를 1회 실행하는 것으로 그 파일로부터 설정을 읽어들여 모든 컨테이너 서비스를 실행시키는 것이 가능하다.
다수의 Container로 소프트웨어가 구성되는 경우 사용할 수 있는 툴 + 환경 설정 파일(yml)
docker-compose.yml로 설정
여기서 다양한 테스트들도 수행 가능
다양한 버전을 만드는 것도 일반적 (dev, test, prod 등등)
예 : docker-compose.dev.yml
개별 Container를 따로 관리하는 것보다 훨씬 더 생산성이 높음
환경설정 파일의 이름은 docker-compose.yml이나 docker-compose.yaml
하지만 그만큼 배워야할 것도 많고 복잡해짐
사용법 자체는 간단: 소프트웨어를 구성하는 모든 컨테이너에게 적용됨
○ docker-compose build ○ docker-compose up # -> pull + build + run ○ docker-compose pull ○ docker-compose ps ○ docker-compose down ○ docker-compose start ○ docker-compose stop ○ docker-compose rm
Python
복사
Docker-Compose
Docker Desktop의 일부로 설치가 됨
Docker Engine을 실행하고 먼저 터미널에서 버전 확인
만일 docker-compose가 없다고 나오면 먼저 docker-compose부터 실행
Docker Compose v.1.27+ 부터 v2와 v3가 합쳐짐
$ docker-compose --version Docker Compose version v2.15.1
Python
복사
docker-compose.yml 작성
다양한 버전이 존재하는데 우리는 v2와 v3가 합쳐진 버전 사용 예정
services: 다음으로 프로그램을 구성하는 서비스들을 지정
각 서비스는 별개의 Docker Image 지정과 Docker Container 실행으로 구성됨
즉 각 서비스는 자신의 Dockerfile을 갖고 있어야함 아니면 docker hub등에서 이미지를 다운로드
서비스 별로 포트번호, 환경변수, 디스크 볼륨 등을 지정해야함
서비스 이름은 아무 이름이나 가능
volumes: 앞서 사용된 docker volume들을 지정
networks: 앞서 사용된 network들을 지정
network은 지정하지 않으면 같은 docker-compose 그룹 내에서는 모두 연결이 됨
지정 안할 시, “default”라는 이름으로 기본 네트워크가 만들어짐
예제
컨테이너 네이밍과 볼륨을 유심히 볼 것
services: frontend: build: ./frontend ports: - 3000:3000 backend: build: ./backend ports: - 3001:3001 environment: DB_URL: mongodb://database/vidly # 컨테이너명/디비명(볼륨 X) database: image: mongo:4.0-xenial ports: - 27017:27017 volumes: - vidly: /data/db volumes: vidly:
Python
복사
앗 같은데요..? ⇒ 당신은 눈치가 빠르시군요!
docker-compose vs. docker compose
v1: docker-compose
v2: docker compose
Docker 1.27부터 docker에 명령으로 compose가 추가됨
docker-compose 보다는 “docker compose”를 쓰는 것이 더 좋음
별도로 docker-compose를 설치할 필요가 없음. 하지만 아직까지 대부분의 문서가 docker-compose 중심으로 만들어져 있음
docker-compose.yaml or docker-compose.yml
docker compose 명령의 위 둘 중의 하나를 찾음
만일 둘다 존재한다면 에러 발생함
만일 다른 이름의 파일을 사용하고 싶다면 -f 옵션 사용
docker-compose -f docker-compose.mac.yml up

docker-compose로 이미지 생성과 관리

docker-compose build
build 키로 지정된 것들 대상
docker-compose pull
docker hub에서 이미지들을 읽어오려고 함
docker images
각 개별 이미지 앞에 폴더 이름을 prefix로 붙임 (docker hub에서 읽어온 것들은 예외)
docker-compose images
컨테이너에 의해 실행되고 있는 이미지들만 보여줌
docker-compose push
docker hub으로 이미지들을 푸시하려고 함

docker-compose로 소프트웨어 시작과 중단

docker-compose up # build => create => start
docker-compose create
docker-compose start
docker-compose down
docker-compose stop
docker-compose rm
docker-compose ls ⇒ # docker-compose를 그룹별로 보여줌
docker-compose ps

docker-compose 네트워킹

docker끼리 네트워크 연결이 필요한 경우
services에 준 이름으로 호스트 이름이 생성됨
내부에 DNS 서버가 하나 생성되어 이름을 내부 IP로 변환해줌
별도로 네트워크를 구성하고 싶다면
networks에 네트워크를 나열하고 네트워크를 적절하게 서비스에 지정해주어야함
docker network ls
example-voting-app_default : docker compose로 실행되는 기본 네트워크

실습 - Voting Application with Docker-compose

우리가 실행해볼 프로그램 = Voting Application

Docker에서 제공해주는 예제 프로그램

example-voting-app
dockersamples
아키텍처 설명
두 옵션 사이에서 투표할 수 있도록 하는 Python 프론트엔드 웹 앱
새로운 투표를 수집하는 Redis
투표를 Consume하고 도트넷(.NET) 워커에서 저장하는 .NET Worker
도커 볼륨에 의해 지원되는 Postgres 데이터베이스
실시간으로 투표 결과를 보여주는 Node.js 웹 앱
먼저 매뉴얼하게 하나씩 빌드해보자
docker build -t vote ./vote ● docker build -t result ./result ● docker build -t worker ./worker # detached 모드로 위의 컨테이너들을 실행docker run -d --name=redis redis ● docker run -d -e POSTGRES_PASSWORD=postgres --name=db postgres ● docker run -d --name=vote -p 5001:80 vote ● docker run -d --name=result -p 5002:80 result ● docker run -d --name=worker worker ## vote(python app) container로 들어가서 redis와의 통신을 ping을 통해 확인 $ docker exec -it --user root vote sh $ apt-get update && apt update $ apt install -y iputils-ping $ ping redis ping: cannot resolve redis: Unknown host
Shell
복사
docker images
redis와 postgres는 공식 이미지들이라 빌드할 필요가 없음
위와 같이 이렇게 내가 일일히 하나씩 실행하면 동작할까?
각 컴포넌트들 간의 네트워크 연결이 안되고 있음!
컨테이너 간 통신이 안되고 있음을 ping을 통해 확인 가능
네트워크 관련 이슈를 더 자세히 보자
python app에서는 아래와 같이 redis를 호출해서 인스턴스를 가져옵니다.
vote/app.py
def get_redis(): """ Redis 인스턴스를 가져오는 함수입니다. 이 함수는 Flask의 g 객체를 사용하여 Redis 인스턴스를 싱글톤으로 관리합니다. 싱글톤 패턴을 사용하여 한 번의 요청에 대해 동일한 Redis 인스턴스를 계속 사용합니다. Returns: Redis: Redis 클라이언트 인스턴스를 반환합니다. Raises: ConnectionError: Redis 서버에 연결할 수 없는 경우 발생합니다. TimeoutError: Redis 서버와의 연결이 시간 초과되는 경우 발생합니다. """ if not hasattr(g, 'redis'): # g 객체에 Redis 인스턴스가 없는 경우에만 Redis 클라이언트를 생성합니다. # host는 "redis"로 설정되어 있으며, db는 0으로 설정되어 있습니다. # socket_timeout은 연결 시도 시간을 5초로 설정합니다. g.redis = Redis(host="redis", db=0, socket_timeout=5) return g.redis
Python
복사
vote에 exec해서 iputils-ping 설치 후 ping 명령으로 redis 호스트 이름이 연결되는지 확인
$ ping redis ping: cannot resolve redis: Unknown host
Python
복사
host ⇒ 컨테이너 이름 → redis
result/server.js
var pool = new pg.Pool({ connectionString: 'postgres://postgres:postgres@db/postgres' });
Python
복사
Postgres 연결시 postgres:postgres를 사용하고 있음을 주의깊게 볼것!
@ 다음에 오는 이름은 컨테이너 이름
Docker Compose 파일에서 db라는 이름으로 PostgreSQL 컨테이너를 정의
worker/Program.cs
var pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); var redisConn = OpenRedisConnection("redis");
Python
복사
worker에서는 postgresql과 redis를 동시에 통신
이 부분에서 서로의 존재를 몰라 에러가 발생 ⇒ 통신 X

그렇다면 어떻게 Network 이슈를 해결할 수 있을까?

docker의 network 기능 사용
전에는 docker run의 link 옵션을 사용
network을 하나 만들고 모든 컨테이너들을 이 네트워크 안으로 지정
연결 상황에 따라 별개의 네트워크를 만들고 사용도 가능함
아래에서는 2가지 네트워크를 통해 컨테이너들 간 통신 그룹을 만들 예정
back-tier
front-tier
매뉴얼 예제에서는 mynetwork 이라는 네트워크를 하나 만들고 진행 예정
docker network create
● docker build -t vote ./vote ● docker build -t result ./result ● docker build -t worker ./worker # 만약 중요한 것이 없다면 아래와 같이 수행 ● docker container rm -f $(docker container ls -aq) # 중요한 컨테이너가 있을 경우 docker ps docker stop {컨테이너 id} docker rm {컨테이너 id} # 네트워크 생성 ● docker network create mynetwork . ├── healthchecks ├── k8s-specifications ├── result # ├── seed-data ├── vote # └── worker # ● docker run -d --name=redis --network mynetwork redis # password 설정과 함께 docker run ● docker run -d --name=db -e POSTGRES_PASSWORD=password --network mynetwork postgres ● docker run -d --name=vote -p 5001:80 --network mynetwork vote ● docker run -d --name=result -p 5002:80 --network mynetwork result ● docker run -d --name=worker --network mynetwork worker ## vote container로 들어가서 redis와의 통신을 확인 $ docker exec -it --user root vote sh $ apt-get update && apt update && apt install -y iputils-ping $ apt install -y iputils-ping $ ping redis # ping redis -> 아래와 같이 통신이 되는 것을 확인 가능! PING redis (172.22.0.2) 56(84) bytes of data. 64 bytes from redis.mynetwork (172.22.0.2): icmp_seq=1 ttl=64 time=0.934 ms 64 bytes from redis.mynetwork (172.22.0.2): icmp_seq=2 ttl=64 time=0.099 ms 64 bytes from redis.mynetwork (172.22.0.2): icmp_seq=3 ttl=64 time=0.135 ms 64 bytes from redis.mynetwork (172.22.0.2): icmp_seq=4 ttl=64 time=0.079 ms 64 bytes from redis.mynetwork (172.22.0.2): icmp_seq=5 ttl=64 time=0.252 ms 64 bytes from redis.mynetwork (172.22.0.2): icmp_seq=6 ttl=64 time=0.235 ms
Python
복사

Docker compose를 통한 실행

앞서 5개의 Container를 일일히 실행했을 때의 문제점
Postgres를 실행하는 부분이 제대로 동작하지 않았음
# ping db PING db (192.168.107.3) 56(84) bytes of data. 64 bytes from db.mynetwork (192.168.107.3): icmp_seq=1 ttl=64 time=0.183 ms 64 bytes from db.mynetwork (192.168.107.3): icmp_seq=2 ttl=64 time=0.048 ms 64 bytes from db.mynetwork (192.168.107.3): icmp_seq=3 ttl=64 time=0.055 ms # 응? 통신은 되는데..?
Python
복사
이는 Postgres 컨테이너를 실행할 때 적절한 환경 변수를 지정하지 않아 발생한 문제.
Worker 컨테이너 내에서 OpenDbConnection 함수를 호출할 때, Postgres 서버와의 연결 정보가 잘못 설정되었기 때문에 동작하지 않았음.
docker run -d --name=db -e POSTGRES_PASSWORD=password --network mynetwork postgres
worker/Program.cs
var pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); var redisConn = OpenRedisConnection("redis");
Python
복사
이를 해결하려면 Container를 실행할 때 아래 2개의 환경변수를 넘겨주어야 함
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
이걸 docker-compose 환경 설정 파일을 통해 넘기면서 해결해볼 예정
docker-compose.yml로 포팅: 뼈대
● docker run -d --name=redis --network mynetwork redis ● docker run -d --name=db -e POSTGRES_PASSWORD=password --network mynetwork postgres ● docker run -d --name=vote -p 5001:80 --network mynetwork vote ● docker run -d --name=result -p 5002:80 --network mynetwork result ● docker run -d --name=worker --network mynetwork worker
Python
복사
앞서 컨테이너를 하나씩 실행할 때 네트워킹 부분은 제대로 동작하지 않았음
docker-compose up
docker volume이란?
Docker volume은 Docker 컨테이너가 데이터를 저장하거나 공유하기 위한 방법.
컨테이너는 기본적으로 독립적인 파일 시스템을 가지고 있으며, 컨테이너가 삭제되면 그 안의 데이터도 함께 삭제됨.
그러나 Docker volume을 사용하면 컨테이너의 데이터를 호스트 머신의 파일 시스템에 영구적으로 저장하고 유지할 수 있다.
docker-compose.yml로 포팅: services 키 보기
빌드는 각 객체 폴더 밑에 Dockerfile로 수행
docker-compose.mac.yml
# v2 and v3 are now combined! # docker-compose v1.27+ required # % docker-compose version # Docker Compose version v2.15.1 services: vote: build: ./vote # use python rather than gunicorn for local dev command: python app.py ports: - "5001:80" result: build: ./result # use nodemon rather than node for local dev entrypoint: nodemon server.js ports: - "5002:80" worker: build: ./worker redis: image: redis:alpine db: image: postgres:15-alpine environment: POSTGRES_USER: "postgres" POSTGRES_PASSWORD: "postgres"
Python
복사
데모
● 먼저 청소 한번 하기 docker container rm -f $(docker container ls -aq) # docker image rm -f $(docker image ls -q) ● 확인해보기 docker ps -a docker images ● 그리고나서 앞의 docker-compose.mac.yml의 내용을 바탕으로 실행해보기 docker-compose -f docker-compose.mac.yml build docker-compose -f docker-compose.mac.yml up -d ● db 컨테이너 접속 docker exec -it --user=postgres expample-voting-app-db-1 sh / $ whoami postgres / $ psql psql (15.3) Type "help" for help. # \c: Connect to database postgres=# \c You are now connected to database "postgres" as user "postgres". # \dt: List tables postgres=# \dt List of relations Schema | Name | Type | Owner --------+-------+-------+---------- public | votes | table | postgres (1 row) postgres=# select * from votes; id | vote -----------------+------ cb9d5ad7206be28 | a (1 row) exit docker-compose -f docker-compose.mac.yml down ✔ Container example-voting-app-worker-1 Removed 0.6s ✔ Container example-voting-app-db-1 Removed 0.8s ✔ Container example-voting-app-vote-1 Removed 1.0s ✔ Container example-voting-app-result-1 Removed 0.7s ✔ Container example-voting-app-redis-1 Removed 0.9s ✔ Network example-voting-app_default Removed
Python
복사

네트워크 정의를 통한 docker-compose 개선

모든 네트워크를 다 통일 시키는 것은 바람직 하지 않다.
네트워크를 논리적으로 분리시켜주는 것이 애플리케이션의 보안성, 확장성, 유연성 및 서비스 분리 측면에서 더 바람직
예시 : 백엔드 내 앞 로직, 백 로직
docker-compose.yml로 포팅: networks 정의
front tier : 사용자에게 노출
back tier : 사용자에게 노출X
이를 통한 network isolation. 혹시라도 생길 수 있는 문제 차단
1.
격리(Isolation): 각 서비스를 네트워크로 분리함으로써, 서로 다른 서비스 간의 네트워크 격리를 구현할 수 있습니다. 이는 각 서비스의 보안을 강화하고, 한 서비스의 장애나 과부하가 다른 서비스에 영향을 미치는 것을 방지할 수 있습니다.
2.
세분화(Segmentation): 네트워크를 back-tier와 front-tier로 분리함으로써, 백엔드 서비스와 프론트엔드 서비스를 구분할 수 있습니다. 백엔드 서비스는 내부 네트워크(back-tier)에 속하고, 프론트엔드 서비스는 내부 및 외부 네트워크(front-tier)에 속합니다. 이는 보안을 강화하고, 외부로부터의 직접적인 액세스를 제한하여 안전한 서비스 구축을 돕습니다.

docker-compose.yml로 포팅: services들에 네트워크 지정

기존
services: redis: image: redis:alpine db: image: postgres:15-alpine vote: build: ./vote ports: - 5001:80 result: build: ./result ports: - 5002:80 worker: build: ./worker
Python
복사
변경
services: redis: … networks: - back-tier db: … networks: - back-tier vote: … networks: - back-tier - front-tier result: … networks: - back-tier - front-tier worker: … networks: - back-tier networks: back-tier: front-tier:
Python
복사

docker-compose.yml로 포팅: volumes 정의

어느 컨테이너 서비스의 데이터가 계속해서 저장되어야 하는가?
PostgreSQL
db의 내용이 persistent해야함 ⇒ 볼륨 생성 (네임드 볼륨)
volumes: db-data:
Python
복사
docker-compose.yml로 포팅: services들에 볼륨 지정
services: redis: db: vote: result: worker:
Python
복사
services: redis: # db 데이터가 저장되는 컨테이너 내부 디렉토리 장소와 네임드 볼륨을 매핑 db: volumes: - db-data:/var/lib/postgresql/data vote: result: worker: volumes: db-data:
Python
복사

docker-compose.yml로 포팅: vote 서비스 개선

depends_on : 서비스들간의 의존성이 있을 경우, 먼저 실행되어야 하는 서비스들을 여기 기술해야함
더 나아가서 해당 서비스의 health check도 포함해줌
healthcheck : 해당 서비스의 건강을 나타내주는 체크를 기술
docker-compose.yml의 commandentrypoint
command: 이미지 Dockerfile의 CMD를 덮어쓰는데 사용
entrypoint: 이미지 Dockerfile의 ENTRYPOINT를 덮어쓰는데 사용
docker-compose.yml의 healthcheck
역시 Dockerfile에서 기술 가능한 기능이며 docker-compose에서 아래와 같이 덮어쓰기 가능
healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] # 0 for success 1 for failure interval: 1m30s timeout: 10s # 실행 시간 10초 넘어가면 fail retries: 3 start_period: 40s
Python
복사
docker-compose.yml의 depends_on
해당 서비스가 실행되기 위해서 먼저 실행되어야 하는 서비스들을 여기 기술
short form:
depends_on: - db - redis
Python
복사
long form:
depends_on: db: condition: service_healthy redis: condition: service_started
Python
복사
depends_on의 condition으로 가능한 값
service_started:
컨테이너가 시작되었는지 여부
service_healthy:
의존성이 건강 상태를 나타내는 healthcheck에 따라 시작 전에 "건강"한 상태인지 확인.
True or False ⇒ 0 for success 1 for failure
service_completed_successfully:
의존성에 종속된 서비스가 성공적으로 완료되기를 기대하고, 완료 후에 종속 서비스를 시작합니다.
처음 초기화해주는 서비스

docker-compose.yml로 포팅: db 서비스 개선

이 환경 변수를 통해 Postgres DB 설정이 됨. 이에 대해서 앞서 살펴보았음
host volume과 named volume을 사용하고 있음
host volume : 직접 호스트 pc의 디렉토리 주소와 컨테이너 내부 주소를 매핑
docker-compose.yml의 environment
해당 서비스가 컨테이너 안에서 실행될 때 환경변수들을 지정 (Dockerfile의 ENV)
2가지 문법이 존재
Map 문법 → 위의 docker compose file은 이 것을 사용
environment: RACK_ENV: development SHOW: "true" USER_INPUT:
Python
복사
Array 문법:
environment: - RACK_ENV=development - SHOW=true - USER_INPUT
Python
복사

docker-compose.yml로 포팅: redis 서비스 개선

volume, health check, network 설정

docker-compose.yml로 포팅: worker 서비스 개선

build 키워드 밑에 빌드 관련 정보를 더 남겨서 docker compose 정보들을 기록 가능
도커 파일 명, 도커파일 위치한 디렉토리
build: context: ./my_app # 도커파일이 위치한 디렉토리 경로 dockerfile: Dockerfile.prod # 도커 파일 명 args: - ENV=production # 빌드 시에 사용할 환경 변수 설정
Python
복사

docker-compose.yml로 포팅: result 서비스 개선 (node)

entrypoint : Dockerfile의 entrypoint를 오버라이딩

최종 Docker-compose.yml 리뷰

주의 : 다시 한번 Monterey Mac에서 포트 5000번을 사용할 수 없기에 vote 앱의 포트번호를 수정해야함 (아무 port를 부여할 것, 5001, 5002 제외)
vote와 result의 포트번호를 각각 5001과 5002로 수정한 버전이 위의 버전임
# version is now using "compose spec" # v2 and v3 are now combined! # docker-compose v1.27+ required services: vote: build: ./vote # use python rather than gunicorn for local dev command: python app.py depends_on: redis: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 15s timeout: 5s retries: 3 start_period: 10s volumes: - ./vote:/app ports: - "5000:80" networks: - front-tier - back-tier result: build: ./result # use nodemon rather than node for local dev entrypoint: nodemon server.js depends_on: db: condition: service_healthy volumes: - ./result:/app ports: - "5001:80" - "5858:5858" networks: - front-tier - back-tier worker: build: context: ./worker depends_on: redis: condition: service_healthy db: condition: service_healthy networks: - back-tier redis: image: redis:alpine volumes: - "./healthchecks:/healthchecks" healthcheck: test: /healthchecks/redis.sh interval: "5s" networks: - back-tier db: image: postgres:15-alpine environment: POSTGRES_USER: "postgres" POSTGRES_PASSWORD: "postgres" volumes: - "db-data:/var/lib/postgresql/data" - "./healthchecks:/healthchecks" healthcheck: test: /healthchecks/postgres.sh interval: "5s" networks: - back-tier # this service runs once to seed the database with votes # it won't run unless you specify the "seed" profile # docker compose --profile seed up -d seed: build: ./seed-data profiles: ["seed"] depends_on: vote: condition: service_healthy networks: - front-tier restart: "no" volumes: db-data: networks: front-tier: back-tier:
Python
복사
참고로 위의 docker compose file은 아래와 같이 실행이 되지 않는다. (그저 이렇게 개선이 될 수 있다는 점을 알려주고 싶었을 뿐) → 아래의 에러를 해결하면 당신은 찐 고수!
example-voting-app-db-1 | 2023-07-25 15:45:47.023 UTC [1] LOG: database system is ready to accept connections dependency failed to start: container example-voting-app-db-1 is unhealthy
Python
복사
대신, 우리는 아래 파일을 실습한다. (네트워크 설정과 볼륨 설정)
docker-compose -f docker-compose.hj.yml up -d를 통해 실습을 해보면 된다.
version: '3.7' services: vote: build: ./vote command: python app.py ports: - "5001:80" networks: - front-tier - back-tier result: build: ./result entrypoint: nodemon server.js ports: - "5002:80" networks: - front-tier - back-tier worker: build: ./worker networks: - back-tier redis: image: redis:alpine networks: - back-tier db: image: postgres:15-alpine environment: POSTGRES_USER: "postgres" POSTGRES_PASSWORD: "postgres" volumes: - db-data:/var/lib/postgresql/data networks: - back-tier volumes: db-data: networks: front-tier: back-tier:
Python
복사
docker-compose rm -fsv