Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@ KOIN_DATA_AIRFLOW_DB=airflow_metadata
KOIN_DATA_SUPERSET_DB=superset_metadata

# Airflow
KOIN_DATA_AIRFLOW_PORT=8080
AIRFLOW_UID=50000
AIRFLOW__CORE__EXECUTOR=LocalExecutor
AIRFLOW__CORE__LOAD_EXAMPLES=false
AIRFLOW__WEBSERVER__EXPOSE_CONFIG=false
AIRFLOW__WEBSERVER__SECRET_KEY=change_me
AIRFLOW_ADMIN_USERNAME=admin
AIRFLOW_ADMIN_PASSWORD=admin
AIRFLOW_ADMIN_FIRSTNAME=KOIN
AIRFLOW_ADMIN_LASTNAME=Admin
AIRFLOW_ADMIN_EMAIL=admin@example.com

# Superset
KOIN_DATA_SUPERSET_PORT=8088
SUPERSET_SECRET_KEY=change_me
SUPERSET_ADMIN_USERNAME=admin
SUPERSET_ADMIN_PASSWORD=admin
Expand All @@ -31,11 +28,7 @@ SUPERSET_ADMIN_LASTNAME=Admin
SUPERSET_ADMIN_EMAIL=admin@example.com

# BigQuery/dbt
DBT_TARGET=dev
GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
KOIN_DATA_GCP_PROJECT=your-gcp-project
KOIN_DATA_BIGQUERY_LOCATION=asia-northeast3
KOIN_DATA_RAW_DATASET=raw_ga4
KOIN_DATA_STAGING_DATASET=staging
KOIN_DATA_BRONZE_DATASET=bronze
KOIN_DATA_SILVER_DATASET=silver
KOIN_DATA_GOLD_DATASET=gold
KOIN_DATA_GA4_TABLE_PREFIX=events_
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sh text eol=lf
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ airflow/**/__pycache__/
dbt/**/target/
dbt/**/dbt_packages/
dbt/**/logs/
dbt/**/.user.yml

outputs/tmp/
notebooks/.ipynb_checkpoints/
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ KOIN_DATA/
plugins/
logs/

dbt/
koin/ # 초기에는 빈 폴더, 모델은 단계적으로 추가
dbt/ # 초기에는 빈 폴더, 모델은 단계적으로 추가

scripts/
docs/
Expand Down Expand Up @@ -67,6 +66,10 @@ docker compose up -d --build

- GitHub repo: `KOIN_DATA`
- Docker project: `koin-data`
- dbt project: `koin`
- dbt workspace: `dbt/`
- Python, dbt, SQL 식별자: `underscore_case`
- 환경변수 prefix: `KOIN_DATA_`

## Docker Compose Reference

- [Docker Compose setup](docs/docker-compose.md)
1 change: 1 addition & 0 deletions dbt/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Empty file removed dbt/koin/.gitkeep
Empty file.
Empty file removed dbt/koin/analyses/.gitkeep
Empty file.
Empty file removed dbt/koin/macros/.gitkeep
Empty file.
Empty file removed dbt/koin/models/bronze/.gitkeep
Empty file.
Empty file removed dbt/koin/models/marts/.gitkeep
Empty file.
Empty file removed dbt/koin/models/silver/.gitkeep
Empty file.
Empty file removed dbt/koin/models/sources/.gitkeep
Empty file.
Empty file removed dbt/koin/models/staging/.gitkeep
Empty file.
Empty file removed dbt/koin/seeds/.gitkeep
Empty file.
Empty file removed dbt/koin/snapshots/.gitkeep
Empty file.
Empty file removed dbt/koin/tests/.gitkeep
Empty file.
32 changes: 17 additions & 15 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ x-airflow-common: &airflow-common
environment:
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://${KOIN_DATA_POSTGRES_USER:-koin}:${KOIN_DATA_POSTGRES_PASSWORD:-change_me}@postgres:5432/${KOIN_DATA_AIRFLOW_DB:-airflow_metadata}
AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://${KOIN_DATA_POSTGRES_USER:-koin}:${KOIN_DATA_POSTGRES_PASSWORD:-change_me}@postgres:5432/${KOIN_DATA_AIRFLOW_DB:-airflow_metadata}
DBT_PROFILES_DIR: /opt/airflow/dbt/koin
volumes:
- ./airflow/dags:/opt/airflow/dags
- ./airflow/plugins:/opt/airflow/plugins
- ./airflow/logs:/opt/airflow/logs
- ./dbt:/opt/airflow/dbt
- ./secrets:/opt/airflow/secrets:ro
depends_on:
postgres:
Expand Down Expand Up @@ -44,24 +42,16 @@ services:

airflow-init:
<<: *airflow-common
command: >
bash -lc "airflow db migrate &&
airflow users create
--username \"$${AIRFLOW_ADMIN_USERNAME}\"
--password \"$${AIRFLOW_ADMIN_PASSWORD}\"
--firstname \"$${AIRFLOW_ADMIN_FIRSTNAME}\"
--lastname \"$${AIRFLOW_ADMIN_LASTNAME}\"
--role Admin
--email \"$${AIRFLOW_ADMIN_EMAIL}\" || true"
command: bash -lc "airflow db migrate"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Apache Airflow 3.0.1에서 api-server 사용 시 초기 관리자 사용자 자동 생성 여부와 권장 bootstrap 방식(airflow users create 필요 여부)을 확인해줘.

💡 Result:

Apache Airflow 3.0.1에서 api-server 사용 시 초기 관리자 사용자는 자동으로 생성되지 않습니다 [1]. Airflow 3는 인증 방식(Auth Manager)에 따라 사용자 관리 방법이 다르므로, 사용 중인 인증 관리자에 맞는 설정이 필요합니다 [2][3]. 1. Simple Auth Manager 사용 시 (기본값): Simple Auth Manager는 사용자 목록을 Airflow 설정 파일([core] 섹션의 simple_auth_manager_users)을 통해 관리합니다 [2]. 초기 관리자 사용자를 생성하려면 설정을 수정해야 합니다. 예시: [core] 설정에 simple_auth_manager_users = "admin:admin"과 같이 사용자명과 역할을 지정할 수 있습니다 [2]. 비밀번호는 자동으로 생성되어 $AIRFLOW_HOME/simple_auth_manager_passwords.json.generated 파일에 저장되거나 로그에 출력됩니다 [2]. 2. FAB (Flask AppBuilder) Auth Manager 사용 시: FAB Auth Manager를 사용하는 경우에는 airflow users create 명령어를 사용하여 관리자 사용자를 생성할 수 있습니다 [3][4]. 이 방식은 기존 2.x 버전과 유사하게 동작하며, 데이터베이스에 사용자가 저장됩니다 [3]. 권장 bootstrap 방식: 별도의 자동화된 환경(Docker 등)을 구성하는 경우, 위에서 설명한 각 인증 방식의 특성에 따라 다음과 같이 구성하는 것이 권장됩니다: - Simple Auth Manager: 설정 파일을 통해 사용자를 정의하고, 서버 실행 전 환경 변수나 마운트된 설정 파일을 통해 관리자 계정을 명시하십시오 [2][1]. - FAB Auth Manager: 데이터베이스 마이그레이션(airflow db migrate) 완료 후, 초기화 스크립트에서 airflow users create 명령을 실행하여 사용자를 생성하십시오 [3][4]. 따라서 Airflow 3.0.1 환경에서는 단순히 api-server를 실행한다고 해서 관리자 계정이 자동으로 생성되지 않으므로, 사용하려는 인증 체계에 맞추어 명시적인 초기화 과정을 거쳐야 합니다 [2][3][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show docker-compose.yml around the referenced line
echo "---- docker-compose.yml: lines 35-70 ----"
nl -ba docker-compose.yml | sed -n '35,70p'

echo
echo "---- Search for airflow users create / admin bootstrap ----"
rg -n "airflow users create|users create|create.*user|admin|simple_auth_manager_users|fab|Auth Manager|api-server" docker-compose*.yml docker-compose*.yaml || true

Repository: BCSDLab/KOIN_DATA

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- docker-compose.yml: lines 35-70 ----"
awk 'NR>=35 && NR<=70 {printf "%5d  %s\n", NR, $0}' docker-compose.yml

echo
echo "---- Search for airflow users create / admin bootstrap ----"
rg -n "airflow users create|users create|create.*user|admin|simple_auth_manager_users|fab|Auth Manager|api-server" docker-compose*.yml docker-compose*.yaml || true

Repository: BCSDLab/KOIN_DATA

Length of output: 1543


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- docker-compose.yml line count ----"
wc -l docker-compose.yml

echo
echo "---- docker-compose.yml (head 1-120) ----"
awk 'NR>=1 && NR<=120 {print NR ":" $0}' docker-compose.yml

echo
echo "---- docker-compose.yml (120-220) ----"
awk 'NR>=120 && NR<=220 {print NR ":" $0}' docker-compose.yml

echo
echo "---- Search for airflow init / user creation / auth manager config ----"
rg -n "airflow-init|users create|simple_auth_manager_users|AIRFLOW__CORE__AUTH|AUTH_MANAGER|AUTH_BACKEND|api-server|fab create-admin|create-admin|bootstrap|admin" docker-compose.yml docker-compose*.yml || true

echo
echo "---- Search for any mounted scripts under ./docker that mention airflow or users ----"
ls -la docker || true
fd -H --exclude-dir node_modules --exclude-dir dist --exclude-dir build . docker -t f 2>/dev/null | head -n 200 || true

# Try a targeted search for user/airflow in likely init scripts
rg -n "airflow users|users create|simple_auth_manager_users|create-admin|AUTH_MANAGER|auth manager|db migrate" docker docker-compose.yml || true

Repository: BCSDLab/KOIN_DATA

Length of output: 4930


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- docker/airflow tree ----"
find docker/airflow -maxdepth 3 -type f -print

echo
echo "---- Search for airflow user bootstrap / auth manager config in docker/airflow ----"
rg -n "airflow users create|users create|create-admin|simple_auth_manager_users|AUTH_MANAGER|auth manager|simple auth|fab|FAB" docker/airflow || true

echo
echo "---- Inspect docker/airflow/Dockerfile ----"
sed -n '1,200p' docker/airflow/Dockerfile

echo
echo "---- Search entire repo for auth manager / simple_auth_manager_users / airflow users create ----"
rg -n "AIRFLOW__.*AUTH|simple_auth_manager_users|airflow users create|api-server|FAB_AUTH|Auth Manager" . || true

echo
echo "---- Inspect .env (if present) for Airflow auth settings ----"
ls -ლა .env 2>/dev/null || true
test -f .env && sed -n '1,200p' .env || true

Repository: BCSDLab/KOIN_DATA

Length of output: 867


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- Search repo for Airflow auth manager / user bootstrap env vars ----"
rg -n "simple_auth_manager_users|SIMPLE_AUTH_MANAGER_USERS|AIRFLOW__CORE__.*AUTH|AIRFLOW__.*AUTH|AUTH_MANAGER|api-server|airflow users create|users create" . || true

echo
echo "---- Inspect docs/docker-compose.md around Airflow notes ----"
if [ -f docs/docker-compose.md ]; then
  awk 'NR>=90 && NR<=180 {print NR ":" $0}' docs/docker-compose.md
fi

Repository: BCSDLab/KOIN_DATA

Length of output: 2466


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- root files ----"
ls -la

echo
echo "---- docker-compose*.yml/yaml files ----"
find . -maxdepth 2 -type f -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' -print

echo
echo "---- search for Airflow auth/user-related config across repo ----"
rg -n "simple_auth_manager_users|simple_auth_manager_passwords|AIRFLOW__.*AUTH|AUTH_MANAGER|api-server|airflow users create|users create" . || true

echo
echo "---- search for any AIRFLOW__CORE / AIRFLOW__API settings in compose/env docs ----"
rg -n "AIRFLOW__|api-server|AUTH_BACKEND|AUTH_MANAGER|AUTHORIZ" docker-compose*.yml docker-compose*.yaml docs* . || true

echo
echo "---- check for env samples mentioning Airflow auth ----"
find . -maxdepth 3 -type f \( -iname '*.env*' -o -iname '*example*' -o -iname '*sample*' \) -print

Repository: BCSDLab/KOIN_DATA

Length of output: 3379


🌐 Web query:

Airflow 3.0.1 api-server Simple Auth Manager default behavior simple_auth_manager_users not set

💡 Result:

In Airflow 3.0.1, the Simple Auth Manager is the default authentication provider [1][2]. If the simple_auth_manager_users configuration option is not explicitly set in your airflow.cfg (or via the AIRFLOWCORESIMPLE_AUTH_MANAGER_USERS environment variable), the manager defaults to a pre-defined user configuration of admin:admin [3][4]. This default configuration creates a user named "admin" with the "admin" role [3][4]. Because the Simple Auth Manager does not manage passwords directly via the configuration file, it utilizes an auto-generated password file [5]. By default, this file is located at $AIRFLOW_HOME/simple_auth_manager_passwords.json.generated [5]. You can find the password for this default admin user within the webserver logs or by inspecting this generated file [3]. Key points regarding this behavior: - Purpose: The Simple Auth Manager is intended solely for development and testing and should not be used in production environments [1][2]. - Configuration: While admin:admin is the functional default, you can override this by providing a comma-separated list of username:role pairs to the simple_auth_manager_users setting [4]. - Password Management: Since there is no mechanism to set static passwords for these users in the configuration file, Airflow generates them automatically and stores them in the aforementioned JSON file [5]. - Alternative Authentication: If you require traditional user management (such as creating users via the CLI or using a database-backed store), you must switch to a different auth manager, such as the FAB Auth Manager, by configuring [core] auth_manager accordingly [6][3].

Citations:


🌐 Web query:

apache airflow 3 auth manager create default users api-server airflow users create requirement FAB Auth Manager

💡 Result:

In Apache Airflow 3, the default authentication component is the Simple Auth Manager, which does not support the airflow users create command [1][2][3]. To use the airflow users create CLI command, you must explicitly configure Airflow to use the Flask AppBuilder (FAB) Auth Manager [1][3][4]. To enable and use the FAB Auth Manager for user management, follow these steps: 1. Install the required provider package: pip install apache-airflow-providers-fab 2. Configure Airflow to use the FAB Auth Manager by setting the auth_manager option in your airflow.cfg file or via an environment variable [1][3][4]: In airflow.cfg: [core] auth_manager = airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager As an environment variable: export AIRFLOWCOREAUTH_MANAGER=airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager 3. Once configured, you can use the airflow users create command to manage users [5][6]: airflow users create \ --username \ --firstname \ --lastname \ --role \ --email The FAB Auth Manager supports roles such as Admin, User, Op, Viewer, and Public [5]. If you continue using the default Simple Auth Manager, users are instead managed via the airflow.cfg file under the simple_auth_manager_users setting, and passwords are auto-generated and stored in a file (defaulting to $AIRFLOW_HOME/simple_auth_manager_passwords.json.generated) [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---- .env.example (lines 1-120) ----"
sed -n '1,120p' .env.example

Repository: BCSDLab/KOIN_DATA

Length of output: 1388


🌐 Web query:

Airflow 3.0.1 Simple Auth Manager default simple_auth_manager_users value if not set admin:admin

💡 Result:

In Airflow 3.0.1, the simple auth manager default for simple_auth_manager_users (when you don’t set it in airflow.cfg) is: admin:admin [1][2]. This config value is documented as the “list of users and their associated role” for the simple auth manager, and the configuration reference shows the default as admin:admin [1]. The simple auth manager docs also show the same configuration format and example usage [2].

Citations:


🌐 Web query:

AIRFLOWCOREAUTH_MANAGER Simple Auth Manager api-server default user auto-created admin password file simple_auth_manager_passwords.json.generated

💡 Result:

In Apache Airflow, the Simple Auth Manager is the default authentication manager [1][2][3]. It is primarily intended for development and testing environments [2][4]. When Airflow starts with the Simple Auth Manager, it automatically generates passwords for users and saves them to a file [1]. By default, this file is located at $AIRFLOW_HOME/simple_auth_manager_passwords.json.generated [1]. Key aspects of this mechanism include: Password Generation: The passwords are auto-generated for each user and printed in the webserver logs [1]. File Persistence: The generated passwords are saved in the configured simple_auth_manager_passwords_file, which defaults to the.json.generated file mentioned [1][5]. Managing Passwords: Users can read or update these passwords directly within this file [1][5]. Development Convenience: In development environments like Breeze, predefined users such as admin and viewer are often created out of the box with passwords matching their usernames [1]. The Simple Auth Manager is not intended for production use; for production deployments, users are encouraged to configure a different, production-ready authentication manager [2][4].

Citations:


Airflow airflow-init이 관리자 계정/비밀번호 부트스트랩을 명시적으로 하지 않습니다.

command: bash -lc "airflow db migrate"

Airflow 3의 기본 인증(S​imple Auth Manager)은 simple_auth_manager_users를 따로 설정하지 않아도 기본 admin 사용자를 만들고, 비밀번호는 simple_auth_manager_passwords.json.generated(또는 로그)로 자동 생성합니다. 그래서 “초기 계정이 아예 없어 UI/API 진입이 불가”까지는 아닙니다.
다만 현재 docker-compose.ymlAIRFLOW_ADMIN_USERNAME/AIRFLOW_ADMIN_PASSWORD(등)를 auth 설정에 연결하지 않아서(= simple_auth_manager_users / airflow users create 미설정) 배포 환경에서 로그인 자격증명을 자동으로 확정하기 어렵습니다. 운영 진입이 목적이면 init 경로에 맞는 방식으로 사용자/비밀번호 생성을 명시해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yml` at line 47, The docker-compose command for the
airflow-init step only runs "airflow db migrate" and does not explicitly create
an admin user or wire AIRFLOW_ADMIN_USERNAME/AIRFLOW_ADMIN_PASSWORD into the
auth config; update the airflow-init service (the command currently "bash -lc
\"airflow db migrate\"") to either (a) run a user creation step after migration
(e.g., run "airflow users create ..." with the desired
AIRFLOW_ADMIN_USERNAME/AIRFLOW_ADMIN_PASSWORD env values) or (b) set the Simple
Auth Manager config by exporting/setting
AIRFLOW__AUTH__SIMPLE_AUTH_MANAGER_USERS (or equivalent env mapping) so the
admin username/password are deterministically provisioned during init; apply the
change to the airflow-init service command and/or environment variables so the
admin credentials are explicitly created.

restart: "no"

airflow-webserver:
<<: *airflow-common
command: webserver
command: api-server
ports:
- "8080:8080"
- "${KOIN_DATA_AIRFLOW_PORT:-8080}:8080"
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
test: ["CMD", "curl", "--fail", "http://localhost:8080/api/v2/monitor/health"]
interval: 30s
timeout: 10s
retries: 5
Expand All @@ -72,6 +62,18 @@ services:
command: scheduler
restart: unless-stopped

dbt:
image: ghcr.io/dbt-labs/dbt-bigquery:1.9.latest
env_file:
- path: .env
required: false
working_dir: /usr/app
volumes:
- ./dbt:/usr/app
- ./secrets:/secrets:ro
command: ["--version"]
restart: "no"

superset:
image: ${KOIN_DATA_SUPERSET_IMAGE:-koin-data-superset:latest}
build:
Expand All @@ -87,7 +89,7 @@ services:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./secrets:/app/secrets:ro
ports:
- "8088:8088"
- "${KOIN_DATA_SUPERSET_PORT:-8088}:8088"
depends_on:
postgres:
condition: service_healthy
Expand Down
6 changes: 4 additions & 2 deletions docker/airflow/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
FROM apache/airflow:2.9.3-python3.11
FROM apache/airflow:3.0.1-python3.11

COPY docker/airflow/requirements.txt /requirements.txt
RUN pip install --no-cache-dir -r /requirements.txt
RUN pip install --no-cache-dir \
--constraint "https://github.com/apache/airflow/constraints-3.0.1/constraints-3.11.txt" \
-r /requirements.txt
4 changes: 1 addition & 3 deletions docker/airflow/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
apache-airflow-providers-google==10.22.0
dbt-bigquery==1.8.2
astronomer-cosmos==1.6.0
apache-airflow-providers-google
17 changes: 13 additions & 4 deletions docker/postgres/init-databases.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

# Create service metadata databases during the first Postgres initialization.
create_database() {
local database="$1"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname postgres <<SQL
SELECT 'CREATE DATABASE ${database}'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${database}')\gexec
SQL
local exists

exists="$(
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname postgres --set=database="$database" \
--tuples-only --no-align \
--command "SELECT 1 FROM pg_database WHERE datname = :'database'"
)"

if [[ "$exists" != "1" ]]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname postgres --set=database="$database" \
--command 'CREATE DATABASE :"database"'
fi
}

create_database "${KOIN_DATA_AIRFLOW_DB:-airflow_metadata}"
Expand Down
148 changes: 148 additions & 0 deletions docs/docker-compose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Docker Compose Setup

This document summarizes the local Docker Compose setup for the KOIN data platform.

## Service Roles

| Service | Role | Long-running |
| --- | --- | --- |
| `postgres` | Metadata database for Airflow and Superset | Yes |
| `airflow-webserver` | Airflow 3 API server and UI | Yes |
| `airflow-scheduler` | DAG scheduling and task dispatching | Yes |
| `airflow-init` | Airflow metadata migration | No |
| `dbt` | dbt BigQuery command runner | No |
| `superset` | BI dashboard service | Yes |

dbt is not exposed as a web service.
It runs as a command-line container with the local `dbt/` workspace mounted at `/usr/app`.

## Ports

| Service | Host Port | Container Port | Notes |
| --- | ---: | ---: | --- |
| Airflow API/UI | `${KOIN_DATA_AIRFLOW_PORT:-8080}` | `8080` | Open `http://localhost:8080` locally by default |
| Superset UI | `${KOIN_DATA_SUPERSET_PORT:-8088}` | `8088` | Open `http://localhost:8088` locally by default |
| Postgres | Not published | `5432` | Internal Docker network only |
| dbt | None | None | dbt runs as a command-line container, not a web server |

If `8080` or `8088` is already used on a developer machine or EC2 host, change only the host-side values in `.env`:

```text
KOIN_DATA_AIRFLOW_PORT=18080
KOIN_DATA_SUPERSET_PORT=18088
```

Postgres does not publish `5432` to the host. This is intentional for local and EC2 security.
Containers should connect to Postgres with the Docker Compose service address:

```text
postgres:5432
```

## Metadata Database Split

The Postgres container is used for service metadata, not for GA4 analytics data.

| Database | Owner | Purpose |
| --- | --- | --- |
| `airflow_metadata` | Airflow | DAG runs, task states, schedules, Airflow users |
| `superset_metadata` | Superset | Dashboard metadata, charts, users, connection metadata |

The databases are created by:

```text
docker/postgres/init-databases.sh
```

The database names are controlled by:

```text
KOIN_DATA_AIRFLOW_DB=airflow_metadata
KOIN_DATA_SUPERSET_DB=superset_metadata
```

## Analytics Data Storage

Analytics data is expected to live in BigQuery, not in the metadata Postgres container.
The initial `dbt/` directory is intentionally empty so project models can be added deliberately later.

## Local Verification

Create a local `.env` file:

```bash
cp .env.example .env
```

Build and start the services:

```bash
docker compose up -d --build
```

Check service status:

```bash
docker compose ps
```

Expected result:

```text
postgres healthy/running
airflow-webserver running
airflow-scheduler running
superset running
```

Useful logs:

```bash
docker compose logs postgres
docker compose logs airflow-webserver
docker compose logs airflow-scheduler
docker compose logs superset
```

## Build Notes

The Airflow image is based on `apache/airflow:3.0.1-python3.11`.
Use the matching Airflow constraints file during `pip install` so provider versions stay compatible with the base Airflow image.

Airflow 3 uses `airflow api-server` instead of the Airflow 2 `airflow webserver` command.

dbt is kept in a separate `ghcr.io/dbt-labs/dbt-bigquery:1.9.latest` container.
This avoids dependency conflicts between Airflow 3.x provider constraints and dbt BigQuery dependencies.

For Airflow `3.0.1` and Python `3.11`, the Dockerfile uses:

```text
https://github.com/apache/airflow/constraints-3.0.1/constraints-3.11.txt
```

Shell scripts mounted into Linux containers must use LF line endings.
The Postgres initialization script creates the service metadata databases:

```text
airflow_metadata
superset_metadata
```

Keep `*.sh` files as LF to avoid errors such as:

```text
env: 'bash\r': No such file or directory
```

## EC2 Security Group

Recommended inbound rules for an EC2 test server:

| Port | Purpose | Recommendation |
| ---: | --- | --- |
| `22` | SSH | Restrict to developer IPs |
| `8080` | Airflow UI | Restrict to team/VPN IPs |
| `8088` | Superset UI | Restrict to team/VPN IPs |
| `5432` | Postgres | Do not expose publicly |

Postgres should stay inside the Docker network unless there is a strong operational reason to expose it.