Docker Compose에서 컨테이너 startup 순서 컨트롤하기

최근 패스트캠퍼스 스쿨 웹 프로젝트를 진행하면서, Docker Compose를 이용해 Django 어플리케이션과 PostgreSQL를 실행하고 있다.

아래는 이를 위해 내가 작성한 Dockerfile, docker-compose.yml, docker-entry.sh 파일들이다.

[Dockerfile]

FROM python:3.5  
MAINTAINER Seunghwan Joo <shjoo@fastcampus.co.kr>

ENV PYTHONUNBUFFERED 1  
ENV DJANGO_SETTINGS_MODULE school.settings.development  
ENV DATABASE_URL postgres://postgres@db:5432/postgres

RUN mkdir /code

ADD requirements /code/requirements  
ADD school /code/school  
ADD requirements.txt /code/  
ADD Procfile /code/  
ADD Makefile /code/  
ADD docker-entrypoint.sh /code/

WORKDIR /code

RUN pip install -r requirements/development.txt

RUN chmod +x docker-entrypoint.sh  
ENTRYPOINT ./docker-entrypoint.sh 

EXPOSE 8000  



[docker-compose.yml]

version: '2'  
services:  
  db:
    image: postgres
    container_name: school_db 
    volumes:
      - /data/postgres:/var/lib/postgresql/data
  web:
    image: school:latest
    container_name: school_web
    ports:
      - '8000:8000'
    links:
      - db:db



[docker-entrypoint.sh]

#!/bin/bash

# Apply database migrations
echo "Apply database migrations"  
make migrate

# Start server
echo "Start server"  
python school/manage.py runserver 0.0.0.0:8000  



간단히 설명하면, 먼저 내가 만든 Django 어플리케이션을 school:latest라는 이미지로 빌드하였다. 그리고 이 이미지가 컨테이너로 실행될 때 docker-entrypoint.sh가 실행되도록 하였다. 이 안에는 데이터베이스를 migration하기 위한 명령어와 서버를 구동하기 위한 명령어들을 적어놓았다.
PostgreSQL 컨테이너를 만들기 위해 Docker에서 기본적으로 제공하는 postgres 이미지를 사용하였고 내 로컬의 /data/postgres/를 볼륨으로 연결하였다. 그리고 links를 통해 이를 Django 어플리케이션 컨테이너와 연결하였다.

하지만 $ docker-compose up 명령어를 실행하면,

위와 같은 에러가 발생하였다. 그리고 이 에러의 원인이, 데이터베이스가 미처 startup 되기 전에 web 컨테이너가 연결을 시도했기 때문이라는 것을 알게 되었다. links(또는 depends_on)로 두 컨테이너를 연결했는데도 왜 이와 같은 문제가 발생한 걸까?
나는 처음에 linksdepends_on가 이런 dependency 문제를 당연히 해결해주리라고 생각했었다. 하지만 Docker 공식 문서에 아래와 같이 적혀있었다.

Note: depends_on will not wait for db and redis to be “ready” before starting web - only until they have been started. If you need to wait for a service to be ready, see Controlling startup order for more on this problem and strategies for solving it.

linksdepends_on 은 단지 컨테이너가 시작되는 순서만 컨트롤할 뿐, 컨테이너 안의 서비스가 "ready"(실제 실행 가능한 상태)인지 아닌지에 대해서까지는 관여하지 않는다. 어떻게 보면 얘들은 컨테이너들을 일단 순서에 맞게 시작만 시켜놓고 그 뒤는 나몰라라하는 무책임한 녀석이었다.
따라서 이를 컨트롤하기 위해서는 다른 방법을 사용하여야 한다. stackoverflow에 이를 위한 여러 답변들이 있었다.

1. restart: always 사용하기

컨테이너에 restart: always를 설정하면, 컨테이너가 실행 중 중단됐을 때(여기서는 어떤 특정 서비스가 ready 상태가 아직 안 돼 중단된 경우) 컨테이너를 다시 알아서 재시작을 해준다.

아래와 같이 docker-compose.yml에서 web 서비스에 restart: always를 추가해주었다.

version: '2'  
services:  
  db:
    image: postgres
    container_name: school_db 
    volumes:
      - /data/postgres:/var/lib/postgresql/data
  web:
    image: school:latest
    container_name: school_web
    ports:
      - '8000:8000'
    links:
      - db:db
    restart: always

하지만 $ docker-compose up를 실행하였을 때, 그전과 똑같은 에러가 발생하였다.

그 원인을 확실히는 잘 모르겠지만, 어떤 분의 답변에 의하면 실행 중 에러가 발생했을 때 "exit" 되지 않는 컨테이너에서는 작동되지 않는다고 한다.(실제로 web 컨테이너에서 에러가 발생했을 때 web 컨테이너는 exit되지 않았고, $ docker ps를 해보면 여전히 실행 중임을 볼 수 있다.

2. wait-for-it이나 dockerize와 같은 툴 이용하기

이런 문제를 해결하기 위해 만들어진 여러 툴들이 있다. 나는 여기서 dockerize라는 툴을 사용해 보았다.

먼저 web 컨테이너에 dockerize를 사용할 수 있게 설치를 해주어야 한다. Dockerfiledockerize를 설치하기 위한 명령어를 추가해주었다.

FROM python:3.5  
MAINTAINER Seunghwan Joo <shjoo@fastcampus.co.kr>

ENV PYTHONUNBUFFERED 1  
ENV DJANGO_SETTINGS_MODULE school.settings.development  
ENV DATABASE_URL postgres://postgres@db:5432/postgres

ENV DOCKERIZE_VERSION v0.2.0  
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \  
    && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz

...이하 생략...

그리고 docker-entrypoint.sh에서 데이터베이스를 migration 하기 전, db 컨테이너의 실제 PostgreSQL 서비스가 실행가능한 상태가 될 때까지 기다리기 위한 코드를 추가해주어야 한다. timeout은 기본적으로 10s인데, 10초보다 더 걸리는 경우가 가끔씩 발생하여 안전하게 20s로 늘려주었다.

#!/bin/bash

dockerize -wait tcp://db:5432 -timeout 20s

# Apply database migrations
echo "Apply database migrations"  
make migrate

...이하 생략 ...

$ docker-compose up를 다시 실행해보면, web 컨테이너는 db 컨테이너의 서비스가 실행가능할 때까지 기다리다가

PostgreSQL 서비스가 ready 상태가 되면 그제서야 다음 명령어인 migration을 실행한다.

3. script에서 Netcat을 이용하여 해당 서비스가 실행이 될 때까지 강제로 기다리게 하기

Netcat은 TCP나 UDP 프로토콜을 사용하는 네트워크 연결에서 데이터를 읽고 쓰는 간단한 유틸리티 프로그램이다. 이를 이용하면 PostgreSQL 서비스의 실행 유무를 네트워크 연결 상태로 체크할 수 있다.(앞에서 다루었던 dockerize의 원리와 동일하다.)

먼저 web 컨테이너에 netcat를 사용할 수 있게 설치를 해주어야 한다. Dockerfilenetcat를 설치하기 위한 명령어를 추가해주었다.

FROM python:3.5  
MAINTAINER Seunghwan Joo <shjoo@fastcampus.co.kr>

ENV PYTHONUNBUFFERED 1  
ENV DJANGO_SETTINGS_MODULE school.settings.development  
ENV DATABASE_URL postgres://postgres@db:5432/postgres

RUN apt-get update && apt-get install netcat-openbsd -y

...이하 생략...

그리고 docker-entrypoint.sh에서 데이터베이스를 migration 하기 전, netcat를 이용한 별도의 script를 넣어주었다.

#!/bin/bash

postgres_host="db"  
postgres_port=5432

# Wait for the postgres docker to be running
while ! nc $postgres_host $postgres_port; do  
  >&2 echo "Postgres is unavailable - sleeping"
  sleep 1
done

# Apply database migrations
echo "Apply database migrations"  
make migrate

...이하 생략 ...

이렇게 하면 Host 이름이 db이고, Port가 5432번인 연결이 확인될 때까지 무한반복문을 돌게 된다.

$ docker-compose up를 다시 실행해보면, web 컨테이너는 db 컨테이너의 서비스가 실행가능할 때까지 기다린 후, migration을 실행함을 볼 수 있다.