1. Docker Volume이란?
2. .dockerignore란?
3. 다수의 Container로 구성된 소프트웨어 실행
4. Voting application의 docker-compose.yml 개선
5. Airflow의 docker-compose.yml 살펴보기
Docker Container가 어떠한 이유에서 중단이되고 재실행이된다면,
데이터들은 유실이 됩니다.
그러나 이 데이터들을 유지하고 싶은 경우,
사용하는 것이 Docker Volume입니다.
특정 소프트웨어가 Docker Container를 통해 일회성으로 동작하는 것이 아니라
계속해서 동작해야 한다면 데이터가 영구적으로 보관되어야합니다.
ex) MySQL을 Container로 작동시켰을 때, 중단된다고 해도 데이터가 유지되어야합니다.
이를 보장하는 기능이 Docker Volume
방법) Docker Container내의 가상 파일 시스템과 호스트 파일 시스템을 매핑(마운트)!
ex) Host OS : Ubuntu라고 가정,
호스트 파일 시스템의/home/user/logs를
Docker Container의/var/lib/airflow/logs와 매핑이렇게 하면, Docker Container가 중단되더라도
모든 Airflow logs는 기록이 남게 됩니다.

Container로 MySQL을 실행하는 경우,
데이터가 저장되는 공간을 Docker Volume으로 설정
호스트 파일 시스템의 폴더를 Docker Container 파일 시스템의 폴더로 마운트하는 것
마운트란?
디스크와 같은 물리적인 장치를 파일 시스템의 특정 위치에 연결해주는 것
예를 들어, 윈도우에서 USB를 연결하면 :D 폴더가 생기는 것이 마운트
Host Volumes
docker run -v를 실행할 때 페어(:)로 지정
ex) docker run -v /home/hostname/logs:/var/lib/airflow/logs
<Host 파일 시스템 경로>:<Container 파일 시스템 경로>Anonymous Volumes
docker run -v를 실행할 때 Container 경로만 지정
docker run -v var/lib/mysql/data
이 방식이 Dockerfile에서 사용되는 방식
호스트 쪽에 액세스되지는 않지만 재시작해도 유지되는 방식입니다.
Named Volumes
docker run -v를 실행할 때 Container 이름과 Container 경로를 지정
docker run -v name:/var/lib/mysql/data
가장 선호되는 방식
이 방식은 하나의 Volume을 다수의 Container에서 공유하는 것도 가능합니다.
docker-compose에서도 사용되는 방식입니다.
Volume을 Readonly로 지정하고 싶다면?
name:/var/lib/mysql/data:ro (ro : readonly)nginx란?
트래픽이 많이 몰리는 경우,
웹 서버 한대로는 트래픽을 감당하기 힘듦으로
다수의 웹 서버로 클러스터를 만들어야합니다.그럴 때, 그런 웹 서버들 앞단에 nginx를 올리고
트래픽을 받아서 웹 서버들에게 나눠주는 역할만 수행합니다.
docker run -d --name=nginx -p 8081:80 nginx
( nginx 공식 이미지를 설치하여 Host의 8081포트를 개방한 Container를 백그라운드에서 실행 )
( http://localhost:8081/ )
docker exec --user=root -it nginx sh
( root계정으로 로그인, -it옵션을 통해 nginx container를 쉘로 실행 )
apt update
apt install nano
# 여기서 index.html의 내용을 변경
# Welcome to Docker Volume을 내용에 넣어줌
nano /usr/share/nginx/html/index.html
exit
docker restart nginx
docker exec --user=root -it nginx sh
docker run -p 8081:80 -d --name nginx_demo -v /users/download/nginx/html:/usr/share/nginx/html nginx
( http://localhost:8081/ )
위와 동일하게 index.html 내용을 변경하고 재실행하는 과정을 수행
-> 편집한 내용이 유지된 것을 확인할 수 있습니다.
Docker Volume은 docker-compose.yml에서 많이 사용됩니다.
Airflow에서 사용하는 예시를 한번 살펴보겠습니다.
volumes:
- ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags
- ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs
- ${AIRFLOW_PROJ_DIR:-.}/plugins:/opt/airflow/plugins일반적으로 volume을 3개를 유지합니다.
volume 타입을 자유롭게 사용할 수 있지만
현재 작성된 타입은 Host Volumes입니다.
이를 사용하기 위해선,
호스트 파일 시스템의 경로 안 파일들의 내용과
Docker Container 파일 시스템의 경로 안 파일들의 내용이
서로 동일해야합니다.

docker volume ls
( 사용중인 volume 확인 )
docker volume rm
( 특정 volume 삭제 )
docker volume prune
( 사용되지 않은 모든 volume들을 삭제 )
docker volume inspect
( 특정 volume을 세부적으로 확인 )
Github에 .gitignore라는 파일이 있습니다.
여기에 추가하고 싶지 않은 파일 혹은 폴더를 지정하는 경우,
지정한 부분들을 제외하고 commit을 하게 해주는 기능을 제공합니다.
( 패스워드 같은 중요한 정보를 개발자가 부주의하게 업로드하는 것을 막기 위함
혹은 Repo의 크기가 불필요하게 너무 커지는 것을 방지하기 위함 )
Docker에도 동일한 기능을 제공하는 것이 있습니다.
그것이 바로 .dockerignore라는 파일입니다.
즉, Image를 build할 때 추가하지 말아야할 파일들이나 폴더를 지정하는 것입니다.
예시)
*.pyc
project.lock.json
bin/
obj/
.vs/
node_modules/
Dockerfile의 COPY 명령
이게 가장 위험 ( 폴더에 비밀스러운 정보가 있을 수 있음 )
민감한 정보들이 이미지로 들어가는 것을 막으려면
한번에 모두 COPY하는 것이 아니라
COPY 명령을 파일이나 폴더별로 일일히 적어주는 것이 좋을 수 있습니다.
그와 동시에 .dockerignore의 내용도 보강하면 좋습니다.
( .dockerignore에 작성된 파일들은 COPY 혹은 ADD가 되지않음 )
하나가 아닌 다수의 Container로 구성된 프로그램의 경우 이미지를 어떻게 빌드해야할까요?
-> Docker-Compose라는 Utility를 사용합니다.
사실 Docker Desktop에서 삭제하는 것이 가장 직관적
Container 삭제
docker container ls
docker container rm -f container_id
( -f 옵션을 통해 실행중인 container도 강제 삭제 )
여러 개 삭제
docker container rm -f container_id1 container_id2
한번에 모두 삭제
docker container rm -f $(docker container ls -aq)
( -aq 옵션으로 id들만 출력 )
이미지 삭제
docker image ls
docker image rm -f image_id
docker image rm -f $(docker image ls -q)삭제되었는지 확인
docker psdocker imagesDocker Desktop의 Troubleshoot 메뉴에는
Clean/Purge data 기능이 존재합니다.
이 기능을 사용하면 위 커맨드 라인의 기능들을
UI 직관적으로 쉽게 사용할 수 있습니다.

다수의 Container로 소프트웨어가 구성되는 경우 사용할 수 있는 툴이자 환경설정 파일입니다.
보통 Docker-Compose 파일은 하나만 갖고 있는 것이 아니라,
여러 버전을 만들어놓고 ex) Test용, Production용, ...
이를 상황에 따라서 다르게 가져가는 것이 일반적입니다.
이를 사용함으로써,
개별 Container를 일일히 관리하는 것이 아니라,
Application을 구성하는 모든 Container를 한번에 관리하는 게 가능해지기 때문에.
생산성이 훨씬 높아집니다.
docker container 개별 명령어와 유사하지만
기본적으로 그룹으로 적용된다는 차이가 있습니다.
docker-compose build
yml파일을 기준으로 build 키로 지정된 이미지들을 build
docker-compose up
docker run과 유사, 이미지가 없으면 다운받아 container 실행
정확하게는 build->create->start 과정으로 이루어져있습니다.
- 이미지가 없으면 build 및 pull을 진행하고
- 이미지가 만들어졌으면 container를 생성해주고
- 그 container들을 실행합니다.
docker-compose pull
로컬 혹은 docker hub에서 읽어올 수 있는 이미지들을 읽어옴
docker-compose push
build 및 pull 했던 이미지들을 한번에 docker hub로 푸시합니다.
docker-compose down
docker-compose로 실행된 서비스가 더이상 필요 없는 경우 사용,
Container 중지 및 삭제까지 해줍니다.
Image는 삭제하지 않고 남게됩니다.
( stop -> rm )
docker-compose start
docker-compose stop
docker-compose rm
docker-compose images
docker-compose 대상으로 실행이된 container들이
어떤 이미지로 실행이 되었는지 보여줍니다.
docker-compose ls
docker-compose를 통해 생성된 container들을 grouping해서 보여줍니다.
ex) STATUS의 running(5) -> 실행 중인 container들이 5개
docker-compose ps
docker-compose를 통해 생성된 container들의 상태를 개별적으로 보여줍니다.
( not grouping )
Docker Desktop의 일부로 설치됨
Docker Engine을 실행 후 버전 확인
docker-compose --version
v.1.27 이후 부터는 v2,v3가 합쳐져있므로
버전이 1.27보다 높으면 문제 없습니다.
services
(Application을 구성하는 Container들을 지정)
각 서비스는 별개의 Docker Image 지정과 Docker Container 실행으로 구성
각 서비스는 빌드시 자신의 Dockerfile가 필요
서버 별로 포트번호, 환경변수, 디스크 볼륨등을 지정해야함
서비스 이름이 Container의 이름이 됨
volumes
networks
docker-compose의 변천사
v1: docker-compose
v2: docker compose
Docker 1.27 이후 docker에 명령으로 compose가 추가
따라서, docker-compose 보다는 “docker compose”를 쓰는 것이 더 좋습니다.
( 버전이 높아도 docker-compose를 사용할 수 있습니다. )
하지만 아직까지 대부분의 문서가 docker-compose 중심으로 만들어져 있습니다.
docker compose 명령이 yaml, yml 둘 중 하나를 찾습니다.
"docker-compose" 기본 이름이 아닌 다른 이름을 사용하고 싶다면?
-f 옵션을 사용하면 됩니다.
e.g.) docker-compose -f docker-compose.mac.yml up
docker끼리 네트워크 연결이 필요한 경우
services에 준 이름으로 호스트 이름이 생성됨
내부에 DNS 서버가 하나 생성되어 호스트 이름을 내부 IP로 변환해줌
e.g.)
services:
frontend:
build: ./frontend
ports:
- 3000:3000
별도로 네트워크를 구성하고 싶다면?
docker network ls
Docker에서 제공해주는 예제 프로그램인 Voting application을
Docker-Compose를 사용해서 동작시켜볼 것입니다.
https://github.com/dockersamples/example-voting-app

5개의 컨테이너로 구성이 된 Multi-Container Program
- voting-app에서 누군가 투표를 하면
- 그 결과가 in-Memory DB인 Redis로 들어가고
- 백그라운드 Worker인 .NET 모듈이 그 변화를 감지하면
- 그 count를 PostgresSQL DB에 입력
- 이 DB를 NodeJS에서 읽어서 투표 결과를 출력
https://github.com/dockersamples/example-voting-app
위 Repo를 clone 후 각 컴포넌트를 이미지로 생성해줍니다.
redis와 PostgreSQL은 공식 이미지이므로 별도로 build할 필요는 없습니다.
docker build -t vote ./vote
docker build -t result ./result
docker build -t worker ./worker
docker images
docker run -d --name=redis redis
# postgres, 컨테이너 이름 : db,
# POSTGRES_PASSWORD 비밀번호를 설정 (-e 옵션을 통해 PW지정)
docker run -d -e POSTGRES_PASSWORD=password --name=db postrgres
docker run -d --name=vote -p 5001:80 vote
docker run -d --name=result -p 5002:80 result
docker run -d --name=worker worker
이런 방식으로 하나씩 Container를 생성하면
각 컴포넌트들 간의 네트워크 연결이 되지 않습니다.
위와 같은 방식으로 Container를 생성하면 서로 간의 네트워크 연결이 되지 않으므로
network를 하나 만들고 모든 Container들을 이 network 안으로 지정할 것입니다.
# 위 과정을 실행했다면 container들을 모두 제거
docker container rm -f $(docker container ls -aq)
# mynetwork라는 이름의 네트워크 생성
docker network create mynetwork
# 위 명령어들에 네트워크 연결을 추가하여 실행
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
iputils-ping 모듈의
ping 명령을 사용하면 ip address 혹은 hostname을 주었을 때,
연결이 되는지 안되는지 확인할 수 있습니다.

# root 계정으로 vote의 shell을 실행
docker exec -it --user root vote sh
apt update
apt install iputils-ping
ping redis

Postgres를 실행하는 부분이 제대로 동작하지 않습니다.
그 이유는, worker가 Redis에 기록이 된 투표 결과를 Postgres에 전달해야하는데
Postgres의 Username, Password 정보가 잘못 입력되었기 때문에 인증이 되지 않았습니다.실행했던 명령어 :
docker run -d --name=db -e POSTGRES_PASSWORD=password --network mynetwork postgres
POSTGRES_USER가 입력되지 않았고, PASSWORD도 postgres가 입력되었어야함
# worker/Program.cs
var pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;");
var redisConn = OpenRedisConnection("redis");
=> 이를 docker-compose에서 환경변수를 설정해주어 해결하겠습니다.
# docker-compose.yml
# container들 기술
# 여기 이름이 호스트 이름이 됨
services:
redis:
db:
vote:
result:
worker:
# container들 간 네트워크 정의
# 별도로 정의하지 않으면 기본 네트워크가 사용됨
networks:
# volume들 정의 (mount)
volumes:
services:
# 공식 이미지 redis:alpine으로 지정
redis:
image: redis:alpine
# 이하 동일
db:
image: postgres:15-alpine
# 환경변수 세팅
environment:
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
# ./vote경로의 dockerfile을 기준으로 build
# 5001:80으로 포트포워딩
vote:
build: ./vote
# 해당 Dockerfile의 CMD 부분을 override
command: python app.py
ports:
- 5001:80
result:
build: ./result
# 해당 Dockerfile의 ENTRYPOINT를 override
entrypoint: nodemon server.js
ports:
- 5002:80
worker:
build: ./worker
git clone https://github.com/learndataeng/example-voting-app.git
cd example-voting-app
# Clear하고 싶은 경우 사용
# docker container rm -f $(docker container ls -aq)
# docker image rm -f $(docker image ls -q)
# Build
# docker-compose -f docker-compose.mac.yml build
# Pull -> official image
# docker-compose -f docker-compose.mac.yml pull
# Build+Pull -> Create -> Start
docker-compose -f docker-compose.mac.yml up
# 생성된 container 확인
docker-compose ps
# postgres(db) container에 shell로 접근
docker exec -it --user=postgres example-voting-app-db-1 sh
# postgres shell 실행
psql
# postgres라는 user이름으로 postgres db에 연결
\c
# db 테이블 확인
\dt
# SQL 구문 사용 가능
SELECT * FROM votes;
# shell 종료
exit
exit
# container들 삭제
docker-compose -f docker-compose.mac.yml down

networks를 별도로 정의하지 않으면
자동으로 기본 DNS 서버가 할당됩니다.
2개의 네트워크를 만들어 관리하고 싶은 경우,
각 서비스 내에 networks를 지정 해주면 됩니다.
( front-tier, back-tier )
front-tier : 외부에 노출되는 부분
back-tier : 내부에서만 접근할 수 있는 부분
networks:
- back-tier
- front-tier
container가 중단되어도 PostgreSQL의 db는 persistent해야합니다.
Redis의 경우, in-memory db이므로 휘발성 데이터이기에 persistent할 필요는 없습니다.
services:
db:
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
container 간에는 이상적인 실행 순서가 존재합니다.
이 순서에 맞게 동작시키고 싶다면,
depends_on 을 사용하는 방법이 있습니다.
depends_on
서비스들간의 의존성이 있을 경우,
먼저 실행되어야 하는 서비스들을 이곳에 기술하면 됩니다.
condition옵션을 통해 조건을 달 수 있음
- service_started
(해당 컨테이너가 시작되면 실행)- service_healthy
(서비스가 건강하다고 확인되면 실행)- service_completed_successfully
(서비스들이 성공적으로 끝난 후 실행)
healthcheck
vote:
build: ./vote
# Dockerfile CMD override
command: python app.py
# vote가 실행되기 위해선
# redis가 먼저 실행되어야한다.
depends_on:
redis:
# option으로 condition을 적어주면,
# redis가 healthy할 때만,
# vote가 실행되게끔 조건을 달 수 있습니다.
condition: service_healthy
# 해당 서비스의 건강 체크
# Dockerfile에 있는 경우 override
healthcheck:
# test에 주어진 명령을 수행
test: ["CMD", "curl", "-f", "http://localhost"]
# 15초마다 명령을 실행
interval: 15s
# 응답이 5초 이상 걸리면 실패
timeout: 5s
# 최대 3번까지만 재시도
retries: 3
# vote가 시작되고 10초 후에 명령을 실행
start_period: 10s
# Mount
volumes:
- ./vote:/app
ports:
- "5001:80"
networks:
- front-tier
- back-tier
db:
image: postgres:15-alpine
# 환경 변수 추가
# Dockerfile의 ENV 역할
environment:
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
# Mount : db + healthcheck
volumes:
- "db-data:/var/lib/postgresql/data"
- "./healthchecks:/healthchecks"
healthcheck:
test: /healthchecks/postgres.sh
interval: "5s"
networks:
- back-tier
redis:
image: redis:alpine
# mount : healthcheck
volumes:
- "./healthchecks:/healthchecks"
healthcheck:
test: /healthchecks/redis.sh
interval: "5s"
networks:
- back-tier
worker:
# 이런식으로 build를 세부적으로 작성 가능
build:
context: ./worker
# redis와 db가 정상화가 된 후에 작동하는 것이 이상적
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
networks:
- back-tier
result:
build: ./result
entrypoint: nodemon server.js
# db가 안정화되면 실행
depends_on:
db:
condition: service_healthy
volumes:
- ./result:/app
ports:
- "5002:80"
- "5858:5858"
networks:
- front-tier
- back-tier
Airflow를 Docker로 설치할 때,
docker-compose.yaml을 통해 컨테이너들을 생성했습니다.
이 docker-compose.yaml 파일을 한번 공부해보겠습니다.
version
docker-compose의 버전을 표기하는 부분으로,
v.1.2.7 이후부터는 기입하지 않아도 되는 부분입니다.
x-airflow-common
반복되는 configuration이 있을 때,
매번 중복해서 쓰는 것은 비효율적이므로
yaml에서 anchor라 부르는 특정 block을 별칭 형태로 만들어서 쉽게 그 block을 반복해서 사용할 수 있는 기능을 제공합니다.그 anchor로 쓰인 것이
x-airflow-common이며
별칭은airflow-common입니다.공통된 environment, depends_on 등이 block으로 작성되었습니다.
services
volumes
x-airflow-common:
# 별칭으로 airflow-common을 등록
# (x-airflow-common을 반복해서 사용할 때 사용하는 별칭)
&airflow-common
# 기본으로 사용되는 공통된 이미지가 존재
image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:2.5.1}
environment:
# 환경변수만 따로 사용하고 싶은 경우
# airflow-common-env라는 별칭을 사용
&airflow-common-env
AIRFLOW__CORE__EXECUTOR: CeleryExecutor
…
# 추가로 설치하고 싶은 모듈을 정의
# 모든 docker container에 설치하고 싶은 경우 유용
_PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:-}
# dags, logs, plugins 폴더가 mount됨
volumes:
- ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags
- ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs
- ${AIRFLOW_PROJ_DIR:-.}/plugins:/opt/airflow/plugins
user: "${AIRFLOW_UID:-50000}:0"
# redis, postgres가 정상화될 때 실행
depends_on:
# depends_on만 따로 사용하고 싶은 경우
# airflow-common-depends-on라는 별칭을 사용
&airflow-common-depends-on
redis:
condition: service_healthy
postgres:
condition: service_healthy
airflow-scheduler:
# anchor 별칭을 이런식으로 사용 (<<: *)
# 작성했던 block을 계승하는 형태로 사용
<<: *airflow-common
command: scheduler
healthcheck:
test: ["CMD-SHELL", 'airflow jobs check --job-type SchedulerJob --hostname "$${HOSTNAME}"']
interval: 10s
timeout: 10s
retries: 5
restart: always
depends_on:
# 이하 동일, 별칭
<<: *airflow-common-depends-on
# airflow-init이 완벽하게 완료되면 실행
airflow-init:
condition: service_completed_successfully
이전에 Dag를 구현할 때, 특정 파이썬 모듈의 설치가 필요했었습니다.
import yfinance as yf
이렇게 DAG를 구현하며 새로 필요로 해진 모듈을 어떻게 설치하는 게 좋을까요?
( 일일히 container에서 설치해주는 것은 유지보수 측면에서 불가능 )
=> x-airflow-common의
_PIP_ADDITIONAL_REQUIREMENTS 값을 변경해주면 됩니다!
( ${_PIP_ADDITIONAL_REQUIREMENTS:-} 뒤 부분에 이어서 작성 )
e.g.)
# x-airflow-common
_PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:- yfinance pandas
numpy}
이후, docker-compose up을 해주면 모듈들이 설치가 될 것입니다.