여러 사람들로 구성된 팀 프로젝트에서 나 혼자만 docker를 알고 있고, 다른 사람에게도 도커 사용을 권한다면

다른 사람들은 다음과 같은 코드를 shell에 작성하면서 내가 대체 뭘 저지르고 있는가에 대해 한참 생각할 것이다.

docker \
run \
    --name "db" \
    -v "$(pwd)/db_data:/var/lib/mysql" \
    -e "MYSQL_ROOT_PASSWORD=root_pass" \
    -e "MYSQL_DATABASE=wordpress" \
    -e "MYSQL_USER=docker_pro" \
    -e "MYSQL_PASSWORD=docker_pro_pass" \
    --network wordpress_net \
mysql:latest

docker \
    run \
    --name app \
    -v "$(pwd)/app_data:/var/www/html" \
    -e "WORDPRESS_DB_HOST=db" \
    -e "WORDPRESS_DB_NAME=wordpress" \
    -e "WORDPRESS_DB_USER=docker_pro" \
    -e "WORDPRESS_DB_PASSWORD=docker_pro_pass" \
    -e "WORDPRESS_DEBUG=1" \
    -p 8000:80 \
    --network wordpress_net \
wordpress:latest

우리는 이제 도커를 어떻게 사용하는 지 알았으니 위의 스크립트를 해석할 수 있지만, 다른 팀원들은 '도대체 이게 뭔데?'라고 생각할 수도 있을 것이다.

그리고, 위와 같은 스크립트를 shell 환경에서 돌린다면 최소한 3개 이상의 shell이 필요하기도 하고,

위의 예제인 wordpress같은 경우에는 mysql 컨테이너를 필수적으로 띄운 상황에서 띄워야 하는데 저 스크립트만 가지고는 '그냥 명령어네, 아무거나 먼저 띄워야지'라고 생각하고 run을 한 경우에는 불상사가 발생할수도 있다.

 

그래서 다음과 같은, 자연어와 비슷하고, yaml 형식으로 제작된 docker-compose가 등장했다.

version: "3.0"

services:
  db:
    image: mysql:latest
    volumes:
      - ./db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_pass
      MYSQL_DATABASE: wordpress
      MYSQL_USER: docker_pro
      MYSQL_PASSWORD: docker_pro_pass
  
  app:
    depends_on: 
      - db
    image: wordpress:latest
    volumes:
      - ./app_data:/var/www/html
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: docker_pro
      WORDPRESS_DB_PASSWORD: docker_pro_pass

이와 같은 문서를 작성한 후, 쉘을 딱 하나만 켜서

$ docker-compose up

이라는 명령어만 실행하면 모든 컨테이너가 알아서 실행되는, 그런 마법같은 일을 docker-compose를 통해 할 수 있다.

 

예제를 통해 바로 시작해보자.

우리는 Wordpress라는, 설치형 블로그 어플을 사용하고 싶다.

그런데 Wordpress를 띄우려면, MySQL 컨테이너를 선행해서 띄워야 한다.

일단 쉘을 켜서 다음 명령어를 쳐보자.

$ docker network create wordpress_net

$ docker network ls
PS docker-pro-2308\lecture\2nd> docker network ls
NETWORK ID     NAME            DRIVER    SCOPE
440dbeb16442   2nd_default     bridge    local
7fb999d3d64f   bridge          bridge    local
483de980f145   host            host      local
3f40074674cc   none            null      local
24113129f7c6   wordpress_net   bridge    local

bridge / host / scope 등에 대해서는 이따가 다시 확인해보자.

다른 shell을 열어서, 이제는 아까 봤던 bash 명령어를 쳐보자.

docker \
run \
    --name "db" \
    -v "$(pwd)/db_data:/var/lib/mysql" \
    -e "MYSQL_ROOT_PASSWORD=root_pass" \
    -e "MYSQL_DATABASE=wordpress" \
    -e "MYSQL_USER=docker_pro" \
    -e "MYSQL_PASSWORD=docker_pro_pass" \
    --network wordpress_net \
mysql:latest

디렉토리로 확인해도 되고, vsc등의 idea에서 확인해보면 db_data라는 폴더가 생성되었을 것이다.

이것은 ‘앞으로 컨테이너를 껐다 켜도 db가 유지됨’을 의미한다.

다음 명령어도 작성해보자.

docker \
    run \
    --name app \
    -v "$(pwd)/app_data:/var/www/html" \
    -e "WORDPRESS_DB_HOST=db" \
    -e "WORDPRESS_DB_NAME=wordpress" \
    -e "WORDPRESS_DB_USER=docker_pro" \
    -e "WORDPRESS_DB_PASSWORD=docker_pro_pass" \
    -e "WORDPRESS_DEBUG=1" \
    -p 8000:80 \
    --network wordpress_net \
wordpress:latest

우리가 -p옵션으로 wordpress 컨테이너를 호스트의 8000번 포트에 연결했으니 들어가보도록 하자.

> localhost:8000

잘 실행되는것을 볼 수 있다.

그런데 맨날 이렇게 명령어를 한번에 쳐서 띄우기는 쉽지 않으니, docker-compose.yml을 띄워서 이 컨테이너들을 관리해보자.

먼저 다음의 명령어로 지금 우리가 만들었던 워드프레스 컨테이너를 삭제해보자. 이름은 db와 app을 주었던 것을 기억하자.

$ docker rm --force app
$ docker rm --force db
$ docker network rm --force wordpress_net
$ docker ps -a
CONTAINER ID   IMAGE                    COMMAND                   CREATED        STATUS                      PORTS     NAMES
$ docker network ls
NETWORK ID     NAME          DRIVER    SCOPE
440dbeb16442   2nd_default   bridge    local
7fb999d3d64f   bridge        bridge    local
483de980f145   host          host      local
3f40074674cc   none          null      local

깔끔하게 지웠으면, 다음 명령어를 다시 입력해보자.

$ docker-compose up

그러면 뭐가 좌르륵 실행되면서 아까와 같은 db_data, app_data 폴더가 다시 생기는것을 확인할 수 있고,

브라우저에서 localhost:8000로 접속해 보면 아까와 같이 워드프레스도 잘 뜬다.

$ docker ps -a
CONTAINER ID   IMAGE                    COMMAND                   CREATED         STATUS                      PORTS                  NAMES   
e9966757bc11   wordpress:latest         "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes                0.0.0.0:8000->80/tcp   2nd-app-1
986b8530bd9a   mysql:latest             "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes                3306/tcp, 33060/tcp    2nd-db-1

컨테이너도 두개 다 잘 뜬것을 볼 수 있다.

반대로, 이것을 한번에 끄고 싶을때도 간편한 명령어도 쉽게 끌 수 있다.

$ docker-compose down

 

 

분석

이제, 우리가 여태 사용해왔던 shell 방식과, docker-compose yml파일을 실행시킨 것을 비교해보자.

  1. mySql
  2. 워드프레스

mysql은 기본적으로 호스트의 3306번 포트에 위치한다.

또한, /var/lib/mysql에 데이터가 저장된다.

먼저, mySql을 돌리기 위해서는 shell에 다음과 같은 명령어가 있어야 한다.

name은 db라고 지어주자.

$ docker \
run \
 --name "db"\
 mysql:latest

\(백슬래시)는, shell에게 ‘아직 안끝났어’라는 말과 같다.

docker-compose로 가보자.

version: "3.0"

services:
 db:
  images:mysql:latest

compose 파일을 작성하기 위해서는 처음에 version에 대한 명시가 필요한데, 보통 가장 처음에 3.0으로 설정해두는것이 ‘국룰’이라고 한다.

그리고 services 안에 내가 넣고 싶은 컨테이너를 넣는데,

services 바로 밑에 오는 저 db가 shell에서 --name 옵션을 주는 것과 동일하다.

 

그러나 이렇게만 만들면 지난 시간에 배웠듯, 컨테이너를 껐다 켜면 데이터가 유실되므로

호스트의 파일 시스템과 컨테이너의 파일 시스템을 연결해준다.(볼륨 마운트)

$ docker \
run \
 --name "db"\
 -v "$(pwd)/db_data:/var/lib/mysql" \
 mysql:latest

docker-compose.yml을 확인해보자.

version: "3.0"

services:
  db:
    image: mysql:latest
    volumes:
      - ./db_data:/var/lib/mysql

이렇게 호스트의 db_data라는 폴더와 컨테이너의 /var/lib/mysql이 연동되었다.

그 외에 id나 password 지정같은 또다른 옵션들을 줄 수 있다.

docker \
run \
    --name "db" \
    -v "$(pwd)/db_data:/var/lib/mysql" \
    -e "MYSQL_ROOT_PASSWORD=root_pass" \
    -e "MYSQL_DATABASE=wordpress" \
    -e "MYSQL_USER=docker_pro" \
    -e "MYSQL_PASSWORD=docker_pro_pass" \
    --network wordpress_net \
mysql:latest

결국 shell에서는 이와같은 스크립트가 탄생하게 되는 것이고,

docker-compose에서는 위의 옵션을

services:
  db:
    image: mysql:latest
    volumes:
      - ./db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_pass
      MYSQL_DATABASE: wordpress
      MYSQL_USER: docker_pro
      MYSQL_PASSWORD: docker_pro_pass

이런 yaml 파일 형식으로 줄 수 있는 것이다.

이 yml 파일을 조금 더 뜯어보자.

https://docs.docker.com/compose/compose-file/03-compose-file/

공식 문서를 보면, 다음과 같은 옵션들이 도커 컴포즈 파일에 들어갈 수 있다.

  • Version(선택적)
  • Services(필수)
  • Networks
  • Volumes
  • Configs
  • Secrets

등과 같은, 다양한 옵션들을 줄 수 있다.

여기서 필수 항목인 Services를 살펴보자.

Services란?

  • 실행하려는 컨테이너들을 정의하는 역할
  • 이름, 이미지, 포트 매핑, 환경 변수, 볼륨등을 포함
  • 해당 정보를 토대로 컨테이너를 생성하고 관리

이 services에 들어갈 수 있는 옵션들에는 어떤 것이 있을까?

  • image : 컨테이너를 생성할 때 쓰일 이미지 지정
  • build : 정의된 도커파일에서 이미지를 빌드해 서비스의 컨테이너를 생성하도록 설정
  • environment : 환경 변수 설정, docker run 명령어의 --env, -e 옵션과 동일
  • command : 컨테이너가 실행될 때 수행할 명령어, docker run 명령어의 마지막에 붙 는 커맨드와 동일
  • depends_on : 컨테이너 간의 의존성 주입, 명시된 컨테이너가 먼저 생성되고 실행
  • ports : 개방할 포트 지정, docker run 명령어의 -p와 동일
  • expose: 링크로 연계된 컨테이너에게만 공개할 포트 설정
  • volumes: 컨테이너에 볼룸을 마운트함
  • restart: 컨테이너가 종료될 때 재시작 정책
    • no: 재시작 되지 않음
    • always: 외부에 영향에 의해 종료 되었을 때 항상 재시작 (수동으로 끄기 전까지)
    • on-failure: 오류가 있을 시에 재시작

 

이 정보를 가지고, 워드프레스 앱 컨테이너를 만들어 보자.

 

워드프레스 컨테이너는 다음과 같이 웹 서버, php, 그리고 파일 시스템으로 /var/www/html이 있다.

웹서버가 있으므로 컨테이너에서는 80번 포트에서 명령을 대기하고 있다.

 

그런데, 여기서 중요한 것은 워드프레스는 db를 이용해야 하므로, 늘 먼저 mySql 컨테이너가 선행되어서 띄워져야 한다.

이 옵션을 docker-compose에서는 depends_on 옵션을 이용해서 설정해준다.

app:
    depends_on: 
      - db
    image: wordpress:latest
    volumes:
      - ./app_data:/var/www/html
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: docker_pro
      WORDPRESS_DB_PASSWORD: docker_pro_pass

 

 

 

 

 

생각해보면, app 컨테이너는 외부에서 접근할 수 있어야 한다.

그러나 db 컨테이너는 외부에서 접근하면 큰일난다. 오로지 app 컨테이너에서만 접근할 수 있어야 한다.

그러므로 app 컨테이너만 ports 옵션을 주고, db 컨테이너는 ports의 옵션을 제외한다.

그리고 나머지 환경변수들을 입력해주면 된다.

 

이제, 마지막으로 network에 대해서 알아보자.

wordpress 입장에서 생각해보자.

워드프레스는 처음 어플리케이션을 실행하는데 db를 들고와야 한다.

그래서 db에 접속할 때 host에 db를 적는데, 매번 이렇게 접속할때마다 호스트의 db 전체주소를 넣기에는 굉장히 불편할 것이다.

그래서 컨테이너의 이름을 적으면 접속이 가능해지도록 하면 너무 편할 것 같다.

 

그래서 shell에서는 네트워크를 만들어서 편하게 접속할 수 있도록 각각 network를 넣어 주었다.

아까 도커 네트워크 리스트를 가져오는 명령어를 쳤던 것을 다시 보자.

$ docker network ls
NETWORK ID     NAME          DRIVER    SCOPE
440dbeb16442   2nd_default   bridge    local
7fb999d3d64f   bridge        bridge    local
483de980f145   host          host      local
3f40074674cc   none          null      local

네트워크 드라이버란, 이런 도커 컨테이너를 이어주는 역할을 한다.

 

도커에서는 몇 가지 자체제공하는 기본 드라이버들이 있는데, 확인해보자.

  • 브릿지(bridge) : 기본 네트워크 드라이버로, 동일한 도커 호스트에서 컨테이너의 역할을 도와준다.
  • 호스트(host) : 호스트의 네트워크를 직접 사용한다.
  • 오버레이(overlay) : 서로 다른 도커 호스트의 컨테이너간 통신을 도와준다.

이것 외에도 여러가지 옵션들이 많고, 서드파티 플러그인도 사용할 수 있다.

https://docs.docker.com/network/drivers/

조금 더 찾아보려면, 위의 사이트에서 더 확인해보자.

 

도커 네트워크에 컨테이너들이 물려있는지 확인하려면, 다음과 같은 명령어로 확인할 수 있다.

$ docker network inspect 2nd_default

[
    {
        "Name": "2nd_default",
        "Id": "20451de16bbc38d536447367679ebb8ae1bdcffd02f93417555fe7d6724c3a7f",
        "Created": "2023-08-07T17:48:53.788668019Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "22339f66dde5c3df5d0916118800940f7b453dc222de5846d328393bb05e53bb": {
                "Name": "2nd-db-1",
                "EndpointID": "d0e3890ec7c4875280851e5a9b07a3a66d6f78c0e04f043595dadab4b68254e4",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "47ba81760d077e720177d870ad00ba0984fd9a37501f2eb65cc16fe3fee4d0be": {
                "Name": "2nd-app-1",
                "EndpointID": "4354f2a5c84a6eb73346e23f37419bed8e84fbbe76898d8c684265712948a3cb",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "2nd",
            "com.docker.compose.version": "2.19.1"
        }
    }
]

여기에서 "containers" 항목을 보면, 우리가 실행한 두 개의 컨테이너가 2nd_default라는 네트워크에 물려 있음을 확인할 수 있다.

 

또한, docker compose도 여러 옵션을 주고 실행할 수 있는데, 예를 들어 다음과 같다.

  • up: 도커 컴포즈 파일로, 컨테이너를 생성하기
  • -f: 도커 컴포즈 파일 지정하기
  • -d: 백그라운드에서 실행하기

이게 도커-컴포즈의 끝이다.

 

마지막으로, 이 docker-compose로 띄웠던 컨테이너들이 유기적으로 만드는 사이클을 살펴보고, 다른 예제도 한번 살펴보고 글을 마치자.

docker-compose를 실행하고, 브라우저에서 localhost:8080에 접속하면 다음과 같은 일이 순차적으로 일어난다.

  • 호스트의 8080번 포트와 연결된 app 컨테이너의 웹 서버가 리스닝을 하고 있다가 연결을 받는다.
  • 웹 서버는 php에게 request를 넘긴다.
  • php는 /var/www/html의 디렉토리에 있는 php application을 읽어서 그대로 실행한다.
  • -v로 마운트했으므로 호스트의 파일 시스템인 app_data에 있는 정보를 읽어온다.
  • php는 app_data에 MySQL db에 접속해서 db를 꺼내오라는 정보를 가지고 같은 네트워크의 3306번 포트에 있는 MySQL 서버에 접속한다.
  • 이 3306번 포트의 MySQL 서버는 /var/lib/mysql이라는 디렉토리에 있는 db를 꺼내와야 한다.
  • -v로 마운트했으므로 호스트의 파일 시스템인 db_data에 있는 정보를 읽어온다.
  • MySQL 서버는 php에 이 정보를 전달한다.
  • php는 이 정보를 가지고 웹 서버에 response를 전달한다.
  • 그리고 마지막으로 웹 서버가 response를 가지고 렌더링을 해서 우리에게 정보를 보여준다.

이 일련의 과정들을 설명할 수 있어야 한다.

마지막으로, 다른 docker-compose 파일을 보면서 어떻게 동작할 지 예상해 보자.

version: '3.0'

services:
  mariadb10:
    image: mariadb:10
    ports:
     - "3310:3306/tcp"
    environment:
      - MYSQL_ROOT_PASSWORD=my_db_passward
      - MYSQL_USER=docker_pro
      - MYSQL_PASSWORD=docker_pro_pass
      - MYSQL_DATABASE=docker_pro
  redis:
    image: redis
    command: redis-server --port 6379
    restart: always
    ports:
      - 6379:6379
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: 'rabbitmq'
    ports:
        - 5672:5672
        - 15672:15672
    volumes:
        - ~/.docker-conf/rabbitmq/data/:/var/lib/rabbitmq/
        - ~/.docker-conf/rabbitmq/log/:/var/log/rabbitmq
    networks:
        - rabbitmq_go_net

networks:
  rabbitmq_go_net:
    driver: bridge

 

+ Recent posts