
타르코프 도서관은 단일 Desktop 서버 입니다.
하나의 서버에 모든 서비스들이 올라가야 합니다. (nextjs, fastapi, kafka etc...)
사용하다보면 Python을 많이 쓰게 되는데 다수의 파이썬 버전을 억지로 통합하여 사용하기는 조금 힘들었습니다.
구축할 시기에는 Airflow가 아무리 최신이여도 Python 지원이 조금 느렸습니다.
그래서 설치도 간단하게 할 겸 Docker를 사용했습니다.
또한 Nginx Proxy Manager(NPM)은 local에 바로 설치하는 예제가 별로 없던 부분도 있습니다.
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를 사용하는 이유를 잠시 설명하고 가겠습니다.
사이트 호스팅을 위해서 저희는 도메인을 구입한 상태 입니다.
Cloudflare를 연결하여 사용하는데 이때, 리버스 프록시와 SSL 관리를 GUI로 간단하게 지원해주어서 사용하게되었습니다.
사용해보시면 아는데 Nginx를 사용하여 관리하는 것보다 훨씬 편합니다.
또 단일 서버니 Nginx 까지 써서 하는건 욕심인 거 같아서 단호하게 쳐냈습니다.
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번 포트로 접속하시면 됩니다.
초기 계정

저희는 Tarkov Dev 라는 사이트에서 제공해주는 API를 가지고 매시간, 1일 단위로 스케줄링 하여 데이터를 갱신하고 있습니다.
별도로 프로세스를 여러개 띄워서 하기 보다는, 이 방식 관리하기 좋은 것 같아 사용하고 있습니다.
현재 사용중인 Dag는 13개 입니다.
version: 3.1.5
주요 설정
| 디렉토리 | 용도 | 동작 방식 | 비고 |
|---|---|---|---|
dags | 작성한 DAG 코드 보관 | Git 저장소에서 git pull로 최신화 | Airflow 스케줄 대상 |
plugins | 커스텀 Plugin 보관 | Git 저장소에서 git pull로 최신화 | Operator/Hook 등 확장 기능 |
logs | Airflow 실행 로그 저장 | 작업 실행 시 자동 생성 | 용량 증가 주의 |
tmp | 대용량 한·영·일 데이터 임시 저장 | XCom 대신 JSON 파일로 저장 후 읽기 | 대용량 처리 최적화 목적 |
latest_data | PostgreSQL 백업 데이터 보관 | 배치/백업 작업으로 파일 적재 | 장기 보관 및 복구 용도 |
# 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
# 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"
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 관련 종속이 사라져서 에러 납니다.
실행 하면서 틈틈이 보며 에러가 나는 경우 확인 합니다.
docker compose up --build
지정한 Port(8083)로 접속하면 정상적으로 된 것을 확인할 수 있습니다.

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