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

Airflow Docker Compose: The Setup That Wiped Our DAGs

Airflow Docker Compose is the standard dev setup - until down -v deletes your DAGs and database.

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
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Docker and Docker Compose installed locally.
  • Basic DAG authoring skills (see the DAGs article).
  • A working docker-compose.yaml from the Airflow docs or repo.
  • Comfort with shell commands and YAML.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • The official docker-compose.yaml runs Airflow as a stack: postgres, redis, webserver, scheduler, worker, and a one-shot airflow-init service.
  • Named volumes (postgres-db-volume, scheduler-dags-volume) survive docker compose down -v; anonymous volumes do not.
  • docker compose down -v deletes anonymous volumes: one team wiped their metadata DB and every DAG with that single command.
  • The init service runs airflow db migrate before webserver and scheduler start; skipping it gives webserver 500s.
  • Bind-mount ./dags for instant DAG sync in dev; use git-sync when the DAGs live in a repo.
  • Restart: unless-stopped keeps the stack alive across host reboots; --scale worker=3 scales out Celery workers.
✦ Definition~90s read
What is Airflow Docker Compose Setup?

Airflow Docker Compose is the official containerized deployment pattern: one image, six services, named volumes for state, and an init service that migrates the metadata DB before the fleet starts.

Docker Compose is like a moving crew that unpacks your whole apartment from one manifest.
Plain-English First

Docker Compose is like a moving crew that unpacks your whole apartment from one manifest. The official Airflow version keeps state in storage boxes: one for the database, one for the DAGs. The destroy command has a flag, down -v, that also throws away the storage boxes. The team in this story ran that flag and lost both the database and all their DAGs at once.

The official docker-compose.yaml is the fastest way to run Airflow anywhere. One file, eight services, one command, and you're up. It's also the fastest way to lose everything if you don't understand its volumes.

The stack is designed around state: the Postgres metadata DB and the DAGs folder are the two things you can never afford to lose. Both live in volumes, and the difference between named and anonymous volumes decides whether they survive cleanup.

Most teams learn this the hard way. A hung scheduler, a frustrated engineer, and the muscle-memory command docker compose down -v. Minutes later the stack is back up: empty, clean, and newly deployed DAGs.

Treat the compose file as infrastructure you own, not a script you run. Your DAGs depend on it.

1. The Official Compose File, Service by Service

The official docker-compose.yaml runs Airflow as one stack of services. postgres holds the metadata DB, redis is the Celery broker, webserver serves the UI, scheduler parses DAGs, worker executes tasks, and airflow-init runs the database migrations once on startup.

Each service uses the same Airflow image with a different entrypoint command. That's the whole trick: one image, six roles, distinguished by command and dependency order.

airflow-init is the quiet hero. It runs airflow db migrate before anything else, creating the metadata schema. Skip it and webserver greets you with 500s and scheduler dies on missing tables.

Current Airflow 3 compose files split the roles further: airflow-dag-processor parses DAG files, airflow-api-server serves the UI (the old webserver role), and airflow-triggerer runs the event loop for deferrable operators. The images are pinned deliberately — postgres:16 and redis:7.2-bookworm (Redis changed its license after 7.2). Flower, the Celery monitor on port 5555, rides along behind a profile: docker compose --profile flower up.

Two environment notes before first boot. On Linux, export AIRFLOW_UID=$(id -u) — the default 50000 makes the init container chown your dags and plugins folders to a user you don't own. And give Docker at least 4 GB of RAM for the Airflow containers; the init service warns when it detects less.

Understand the roles before you touch a single flag. Every debugging session starts here.

📊 Production Insight
One image, six roles: postgres, redis, webserver, scheduler, worker, init.
Init runs migrations; skipping it breaks every other service.
Rule: read the service list before you modify the stack.
The official file is a dev quick-start — the docs state it provides no production security guarantees.
🎯 Key Takeaway
The compose stack is one image running different entrypoint commands.
airflow-init creates the metadata schema for everything else.
Punchline: init comes first, always.
The official compose stack, service by service The Official Compose Stack, Service by Service One image, six roles — init first Bootstrap airflow-init airflow db migrate, once postgres — metadata DB schema must exist first Fleet — after migrations webserver serves the UI scheduler parses DAGs worker executes tasks redis — Celery broker hands tasks to workers Skip init → webserver 500s scheduler dies on missing tables THECODEFORGE.IO
thecodeforge.io
Airflow Docker Compose

2. Named Volumes vs Anonymous: Where State Lives

Volumes are where container state survives. Anonymous volumes get a random name at creation; named volumes get the name you give them. That one difference decides what survives docker compose down -v.

The metadata DB must survive restarts and cleanups. Declare postgres-db-volume and mount it at the Postgres data directory. Losing it means losing run history, variables, connections, and every audit trail.

The same goes for the DAGs folder when you mount it from a volume: schedule it as scheduler-dags-volume, or you'll wonder where your DAGs went after the first cleanup.

Name your volumes. Anonymous volumes are a delete waiting to happen.

docker-compose.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: airflow
      POSTGRES_PASSWORD: airflow
      POSTGRES_DB: airflow
    volumes:
      - postgres-db-volume:/var/lib/postgresql/data

volumes:
  postgres-db-volume:
    name: airflow-postgres-db
  scheduler-dags-volume:
    name: airflow-dags
Output
Named volumes survive docker compose down -v
⚠ Anonymous Volumes Are Debt
docker compose down -v removes every anonymous volume. If the Postgres data directory or your DAGs folder mounts anonymously, the command deletes them. Name everything you care about.
📊 Production Insight
Anonymous volumes die with down -v; named volumes survive.
DB, DAGs, and logs are the state that matters.
Rule: every volume you care about gets a name.
🎯 Key Takeaway
down -v deletes anonymous volumes, not named ones.
The metadata DB is the most expensive thing in the stack.
Punchline: named volumes are the difference between reset and disaster.
Named vs anonymous volumes under down -v Named vs Anonymous Volumes: down -v One flag decides what survives cleanup Named volume Anonymous volume Volume name postgres-db-volume random hex name Metadata DB Survives down -v Deleted by down -v DAG folder Survives Deleted with it Cleanup result State intact Brand-new empty stack Name every volume you care about THECODEFORGE.IO
thecodeforge.io
Airflow Docker Compose

3. DAGs Sync: Bind Mount vs git-sync

Your DAGs need to reach the scheduler and workers. The bind mount is the dev workhorse: ./dags on your host maps into /opt/airflow/dags in the containers. Edit on the host, see it in the UI within a parse cycle.

Mount it read-only in production. A writable mount lets the container mutate your source, and you'll chase phantom diffs.

git-sync is the production pattern: a sidecar container clones a repo branch into the DAGs folder on an interval. Merge to main, and DAGs land without touching the host.

The rule: bind mount for fast iteration, git-sync for anything that needs a review trail.

docker-compose.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
services:
  scheduler:
    image: apache/airflow:3.3.0
    volumes:
      - ./dags:/opt/airflow/dags:ro
      - ./logs:/opt/airflow/logs
      - ./config:/opt/airflow/config
    environment:
      AIRFLOW__CORE__DAGS_FOLDER: /opt/airflow/dags

  worker:
    image: apache/airflow:3.3.0
    volumes:
      - ./dags:/opt/airflow/dags:ro
Output
Host ./dags syncs read-only into scheduler and worker
📊 Production Insight
Bind mount: instant dev iteration, host files are the source.
git-sync: reviewed deploys straight from the repo.
Rule: read-only mounts in production, always.
🎯 Key Takeaway
DAGs reach containers via bind mount or git-sync.
Bind mounts win for dev; git-sync wins for reviewable deploys.
Punchline: never ship DAGs that live only inside a container.
Getting DAGs to the scheduler: two paths DAGs to the Scheduler: Two Paths Bind mount for dev, git-sync for review Bind mount git-sync Sync source host ./dags folder repo branch sidecar Cadence instant — parse cycle interval clone Production read-only :ro mount no host dependency Best for fast dev iteration reviewed deploys DAGs belong outside the container layer THECODEFORGE.IO
thecodeforge.io
Airflow Docker Compose

4. Running Migrations Safely

The metadata schema must match the Airflow version. The init service runs airflow db migrate on startup, applying migrations in order. That's the safe path.

The unsafe path is running migrations ad hoc while webserver and scheduler are already serving. Two containers migrating the same schema race, and you get lock errors and half-applied versions.

Before you change anything destructive, take a dump. pg_dump the metadata DB to a file, label it with the date, and store it somewhere outside the compose stack. Restore is one command.

Migrations are boring until they're not. Back up first, migrate once, verify after.

backup_restore.shBASH
1
2
3
4
5
6
7
8
9
10
# Back up the metadata DB before touching anything destructive
docker compose exec postgres pg_dump -U airflow -d airflow \
  > backups/airflow_$(date +%F).dump

# Verify the dump is readable
ls -la backups/airflow_$(date +%F).dump

# Restore when the unthinkable happens
cat backups/airflow_2026-08-01.dump | \
  docker compose exec -T postgres psql -U airflow -d airflow
Output
backups/airflow_2026-08-01.dump
📊 Production Insight
Init service migrates once, before the fleet starts.
Two containers migrating at once corrupt the schema.
Rule: dump the DB before every destructive operation.
🎯 Key Takeaway
Migrations run via the init service, not ad hoc.
A dated pg_dump makes every migration reversible.
Punchline: backup first is not a suggestion, it's the rule.

5. Restart Policies and Crash Loops

Compose gives every service a restart policy. unless-stopped is the production default: the container comes back after crashes and host reboots, but respects an explicit stop. Set it as a service key, as in restart: unless-stopped on scheduler, webserver, and worker.

Set it and a transient OOM or a host reboot stops taking your platform down with it. Skip it and every reboot becomes a manual docker compose up.

A restart loop tells you something real: config errors, missing volumes, or a bad image tag. Watch the logs, not the uptime counter. If the scheduler restarts every 5 seconds, it's not the policy's fault.

The policy is a safety net, not a fix. Diagnose the loop, then let the policy keep the healthy stack alive.

📊 Production Insight
unless-stopped survives crashes and reboots.
A crash loop is a diagnosis, not a policy problem.
Rule: read logs before you blame the restart policy.
🎯 Key Takeaway
Restart policies keep the stack alive across failures.
Restart loops point at config, volumes, or images.
Punchline: the policy resurrects; the logs explain.

6. Scaling Services with --scale

Need more workers? Compose scales a service with one flag: docker compose up -d --scale worker=3. Each replica runs the same image and config, pulling from the same metadata DB and broker.

Celery workers are the safe thing to scale in the compose stack. They're stateless: the queue hands them work, they report results to the DB. More workers, more concurrency, no state to sync.

What you should never scale: postgres, webserver, or scheduler behind a single Compose deployment. Multiple schedulers need HA configuration, and multiple webservers need a shared config story. That's a production upgrade, not a flag.

Scale the stateless workers. Leave the stateful services alone.

scale_workers.shBASH
1
2
3
4
5
6
7
8
# Scale the Celery worker fleet to 3 replicas
docker compose up -d --scale worker=3

# Confirm replicas are running
docker compose ps

# Check worker heartbeats
docker compose logs worker | tail -20
Output
NAME SERVICE REPLICAS STATUS
airflow-worker-1 worker 1/1 Running (healthy)
airflow-worker-2 worker 1/1 Running (healthy)
airflow-worker-3 worker 1/1 Running (healthy)
📊 Production Insight
Workers are stateless: safe to scale with --scale.
Never scale postgres or scheduler with a plain flag.
Rule: scale workers, verify replicas, watch heartbeats.
🎯 Key Takeaway
--scale worker=3 multiplies executor capacity fast.
Stateful services need real HA, not replicas.
Punchline: stateless scales with a flag; state needs architecture.
● Production incidentPOST-MORTEMseverity: high

The Night down -v Ate the DAGs

Symptom
The dev environment felt slow, so someone ran docker compose down -v to reset everything. The next docker compose up came back with zero DAGs in the UI, missing scheduler tables, and login errors on every page.
Assumption
The team assumed -v only removed unused containers and networks, a harmless weekend reset. Nobody read the flag's name: "remove anonymous volumes."
Root cause
The official compose file stores the Postgres metadata DB and the DAG folder in anonymous volumes by default. docker compose down -v deletes every anonymous volume, so the database and all DAGs vanished with the cleanup command. The stack came up brand new and empty.
Fix
Declared named volumes (postgres-db-volume and scheduler-dags-volume) explicitly in the compose file, bind-mounted ./dags instead of a volume, started taking pg_dump backups of the metadata DB, and made plain docker compose down the team's standard cleanup.
Key lesson
  • docker compose down -v deletes anonymous volumes, including your database and DAGs.
  • Named volumes survive down -v; anonymous volumes do not.
  • DAGs belong in a bind mount or git-sync, never in a container layer.
  • Back up the metadata DB with pg_dump before any destructive command.
Production debug guideSymptom to Action5 entries
Symptom · 01
No DAGs appear in the UI after a restart
Fix
Check what the scheduler mounts with docker compose exec scheduler ls /opt/airflow/dags. If the folder is empty, the volume or bind mount is wrong.
Symptom · 02
Webserver returns 500 on first boot
Fix
The metadata DB was never initialised. Run the init service (airflow db migrate) before webserver and scheduler start.
Symptom · 03
Scheduler crashes in a restart loop
Fix
Read docker compose logs scheduler and look for config or DB errors. A bad airflow.cfg typo is the usual culprit.
Symptom · 04
Metadata disappeared after cleanup
Fix
Confirm whether a volume was deleted with docker volume ls. Restore from pg_dump, then switch to named volumes.
Symptom · 05
Workers missing after --scale
Fix
Verify replicas with docker compose ps. The worker service must be scaled explicitly, and new replicas need the same config and volumes.
★ Quick Debug Cheat SheetCommon docker-compose issues and immediate actions
No DAGs in the UI after restart
Immediate action
Check what the scheduler mounts
Commands
docker compose ps
docker compose exec scheduler ls /opt/airflow/dags
Fix now
Bind-mount ./dags or restore the DAG volume, then restart the scheduler.
Webserver 500s on first boot+
Immediate action
Check the DB migration state
Commands
docker compose logs webserver | tail -50
docker compose exec airflow-init airflow db check
Fix now
Run the init service (airflow db migrate) before webserver starts.
Scheduler crash-loops+
Immediate action
Read the scheduler logs
Commands
docker compose logs scheduler | tail -50
docker compose exec scheduler airflow config get-value core executor
Fix now
Fix the config error, then docker compose up -d --no-deps scheduler.
Metadata DB lost after down -v+
Immediate action
Check for a dump or restore point
Commands
ls -la backups/
docker compose exec postgres pg_restore -U airflow -d airflow backups/airflow.dump
Fix now
Re-init with airflow db migrate and restore from the latest dump.
Worker replicas not scaling+
Immediate action
Verify the worker service
Commands
docker compose ps
docker compose up -d --scale worker=3
Fix now
Scale the worker service explicitly and check its logs for heartbeats.
Ways to Get DAGs into the Scheduler
MethodHow it worksPick when
Bind mount ./dagsHost folder mounted into /opt/airflow/dagsDev loops where edit-save-see matters
Named volume for DAGsVolume survives down -v, DAGs copied in by init or imageYou need DAGs to survive container cleanup
git-sync sidecarSidecar clones a repo branch on an intervalDAGs live in git and deploys need a review trail
DAGs baked into the imagepip install or COPY into the Airflow imageHermetic deploys where image = version
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
docker-compose.yamlservices:2. Named Volumes vs Anonymous
backup_restore.shdocker compose exec postgres pg_dump -U airflow -d airflow \4. Running Migrations Safely
scale_workers.shdocker compose up -d --scale worker=36. Scaling Services with --scale

Key takeaways

1
The official compose stack is one Airflow image running six roles, with airflow-init doing migrations first.
2
Named volumes survive docker compose down -v; anonymous volumes are deleted with it.
3
DAGs belong in a bind mount or git-sync, never inside a container layer.
4
pg_dump the metadata DB before any destructive operation.
5
Scale stateless Celery workers with --scale; give stateful services real HA instead.

Common mistakes to avoid

4 patterns
×

Running docker compose down -v out of habit for every reset.

Symptom
Metadata DB and DAGs vanish; the stack comes up empty and re-initialized.
Fix
Use plain docker compose down for normal cleanup. Delete volumes by name only when you truly mean it.
×

Letting the Postgres data live in an anonymous volume.

Symptom
A restart or down -v silently loses the entire metadata DB.
Fix
Declare postgres-db-volume explicitly and mount it at /var/lib/postgresql/data.
×

Starting webserver and scheduler before the init service runs migrations.

Symptom
Webserver 500s and scheduler crashes with missing-table errors.
Fix
Keep depends_on and the init service in place, or run airflow db migrate before the fleet starts.
×

Editing DAGs inside a running container.

Symptom
Changes vanish on recreate and the container drifts from git.
Fix
Bind-mount ./dags read-only and edit on the host; the container is a stateless runner.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does docker compose up -d do compared to docker compose down?
Q02SENIOR
Why does the official Airflow compose stack need an init service?
Q03SENIOR
Your scheduler keeps restarting in a loop after a config change. How do ...
Q01 of 03JUNIOR

What does docker compose up -d do compared to docker compose down?

ANSWER
up -d creates and starts the stack's containers in the background according to the compose file. down stops and removes containers and networks. Without -v, down keeps volumes, so state survives. With -v, anonymous volumes are deleted too.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What does the -v flag do in docker compose down?
02
Why do I see no DAGs after restarting the stack?
03
How do I back up the Airflow metadata DB?
04
Bind mount or named volume for DAGs?
05
How do I add more workers to the compose stack?
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
August 01, 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
22 / 37 · Airflow
Next
Airflow Celery Setup