Home DevOps Airflow Docker Compose: The Setup That Wiped Our DAGs
Intermediate 3 min · September 04, 2026

Airflow Docker Compose: The Setup That Wiped Our DAGs

Airflow Docker Compose wiped DAGs and history with one down -v.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 30 min
  • You run Docker and docker compose on a dev host
  • You understand volumes, bind mounts, and .env files
  • You have the official Airflow compose file nearby
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Official Airflow docker-compose runs webserver, scheduler, workers, postgres, redis, and Flower as versioned services on one host
  • Key components: named volumes for DB and DAGs, ./dags bind mounts, db migrate step, restart policies, --scale worker for capacity
  • Performance insight: bind-mounted DAGs appear in the UI within 30 seconds versus 4-minute image rebuilds, speeding dev loops 8x
  • Production insight: down -v deletes anonymous volumes permanently, so name postgres-db-volume and back it up before any cleanup
✦ Definition~90s read
What is Airflow Docker Compose Setup?

The official Airflow docker-compose stack runs scheduler, webserver, workers, postgres, and redis as versioned containers, persisting state in named volumes with bind-mounted DAGs for fast iteration.

Think of Airflow on compose as a food truck with fridges.
Plain-English First

Think of Airflow on compose as a food truck with fridges. Named volumes are fridges bolted to the truck that keep food overnight. Anonymous volumes are cooler bags you toss at closing. Running down -v throws every bag and unbolted fridge in the dumpster, so breakfast starts from zero.

The cleanup command looked harmless. One docker compose down -v to reclaim disk, one fresh up, and the entire Airflow history was gone.

DAGs, run history, connections, all of it lived in volumes that -v just deleted. The stack came up smiling and empty.

Compose is the fastest way to run Airflow and the fastest way to wipe it. You'll learn the volume and startup discipline here.

The Official Compose File, Service by Service

The official compose file is a small fleet: postgres for metadata, redis as Celery broker, scheduler, airflow-dag-processor, api-server on port 8080, workers, triggerer for deferrable tasks, and a one-shot airflow-init service, plus optional flower on port 5555 via --profile flower. Each container gets its role from the same image with a different command, which keeps versions consistent.

Fetch the versioned file with curl -LfO 'https://airflow.apache.org/docs/apache-airflow/3.3.1/docker-compose.yaml'. Read it service by service before changing anything. Postgres holds all state, redis holds task messages, scheduler orders work, workers execute, api-server renders the UI, triggerer runs deferrable event loops. Remove any piece without understanding its contract and the stack fails in confusing ways.

Pin the image tag to a tested Airflow version. Floating latest turns every recreate into a potential schema migration you didn't plan. Budget 4GB RAM minimum for Docker (8GB breathes), and use Compose v2.14+ since v1 can't parse the file. The compose quick-start carries no production security guarantees; graduate to the Helm chart when you outgrow one host.

📊 Production Insight
A floating latest tag upgraded the DB schema on a routine restart.
Scheduler crash-looped 40 minutes until migrate ran.
Rule: pin image tags and migrate deliberately.
🎯 Key Takeaway
Seven services, one image, distinct commands per role.
Postgres holds state, redis holds messages, scheduler orders, workers run.
Pin tags so recreates don't become surprise upgrades.

Named Volumes vs Anonymous Disaster

Named volumes survive docker compose down; anonymous ones don't. Name postgres-db-volume and any DAG volume explicitly so routine restarts keep state. Treat down -v as a data-destruction command that needs a backup first.

Bind mounts connect repo folders into containers: ./dags for code, ./logs for task logs, ./plugins for extensions, plus .env for AIRFLOW_UID. Mounts must appear on scheduler, worker, and webserver identically or each service sees different DAGs.

Back up the DB volume nightly. A pg_dump cron plus a volume snapshot turns a -v disaster from a resume event into a restore drill.

compose/airflow-compose.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
services:
  postgres:
    image: postgres:16
    volumes:
      - postgres-db-volume:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U airflow"]
      interval: 10s
      retries: 5
    restart: unless-stopped

  airflow-scheduler:
    image: apache/airflow:3.3.1
    command: scheduler
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - ./dags:/opt/airflow/dags
      - ./logs:/opt/airflow/logs
      - ./plugins:/opt/airflow/plugins
    env_file: [.env]
    restart: unless-stopped

volumes:
  postgres-db-volume:  # named: survives `down` without -v
    name: airflow_postgres_db
📊 Production Insight
Anonymous postgres volume plus down -v erased 6 months of history.
Named volume plus nightly pg_dump restored in 25 minutes next time.
Rule: name it, mount it, back it up.
🎯 Key Takeaway
Named volumes persist; anonymous volumes vanish with -v.
Mount DAGs identically on all three core services.
Backups turn -v disasters into drills.

DAG Sync: Bind Mount vs Git-Sync

Bind mounts win for dev: save a DAG file and the scheduler parses it within 30 seconds. Git-sync sidecars win for staging mirrors: they pull a branch on an interval so the stack tracks version control. Baked images win for prod: DAGs ship inside the image digest with atomic rollback.

Don't mix models on one stack without documenting it. A teammate editing ./dags while git-sync overwrites the same folder creates phantom diffs that waste afternoons.

Whatever you pick, keep DAG delivery in Git. Container-local edits evaporate on recreate and can't be reviewed or rolled back.

📊 Production Insight
Mixed mount plus git-sync overwrote dev edits every 3 minutes.
One documented model cut ghost diffs to zero.
Rule: pick one sync path per environment.
🎯 Key Takeaway
Mounts for speed, git-sync for branch mirrors, images for prod.
One stack, one sync model, documented.
Container-local edits are already lost.

Running Migrations Safely

First boot needs an init pass on every OS: docker compose up airflow-init creates the metadata tables and the airflow/airflow login, exiting 0 when done. Seed a reviewable airflow.cfg first with docker compose run airflow-cli airflow config list. Then start the fleet with docker compose up and confirm healthy containers via docker ps.

Migrations must run after every image change and before the scheduler starts. The scheduler crash-looping on a new schema is the classic skipped-migration symptom. Verify with airflow db check, not by watching the UI hopefully. CLI access goes through a service container (docker compose run airflow-worker airflow info) or the airflow.sh wrapper for bash and python shells.

Order startups after reboots: postgres healthy first, then redis, then scheduler and workers. depends_on with service_healthy enforces it; plain depends_on only orders container start, not readiness. Keep AIRFLOW_UID in .env set to your host UID. Without it, container-written logs and DAG files come back root-owned and block the next edit. On SELinux hosts add :z to volume mounts.

ops/compose_upgrade.shBASH
1
2
3
4
5
6
7
8
9
10
11
# Safe upgrade path: migrate before serving traffic
docker compose pull
# run migrations as a one-shot (matches image version)
docker compose run --rm airflow-cli airflow db migrate
docker compose run --rm airflow-cli airflow db check
# only then restart the long-lived services
docker compose up -d postgres redis
sleep 10
docker compose up -d airflow-scheduler airflow-worker airflow-webserver

docker compose ps --format "table {{.Name}}\t{{.Status}}"
📊 Production Insight
Skipped migrate after a minor bump caused 500s for 2 hours.
Migrate-then-check sequence cut upgrade incidents to zero.
Rule: db check gates every scheduler start.
🎯 Key Takeaway
Migrate, check, then start; never start and hope.
Health-gated startup beats start-order superstition.
AIRFLOW_UID prevents root-owned file lockouts.

Restart Policies That Survive Reboots

Restart policies decide what survives a host reboot. unless-stopped on scheduler, workers, broker, and postgres brings the fleet back without human clicks. Pair them with healthchecks so Docker restarts genuinely wedged services, not just exited ones.

Startup ordering uses health, not hope. Postgres gets a pg_isready healthcheck; scheduler waits on service_healthy. Redis gets a similar gate before Celery workers subscribe.

Test reboots in staging. Rebooting prod to discover ordering bugs is a career-limiting experiment.

📊 Production Insight
No restart policy left half the stack down after patching.
unless-stopped plus health gates restored clean boots.
Rule: every core service restarts itself.
🎯 Key Takeaway
unless-stopped everywhere that matters, health-gated ordering.
Healthchecks turn restarts into recovery.
Rehearse reboots in staging, not prod.

Scaling Services With Compose

Compose scales workers horizontally on one host with docker compose up -d --scale airflow-worker=3. That triples Celery throughput in seconds for morning peaks. It doesn't add hosts, so watch CPU, memory, and DB connections as you scale. Custom needs ride custom images: swap image: for build: ., add a Dockerfile FROM apache/airflow:3.3.1 that pins apache-airflow==${AIRFLOW_VERSION} while pip-installing requirements.txt, then docker compose build or --build on up.

Two shortcuts save afternoons. _PIP_ADDITIONAL_REQUIREMENTS installs small extras at container start without a rebuild, handy for lxml-class additions during iteration. Host-reaching DAGs need extra_hosts: host.docker.internal:host-gateway on the worker plus host.docker.internal URLs, since localhost inside a container isn't your laptop.

Beyond one host's capacity, move to multi-host Celery or Kubernetes. Compose got you to product-market fit; orchestrators carry you past it. Monitor per-worker concurrency as you scale. Three workers at concurrency 8 need 24 DB connections plus headroom, and postgres defaults punish optimists.

⚠ Down -V Deletes Data by Design
Treat docker compose down -v as rm -rf for your metadata. Back up postgres-db-volume first, confirm the backup restores, and use plain down for routine restarts.
📊 Production Insight
Scaling to 5 workers without DB headroom caused connection timeouts.
Capped at 3 with tuned concurrency drained peaks cleanly.
Rule: scale against DB connections, not vibes.
🎯 Key Takeaway
Scale workers with --scale for quick wins on one host.
Watch CPU, RAM, and DB connections as you add replicas.
Graduate to real orchestration past one box.
● Production incidentPOST-MORTEMseverity: high

The Compose Cleanup That Wiped Our DAGs. One down -v deleted the metadata database.

Symptom
After the Friday 6 PM cleanup the UI showed zero DAGs and zero run history on Monday 9 AM. All 12 connections and 30 variables were gone, 18 scheduled weekend runs never fired, and logs showed a fresh database initializing instead of the old one. The team first suspected a bad image pull before docker volume ls showed the data volume was missing.
Assumption
The 4-person platform team assumed down -v only removed stopped containers and cached layers. They thought DAGs lived in the image and history lived somewhere safe, so they didn't map which volume held postgres data versus disposable cache before running cleanup at 6 PM on Friday.
Root cause
The compose file used anonymous volumes for postgres data and DAG storage. docker compose down -v deletes volumes by design, so one disk-cleanup command wiped the metadata DB holding 6 months of run history, 40 DAGs, connections, and variables. The stack restarted cleanly against an empty database with nothing to show.
Fix
They ran docker volume ls, confirmed postgres-db-volume was gone, and restored the 2 AM pg_dump backup into a fresh named volume in 25 minutes. Then they pinned postgres-db-volume and scheduler-dags-volume as named volumes in compose and moved DAGs to a ./dags bind mount checked into Git. Routine restarts switched to docker compose down without -v, and they don't allow down -v without a verified backup.
Key lesson
  • Named volumes are the only volumes that survive routine operations; don't treat anonymous storage as durable.
  • DAG code belongs in Git with a mount, never inside container-only storage.
Production debug guideRecover wiped volumes, failed migrations, and silent DAG mounts.4 entries
Symptom · 01
Stack comes up empty with zero DAG history after a cleanup
Fix
Run docker compose ps and docker volume ls | grep airflow. If postgres-db-volume is missing, -v deleted it. Restore from backup, then restart with docker compose up -d postgres && docker compose run airflow-cli airflow db check. Add the volume back to version control.
Symptom · 02
Webserver 500s and scheduler crash-loops after image bump
Fix
Run docker compose logs postgres --tail 50 and look for role or database errors. If airflow-init never exited 0, rerun docker compose up airflow-init first. Then docker compose run airflow-cli airflow db migrate && airflow db check. Start api-server and scheduler only after check passes.
Symptom · 03
Local DAG edits never appear in the UI
Fix
Run docker compose config | grep -A 5 'dags:' to confirm the ./dags bind mount exists on scheduler, worker, and webserver. Check AIRFLOW_UID in .env matches host ownership with ls -la dags/. Fix with echo AIRFLOW_UID=$(id -u) >> .env and recreate.
Symptom · 04
Half the stack stays down after host reboot
Fix
Run docker compose ps --format table and docker compose logs scheduler --tail 100. If scheduler started before postgres was healthy, add a postgres healthcheck and depends_on condition service_healthy, then docker compose up -d.
★ Airflow Docker Compose Rescue Cheat SheetRecover a wiped or wedged compose stack without losing another weekend of history.
Empty UI with zero DAGs after down -v
Immediate action
Confirm which volumes survived the cleanup
Commands
docker compose ps && docker volume ls | grep airflow
docker compose logs postgres --tail 50
Fix now
Restore postgres-db-volume from backup, pin named volumes in compose, ban bare down -v.
Webserver 500s after image upgrade+
Immediate action
Verify DB schema before starting services
Commands
docker compose run airflow-cli airflow db check
docker compose logs scheduler --tail 100
Fix now
Run airflow db migrate, re-check, then start webserver and scheduler.
Edits invisible in UI+
Immediate action
Confirm DAG mount exists on every service
Commands
docker compose config | grep -B 2 -A 5 'dags:'
ls -la dags/ && airflow dags list-import-errors
Fix now
Bind-mount ./dags on all services and set AIRFLOW_UID=$(id -u) in .env.
Scheduler crash-loops on boot+
Immediate action
Start postgres first, verify healthy
Commands
docker compose up -d postgres && docker compose ps
docker compose logs scheduler --tail 50
Fix now
Add postgres healthcheck plus depends_on, then bring up the full stack.
DAG Sync Methods on Compose Compared
Sync methodSpeedRiskBest for
Bind mount ./dagsInstant (<30s)Low: local edits visible fastDev loops on one host
Named volume + sidecar syncMinutesMedium: sync lag confusionSmall teams sharing one host
git-sync sidecarMinutes, versionedMedium: broken commit syncs fastStaging mirroring prod branches
Baked into imageDeploy-time onlyLowest drift, slower iterationProd parity and rollbacks
Custom image (build: . + requirements)Build-time, pinnedLow drift, rebuild latencyTeams with extra deps
_PIP_ADDITIONAL_REQUIREMENTSContainer start, unpinnedFast iteration, slow bootsTiny extras during dev
⚙ Quick Reference
2 commands from this guide
FileCommand / CodePurpose
composeairflow-compose.ymlservices:Named Volumes vs Anonymous Disaster
opscompose_upgrade.shdocker compose pullRunning Migrations Safely

Key takeaways

1
Named volumes persist the DB and DAGs; down -v deletes them permanently.
2
Bind-mount ./dags for instant dev loops with AIRFLOW_UID pinned; boot first with docker compose up airflow-init (airflow/airflow).
3
Run DB migrations after every image change and verify with db check.
4
Restart policies plus DB healthchecks survive host reboots.
5
Scale workers with --scale for small teams; use images for prod parity.

Common mistakes to avoid

4 patterns
×

Using anonymous volumes and docker compose down -v casually

Symptom
One cleanup command deletes the metadata DB and DAG history; the stack comes up empty with zero runs.
Fix
Declare named volumes (postgres-db-volume, scheduler-dags-volume) and bind-mount DAGs from ./dags. Back up the DB volume before any down -v, and run down without -v for routine restarts.
×

Skipping DB migrations after changing the Airflow image tag

Symptom
Webserver 500s and scheduler crash-loops on schema mismatch right after an upgrade.
Fix
Run airflow db migrate (or the init service) after every image bump and confirm with airflow db check before starting webserver and scheduler. Pin the image tag so upgrades are deliberate.
×

Editing DAGs in the container instead of the mounted folder

Symptom
Changes vanish on recreate and teammates can't reproduce the running DAGs.
Fix
Mount ./dags, ./logs, ./plugins explicitly and keep .env with AIRFLOW_UID in version control. Document bind vs git-sync so local edits appear in under 30 seconds.
×

No restart policy or startup ordering on compose services

Symptom
Host reboot leaves half the stack down; scheduler starts before postgres and crash-loops.
Fix
Set restart: unless-stopped on scheduler, worker, and broker services, plus a healthcheck on postgres. Stagger depends_on so the DB is healthy before the scheduler starts.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does docker compose down -v do to an Airflow stack?
Q02SENIOR
Walk me through a production-grade compose setup for Airflow.
Q03SENIOR
Compare bind mounts, git-sync, and baked images for DAG delivery.
Q01 of 03JUNIOR

What does docker compose down -v do to an Airflow stack?

ANSWER
down -v deletes named and anonymous volumes, including postgres-db-volume holding the metadata DB. The stack restarts with an empty DB and no DAG history. Routine restarts should use docker compose down without -v, and DB volumes need backups.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why did docker compose down -v delete everything?
02
How do local DAG edits reach the containers?
03
When must I run DB migrations on compose?
04
Can I scale workers with compose?
05
What belongs in version control?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

3 min read · try the examples if you haven't

Previous
Airflow Executors Explained
22 / 37 · Airflow
Next
Airflow Celery Executor Setup