Skip to content

feat: add astro dev local for Docker-free local development#2012

Draft
jlaneve wants to merge 13 commits intomainfrom
feat/standalone-mode
Draft

feat: add astro dev local for Docker-free local development#2012
jlaneve wants to merge 13 commits intomainfrom
feat/standalone-mode

Conversation

@jlaneve
Copy link
Contributor

@jlaneve jlaneve commented Feb 13, 2026

Problem

Local Airflow development with astro dev start requires Docker Desktop and spins up 6 containers (Postgres, db-migration, api-server, scheduler, triggerer, dag-processor). This takes ~70 seconds for a cold start and consumes significant memory/CPU. For iterating on DAGs during development, this is slow and heavyweight.

Solution

Add a new astro dev local command that runs Airflow natively on the host without Docker, using:

  • airflow standalone — Airflow's built-in single-process mode (SQLite, all components in one process)
  • uv — fast Python package manager for venv creation and dependency installation
  • Existing project structure — reads the same Dockerfile and requirements.txt, zero migration needed

Startup times

Mode Cold start Warm start
astro dev start (Docker) ~70s ~15s
astro dev local 11s 1s

Commands

# Start Airflow locally (backgrounds, returns after health check)
astro dev local

# Start in foreground for debugging (stream output, Ctrl+C to stop)
astro dev local --foreground

# View logs
astro dev local logs
astro dev local logs -f   # follow

# Stop
astro dev local stop

# Reset environment (stop + remove .venv, .astro/standalone/, and all cached state)
astro dev local reset

How it works

  1. Parses the project Dockerfile to extract the runtime image tag
  2. Validates Airflow version (Airflow 3 / runtime 3.x only)
  3. Checks that uv is installed
  4. Fetches two files from cdn.astronomer.io (cached in .astro/standalone/):
    • Freeze file (cdn.astronomer.io/runtime-freeze) — full ~191-package list used as pip -c constraints for reproducible installs
    • Constraints file (cdn.astronomer.io/runtime-constraints) — 3-line file for extracting airflow and task-sdk version pins
  5. Creates a Python venv and installs dependencies using a 2-step install:
    • Step 1: Install apache-airflow with the full freeze file as constraints (reproduces the exact runtime environment)
    • Step 2: Install requirements.txt with only apache-airflow and apache-airflow-task-sdk version locks (gives the resolver freedom to satisfy user dependencies without the full freeze blocking)
  6. Seeds a simple_auth_manager_passwords.json.generated file with admin:admin credentials (no-op if file already exists, preserving any custom credentials)
  7. Applies airflow_settings.yaml (connections, variables, pools)
  8. Background mode (default): Starts airflow standalone, redirects output to airflow.log, writes a PID file, runs health check, prints status with credentials, and returns
  9. Foreground mode (--foreground): Streams output to terminal, blocks on process, Ctrl+C sends SIGTERM to process group

Flags

Flag Description
--env / -e Location of .env file (default: .env)
--settings-file / -s Airflow settings file (default: airflow_settings.yaml)
--wait Health check timeout (default: 1m)
--foreground / -f Run in foreground instead of backgrounding
--workspace-id / -w Workspace ID for environment connections
--deployment-id / -d Deployment ID for environment connections

State files

All generated files are kept inside .astro/standalone/ — the project root stays clean (no stray airflow.cfg, airflow.db, logs/, or passwords files):

project-root/
  .astro/standalone/          # AIRFLOW_HOME — all Airflow state lives here
    airflow.cfg               # Airflow configuration (auto-generated)
    airflow.db                # SQLite database
    logs/                     # Task and scheduler logs
    airflow.pid               # PID of the backgrounded process group leader
    airflow.log               # stdout/stderr from the backgrounded process
    constraints-<tag>.txt     # cached constraints file (version pins)
    freeze-<tag>.txt          # cached freeze file (full package list)
    simple_auth_manager_passwords.json.generated  # admin:admin credentials
  .venv/                      # Python venv (outside AIRFLOW_HOME, preserved on reset)
  dags/                       # Your DAG files (unchanged)

Authentication

Standalone mode uses Airflow 3's SimpleAuthManager. On first run, admin:admin credentials are seeded automatically. Credentials are displayed in the startup output:

✔ Airflow is ready! (PID 12345)
➤ Airflow UI: http://localhost:8080
➤ Username:   admin
➤ Password:   admin
➤ View logs: astro dev local logs -f
➤ Stop:      astro dev local stop

The passwords file is preserved across restarts — credentials can be changed by editing .astro/standalone/simple_auth_manager_passwords.json.generated.

Prerequisites

  • uv must be installed (installation guide)
  • Python 3.12 (the version targeted by Astro CLI standalone)
  • Airflow 3 / runtime 3.x (Airflow 2 is not supported)

Implementation

New files

  • airflow/standalone.goStandalone struct implementing ContainerHandler interface
  • airflow/standalone_test.go — unit tests (background, foreground, stop, logs, PS, edge cases)

Modified files

  • cmd/airflow.golocal command with stop, logs, reset subcommands + --foreground flag
  • cmd/airflow_hooks.goEnsureLocalRuntime pre-run hook (skips Docker init)
  • cmd/airflow_test.go — command-layer tests for all subcommands
  • airflow/container.goStandaloneHandlerInit factory function
  • settings/settings.goSetExecAirflowCommand to allow standalone settings injection

Test plan

  • Unit tests pass (go test ./airflow/ ./cmd/ ./settings/)
  • Race detector clean (go test -race ./airflow/)
  • E2E: astro dev local backgrounds, PID file written, health check passes, CLI returns
  • E2E: astro dev local while running → "already running" error (check happens before install)
  • E2E: astro dev local logs prints log file content
  • E2E: astro dev local stop sends SIGTERM, process exits, PID file removed
  • E2E: astro dev local stop when not running → graceful message
  • E2E: astro dev local --foreground streams output, exits on termination
  • E2E: astro dev local reset stops running process then cleans up all files
  • E2E: Warm start — run again with cached venv → healthy in ~1s
  • E2E: requirements.txt with extra providers (e.g. apache-airflow-providers-http) installs correctly
  • E2E: Stale PID file recovery — if process died without cleanup, next start detects stale PID and proceeds
  • E2E: Airflow 2 runtime tag → rejected with clear error
  • E2E: Real DAGs written, triggered via REST API, task outputs verified via logs
  • E2E: admin:admin credentials work for Airflow UI and REST API
  • E2E: All generated files land in .astro/standalone/, project root stays clean
  • Test on Linux

Known limitations / future work

  • Standalone mode uses SQLite — suitable for development but not production-representative for database-sensitive testing.
  • Airflow 2 is not supported — standalone mode requires runtime 3.x.

🤖 Generated with Claude Code

@coveralls-official
Copy link

coveralls-official bot commented Feb 13, 2026

Pull Request Test Coverage Report for Build e9f025c7-7a3b-4d40-bb7a-fb9f2bd0045e

Details

  • 504 of 665 (75.79%) changed or added relevant lines in 5 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.4%) to 35.524%

Changes Missing Coverage Covered Lines Changed/Added Lines %
cmd/airflow_hooks.go 0 3 0.0%
settings/settings.go 0 5 0.0%
cmd/airflow.go 88 97 90.72%
airflow/local.go 413 557 74.15%
Totals Coverage Status
Change from base Build 78a63b8e-67c6-4c1c-a99c-85c5b61c40bc: 0.4%
Covered Lines: 23493
Relevant Lines: 66133

💛 - Coveralls

…pment

Add a new `astro dev standalone` command that runs Airflow locally
without Docker, using `airflow standalone` and `uv` for dependency
management. This provides a dramatically faster dev loop for Airflow 3
projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jlaneve jlaneve force-pushed the feat/standalone-mode branch from d95fbe6 to 1e42a1e Compare February 13, 2026 19:22
pre-commit-ci bot and others added 4 commits February 13, 2026 19:22
Standalone mode now works with both Airflow 2 (runtime 4.0.0+) and
Airflow 3 (runtime 3.x). The health check endpoint, image registry,
and settings version are determined dynamically based on the detected
Airflow major version.

Changes:
- Accept airflowMajor "2" or "3" (was "3" only)
- Health check: /health + webserver (AF2) vs /api/v2/monitor/health + api-server (AF3)
- Image registry: quay.io/astronomer/astro-runtime (AF2) vs astrocrpublic.azurecr.io/runtime (AF3)
- Settings version passed dynamically (was hardcoded 3)
- Kill/reset cleans up both AF2 and AF3 credential files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Default `astro dev standalone` to background mode — the CLI starts the
airflow process, writes a PID file, waits for the health check, prints
status, and returns.  A `--foreground` flag preserves the previous
stream-to-terminal behaviour.

New subcommands:
  - `astro dev standalone stop`  — SIGTERM the process group, clean up PID file
  - `astro dev standalone logs [-f]` — dump or tail the log file

Also wires `reset` to stop a running process before cleaning up files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jlaneve jlaneve force-pushed the feat/standalone-mode branch from 9683aac to 2593b32 Compare February 13, 2026 21:25
@tayloramurphy
Copy link
Contributor

@jlaneve I know this mirrors the airflow standalone command but would an shorter alias be useful too? standalone is just a lot to type. lite or native could be alternates.

standaloneLogFile = "airflow.log"
defaultStandalonePort = "8080"
standaloneIndexURL = "https://pip.astronomer.io/v2/"
standalonePythonVer = "3.12"
Copy link
Member

Choose a reason for hiding this comment

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

You'd want to capture the python version from the image they have in their dockerfile as well.

requirementsPath := filepath.Join(s.airflowHome, "requirements.txt")
installArgs := []string{
"pip", "install",
fmt.Sprintf("apache-airflow==%s", airflowVersion),
Copy link
Member

Choose a reason for hiding this comment

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

This alone isn't enough - you'd need to capture the full set of "stuff" we install by default. And that varies a bit.

Airflow 2 has the concept of a slim and "full" image. The latter has a number of extra providers installed by default. Both have a set of providers. From memory, it also varies a bit based on the Runtime version as well, so you likely can't have a static list across the board here.

Airflow 3 is only slim, but there is a bit of risk here that changes in the default set would cause environment drift. Ideally we aren't hard coding those there to get stale.

…install

- Remove airflowMajor field and AF2 code paths (standalone is AF3-only)
- Replace Docker-based constraint extraction with HTTP fetch from
  pip.astronomer.io/runtime-constraints
- Implement 2-step install: first install airflow with full constraints,
  then install user requirements with only airflow/task-sdk version locks
- Add parsePackageVersionFromConstraints helper for task-sdk version
- Remove runtimeImageName, execDockerRun, constraintsFileInImage
- Simplify healthEndpoint to always return AF3 endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jlaneve jlaneve force-pushed the feat/standalone-mode branch from b1a21b5 to e91a7ad Compare February 17, 2026 19:06
jlaneve and others added 3 commits February 18, 2026 21:03
…an project root

- Fix CDN base URLs: constraintsBaseURL → cdn.astronomer.io/runtime-constraints
- Add freezeBaseURL (cdn.astronomer.io/runtime-freeze) for full 191-package list
- getConstraints() now fetches and caches both constraints + freeze files;
  returns freeze path for use as pip -c arg in step 1 install
- Move "already running" check to top of Start() before any install work
- Add ensureCredentials() to seed passwords file with admin:admin on first run
- Add readCredentials() to display username/password in startup output
- Redirect AIRFLOW_HOME → .astro/standalone/ so airflow.cfg, airflow.db,
  and logs/ all live there instead of cluttering the project root
- Set AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_PASSWORDS_FILE to .astro/standalone/
- Update Kill() to clean up .venv/ and .astro/standalone/ only
- Update tests: freeze file routing in fetch mock, AIRFLOW_HOME assertions,
  new TestStandaloneEnsureCredentials, TestStandaloneReadCredentials tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Standalone.Build() stub (errStandaloneNotSupported) to satisfy
  the updated ContainerHandler interface which gained a Build method in main
- Resolve conflict in cmd/airflow_test.go: keep both TestAirflowStandalone
  and new TestAirflowBuild from main, preserving all subtests from both

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename the command from 'standalone' to 'local' for a more intuitive UX.
The internal Go types (Standalone, StandaloneHandlerInit) are unchanged.

- cmd/airflow.go: Use:"standalone" → Use:"local", all function/var names
- cmd/airflow_hooks.go: EnsureStandaloneRuntime → EnsureLocalRuntime
- cmd/airflow_test.go: update all test names and assertions
- airflow/standalone.go: add Build() stub (satisfies updated ContainerHandler)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jlaneve jlaneve changed the title feat: add astro dev standalone for Docker-free local development feat: add astro dev local for Docker-free local development Feb 19, 2026
jlaneve and others added 4 commits February 18, 2026 21:29
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parse the optional -python-X.Y suffix from runtime image tags
(e.g. 3.1-12-python-3.11) instead of hardcoding Python 3.12.
Falls back to 3.12 (the default for all Runtime 3.x images)
when no suffix is present.

Cache filenames now include the Python version to avoid stale
lookups when switching between Python versions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments