Docker 기반 NPM, Airflow 구축하기

이선엽·2026년 2월 24일
post-thumbnail

Docker를 왜 썼을까

타르코프 도서관은 단일 Desktop 서버 입니다.

하나의 서버에 모든 서비스들이 올라가야 합니다. (nextjs, fastapi, kafka etc...)

사용하다보면 Python을 많이 쓰게 되는데 다수의 파이썬 버전을 억지로 통합하여 사용하기는 조금 힘들었습니다.

구축할 시기에는 Airflow가 아무리 최신이여도 Python 지원이 조금 느렸습니다.

그래서 설치도 간단하게 할 겸 Docker를 사용했습니다.

또한 Nginx Proxy Manager(NPM)은 local에 바로 설치하는 예제가 별로 없던 부분도 있습니다.

Docker 설치

Docker 부터 차근차근히 가보겠습니다.

기반은 Rocky Linux 10 입니다.

# 저장소 추가
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# 설치
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 활성화
sudo systemctl enable --now docker

이렇게 하면 Docker는 간단히 설치가 끝납니다.

Nginx Proxy Manager

여기서 Nginx Proxy Manager를 사용하는 이유를 잠시 설명하고 가겠습니다.

사이트 호스팅을 위해서 저희는 도메인을 구입한 상태 입니다.

Cloudflare를 연결하여 사용하는데 이때, 리버스 프록시와 SSL 관리를 GUI로 간단하게 지원해주어서 사용하게되었습니다.

사용해보시면 아는데 Nginx를 사용하여 관리하는 것보다 훨씬 편합니다.

또 단일 서버니 Nginx 까지 써서 하는건 욕심인 거 같아서 단호하게 쳐냈습니다.

Nginx Proxy Manager 설치

version: 2.13.5

Docker를 사용하여 설치 합니다.

docker-compose.yml 파일 위치는 아무곳에나 하셔도 됩니다.
여기서는 /home/tool/npm 에 구축 합니다.

# 폴더 생성
mkdir -p /home/tool/npm
mkdir -p /home/tool/npm/data
mkdir -p /home/tool/npm/letsencrypt

# docker-dompose.yml 작성
vi docker-dompose.yml

services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm
    ports:
      - "80:80"
      - "81:81"
      - "443:443"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    restart: unless-stopped
    
# 실행
docker-compose up -d

주의할 점

Port는 건들면 안됩니다.
저도 다른 Port를 사용해서 연동 하고 싶었는데, Cloudflare는 무조건 80 Port를 바라봅니다.

저도 해결 방법을 찾아보았는데 못찾아서 수정을 안하고 사용하는 것으로 일단 결정 했습니다.

만약 SKB 인터넷을 사용하신다면 꼭 확인하세요
SKB는 자체적으로 80 Port를 사용하지 못하게 막아 두었습니다.
저희도 이것 때문에 한 번 난리가 났었습니다. 지금은 다시 LG로 원복

접속 및 확인

UI는 81번 포트로 접속하시면 됩니다.

초기 계정

  • id: admin@example.com
  • password: changeme

Airflow

저희는 Tarkov Dev 라는 사이트에서 제공해주는 API를 가지고 매시간, 1일 단위로 스케줄링 하여 데이터를 갱신하고 있습니다.

별도로 프로세스를 여러개 띄워서 하기 보다는, 이 방식 관리하기 좋은 것 같아 사용하고 있습니다.

현재 사용중인 Dag는 13개 입니다.

Airflow 설치

version: 3.1.5

주요 설정

  1. Airflow를 설치하고 볼륨을 공유하는 방식으로 사용합니다.
  2. Meta DB는 기존에 설치한 PostgreSQL을 사용합니다.
  3. UI Port는 8083, 아이디 비밀번호는 dev를 사용합니다.
  4. 단일 서버로 LocalExecutor 방식을 사용합니다.
디렉토리용도동작 방식비고
dags작성한 DAG 코드 보관Git 저장소에서 git pull로 최신화Airflow 스케줄 대상
plugins커스텀 Plugin 보관Git 저장소에서 git pull로 최신화Operator/Hook 등 확장 기능
logsAirflow 실행 로그 저장작업 실행 시 자동 생성용량 증가 주의
tmp대용량 한·영·일 데이터 임시 저장XCom 대신 JSON 파일로 저장 후 읽기대용량 처리 최적화 목적
latest_dataPostgreSQL 백업 데이터 보관배치/백업 작업으로 파일 적재장기 보관 및 복구 용도
# image 다운로드
docker pull apache/airflow:latest-python3.13

# 필요한 디렉토리 생성 및 권한, 소유자 등록
mkdir latest_data config tmp logs
chmod 777 -R latest_data config tmp logs eft-library-airflow
chwon 50000:50000 -R latest_data config tmp logs eft-library-airflow

# JWT 값 만들기
openssl rand -base64 16

# yml 작성 
vi docker-compose.yml

주의할 점

3.X 버전 부터 web-server라는 단어는 api-server로 변경 되었습니다.

또한 아키텍처 단순화, 유지보수성 개선을 위해서 로그인이 기존 FabManager가 사라지고, SimpleAuthManager로 변경 되었습니다.

만약 role이나 이런 제약 없이 간편한게 사용한다면, SimpleAuthManager로 바꿔서 사용하면 됩니다.

SimpleAuthManager를 사용할 경우 Create User 명령어는 제외됩니다. admin 계정에 비밀번호는 자동으로 만드니 api-server의 log를 보시면 초반에 나옵니다.

# simple auth manger 사용하기
environment:
  AIRFLOW__WEBSERVER__AUTH_MANAGER: airflow.auth.managers.simple.simple_auth_manager.SimpleAuthManager

기존 방식인 FabManager를 쓸 경우 AIRFLOWAPI_AUTHJWT_SECRET를 추가해줘야 합니다.

공식파일에는 해당 env 정보가 없습니다. 그래서 그대로 진행할 경우,
Airflow는 정상적으로 구축되나 dag를 실행하면, 감사로그에서 StateMistMatch가 발생합니다.

처음에 state를 만들어서 api-server에서 fab 인증서버로 요청을 날리는데 저 값을 비우고 하면 전부 별도의 jwt가 생기게 됩니다.

api-server: A
dag-processor: B
trigger: C
scheduler: D

이 상태에서 dag run 시 api 요청을 할 때 서로 다른 jwt를 가지고 있어 인증을 실패합니다.

그러니 주의 합시다. 이거 때문에 20번 설치하고 지우고 했습니다...

만약 DB 계정 비밀번호에 특수문자가 들어간다면 특수문자를 URL Encoding으로 치환해서 넣어줘야 합니다.

-- 잘못된 예시
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://dev:dev!23$@localhost:1234/airflow_meta

-- 제대로 된 예시
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://dev:dev%2123%24@localhost:1234/airflow_meta

Airflow yml 작성

# JWT Secret 추가 - <openssl rand -base64 16> 로 생성한 값
AIRFLOW__API_AUTH__JWT_SECRET: 'pTirJtP+QjqHB0YgUcNNaCPtUdEmIrOjNt7QnQXX8XE='
x-airflow-common:
  &airflow-common
  image: apache/airflow:latest-python3.13
  build: ./docker-airflow-fab
  environment:
    &airflow-common-env
    AIRFLOW__CORE__EXECUTOR: LocalExecutor
    AIRFLOW__CORE__AUTH_MANAGER: airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager
    AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://dev:dev@localhost:1234/airflow_meta
    AIRFLOW__CORE__FERNET_KEY: 'JkJ70Vjye2YeQ+t2ItlrhHqwjzp/vb2/cTOBka8GOrg='
    AIRFLOW__API_AUTH__JWT_SECRET: 'pTirJtP+QjqHB0YgUcNNaCPtUdEmIrOjNt7QnQXX8XE='
    AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true'
    AIRFLOW__CORE__LOAD_EXAMPLES: 'False'
    AIRFLOW__CORE__EXECUTION_API_SERVER_URL: 'http://airflow-apiserver:8080/execution/'
    AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: 'true'
    _PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:-}
  volumes:
    - /home/airflow/eft-library-airflow/dags:/opt/airflow/dags
    - /home/airflow/eft-library-airflow/plugins:/opt/airflow/plugins
    - /home/airflow/logs:/opt/airflow/logs
    - /home/airflow/tmp:/opt/airflow/tmp
    - /home/airflow/latest_data:/opt/airflow/latest_data
  user: "${AIRFLOW_UID:-50000}:0"

services:
  airflow-apiserver:
    <<: *airflow-common
    command: api-server
    ports:
      - "8083:8080"
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8080/api/v2/version"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always

  airflow-scheduler:
    <<: *airflow-common
    command: scheduler
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8974/health"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always

  airflow-dag-processor:
    <<: *airflow-common
    command: dag-processor
    healthcheck:
      test: ["CMD-SHELL", 'airflow jobs check --job-type DagProcessorJob --hostname "$${HOSTNAME}"']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always

  airflow-triggerer:
    <<: *airflow-common
    command: triggerer
    healthcheck:
      test: ["CMD-SHELL", 'airflow jobs check --job-type TriggererJob --hostname "$${HOSTNAME}"']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always

  airflow-init:
    <<: *airflow-common
    entrypoint: /bin/bash
    command:
      - -c
      - |
        if [[ -z "${AIRFLOW_UID}" ]]; then
          echo
          echo -e "\033[1;33mWARNING!!!: AIRFLOW_UID not set!\e[0m"
          echo "If you are on Linux, you SHOULD follow the instructions below to set "
          echo "AIRFLOW_UID environment variable, otherwise files will be owned by root."
          echo "For other operating systems you can get rid of the warning with manually created .env file:"
          echo "    See: https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#setting-the-right-airflow-user"
          echo
          export AIRFLOW_UID=$$(id -u)
        fi
        one_meg=1048576
        mem_available=$$(($$(getconf _PHYS_PAGES) * $$(getconf PAGE_SIZE) / one_meg))
        cpus_available=$$(grep -cE 'cpu[0-9]+' /proc/stat)
        disk_available=$$(df / | tail -1 | awk '{print $$4}')
        warning_resources="false"
        if (( mem_available < 4000 )) ; then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough memory available for Docker.\e[0m"
          echo "At least 4GB of memory required. You have $$(numfmt --to iec $$((mem_available * one_meg)))"
          echo
          warning_resources="true"
        fi
        if (( cpus_available < 2 )); then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough CPUS available for Docker.\e[0m"
          echo "At least 2 CPUs recommended. You have $${cpus_available}"
          echo
          warning_resources="true"
        fi
        if (( disk_available < one_meg * 10 )); then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough Disk space available for Docker.\e[0m"
          echo "At least 10 GBs recommended. You have $$(numfmt --to iec $$((disk_available * 1024 )))"
          echo
          warning_resources="true"
        fi
        if [[ $${warning_resources} == "true" ]]; then
          echo
          echo -e "\033[1;33mWARNING!!!: You have not enough resources to run Airflow (see above)!\e[0m"
          echo "Please follow the instructions to increase amount of resources available:"
          echo "   https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#before-you-begin"
          echo
        fi
        echo
        echo "Creating missing opt dirs if missing:"
        echo
        mkdir -v -p /opt/airflow/{logs,dags,plugins,config}
        echo
        echo "Airflow version:"
        /entrypoint airflow version
        echo
        echo "Files in shared volumes:"
        echo
        ls -la /opt/airflow/{logs,dags,plugins,config}
        echo
        echo "Running airflow config list to create default config file if missing."
        echo
        /entrypoint airflow config list >/dev/null
        echo
        echo "Files in shared volumes:"
        echo
        ls -la /opt/airflow/{logs,dags,plugins,config}
        echo
        echo "Change ownership of files in /opt/airflow to ${AIRFLOW_UID}:0"
        echo
        chown -R "${AIRFLOW_UID}:0" /opt/airflow/
        echo
        echo "Change ownership of files in shared volumes to ${AIRFLOW_UID}:0"
        echo
        chown -v -R "${AIRFLOW_UID}:0" /opt/airflow/{logs,dags,plugins,config}
        echo
        echo "Files in shared volumes:"
        echo
        ls -la /opt/airflow/{logs,dags,plugins,config}

    environment:
      <<: *airflow-common-env
      _AIRFLOW_DB_MIGRATE: 'true'
      _AIRFLOW_WWW_USER_CREATE: 'true'
      _AIRFLOW_WWW_USER_USERNAME: 'dev'
      _AIRFLOW_WWW_USER_PASSWORD: 'dev'
      _PIP_ADDITIONAL_REQUIREMENTS: ''
    user: "0:0"

Docker File 생성

3.X 버전 부터 FabManager가 제외되어서 관련 패키지를 추가해야 합니다.

mkdir docker-airlfow-fab
vi docker-airlfow-fab/Dockerfile

# 내용
FROM apache/airflow:latest-python3.13

ENV PIP_NO_CACHE_DIR=1

RUN pip install \
    apache-airflow-providers-fab \
    flask_appbuilder \
    flask-session \
    "connexion<3"

connextion을 3보다 낮게 설치하는 이유는 3부터 flask 관련 종속이 사라져서 에러 납니다.

Airflow 실행

실행 하면서 틈틈이 보며 에러가 나는 경우 확인 합니다.

docker compose up --build

접속 및 확인

지정한 Port(8083)로 접속하면 정상적으로 된 것을 확인할 수 있습니다.

설정들

NPM과 도메인 연동, Airflow 세부 설정들은 별도의 포스트로 작성하겠습니다.

profile
만드는게 좋아

0개의 댓글