Skip to content

feat(dev): run agentex locally without Docker - #353

Merged
aringuyen3 merged 9 commits into
mainfrom
aringuyen/run-local-no-docker
Aug 10, 2026
Merged

feat(dev): run agentex locally without Docker #353
aringuyen3 merged 9 commits into
mainfrom
aringuyen/run-local-no-docker

Conversation

@aringuyen3

@aringuyen3 aringuyen3 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What

Adds a docker-free mode that runs the full agentex backend as host processes with embedded datastores — a lighter alternative to the Docker Compose stack, closer to a one-command langgraph dev-style workflow.

./dev.sh no-docker                    # whole stack, no Docker
./dev.sh no-docker --lean             # Postgres + Redis + API + MongoDB only (no Temporal/OTel)
./dev.sh no-docker --no-temporal      # skip Temporal + the worker
./dev.sh no-docker --mongo-uri <uri>  # use an external MongoDB instead of a local mongod

The bare ./dev.sh (Docker) is unchanged and now also accepts an explicit ./dev.sh docker alias. The docker-free mode is also available as make dev-no-docker and python -m scripts.dev_nodocker.

Why

Standing up a local environment previously required the full Docker stack. This lets a developer run the backend with a single command and no Docker daemon:

  • Postgres via bundled pgserver (unix socket) and Redis via bundled redislite
  • a Temporal dev server + Web UI and the agentex worker
  • a local mongod — always started; the stack requires it (the Temporal worker builds Mongo-backed repositories at boot)
  • an optional OpenTelemetry collector

It runs migrations, supervises uvicorn + the worker, and tears everything down cleanly on Ctrl-C / SIGTERM. --ephemeral uses a throwaway data dir; --mongo-uri points at an existing MongoDB instead of launching a local mongod.

App-side change

Safe no-op wherever the backend runs in Docker / staging / prod (the env var it keys on is unset there):

ACP host rewrite for docker-free mode. Agents register their ACP URL at host.docker.internal (the SDK default, so a Docker backend can reach an agent on the host), which a host-process backend can't resolve. The runner sets AGENTEX_ACP_HOST_OVERRIDE=127.0.0.1 and the backend rewrites only that sentinel host to the override when dialing agents — in the ACP request path, the agent-API-key proxy path, and the Temporal healthcheck. When the env var is unset, the stored URL is used verbatim. Default-scaffolded agents work without manifest edits.

Also

  • Fix a frontend dev-server process leak in dev.sh (kill the whole make → npm → next tree and sweep orphans; report status by listening port).
  • Correct the MongoDB (brew trust first) and OpenTelemetry (release binary; not in Homebrew) install commands.
  • Add a dev-no-docker uv dependency group (pgserver, redislite, greenlet) pulled only for docker-free mode.
  • Document docker-free mode in README.md and CLAUDE.md.

Contract note

The runner is a package (scripts/dev_nodocker/), so the direct invocation is python -m scripts.dev_nodocker (not python scripts/dev_nodocker.py). ./dev.sh no-docker and make dev-no-docker are unchanged for callers.

Testing

Manually exercised on macOS (Docker stopped):

  • Full stack and --lean — the API passes /healthz within ~1s and /readyz reports Postgres, Redis, and MongoDB all healthy; the Temporal worker stays up; teardown frees all ports with no orphaned processes.
  • Fail-fast path: full mode with mongod absent aborts with an actionable install message (or point at an external instance with --mongo-uri).
  • Booted end-to-end via python -m scripts.dev_nodocker; agents scaffolded by agentex init connect without manifest edits (ACP host rewrite).

Platform support: macOS and Linux only. Not supported on native Windows — the embedded Redis (redislite) ships no Windows server build; run it under WSL2, where redislite/pgserver/mongod behave as on Linux (WSL2 path not yet verified).

Greptile Summary

This PR introduces a docker-free local development mode (./dev.sh no-docker) that provisions embedded datastores (bundled Postgres via pgserver, Redis via redislite, auto-downloaded Temporal dev server, and a local mongod) and supervises the API server and worker as host processes — no Docker daemon required. It also adds an ACP host-rewrite utility (src/utils/acp_url.py) so that agents registered at host.docker.internal are dialed correctly when the backend itself runs on the host.

  • New scripts/dev_nodocker package: pure config module, service provisioners, subprocess supervisor, and orchestration runner. Previously flagged issues (crash returning exit code 0, empty-string deployment.acp_url falling through incorrectly) are both addressed in this revision.
  • ACP host rewrite (resolve_acp_url): applied consistently across all three ACP dial sites (ACP use case, API-key proxy, Temporal healthcheck activity). Safe no-op in Docker/staging/prod when AGENTEX_ACP_HOST_OVERRIDE is unset.
  • dev.sh enhancements: no-docker subcommand, mode-file tracking for stop/status/restart, kill_tree + sweep_stray_frontends to fix the frontend process-leak bug, and a preflight port-conflict checker.

Confidence Score: 5/5

  • Safe to merge — the new docker-free runner is additive and isolated behind an explicit subcommand; the ACP host-rewrite is a no-op in all non-local environments; and the two regressions called out in previous rounds are both corrected here.
  • The orchestration, teardown, and mode-aware stop/status paths are well-structured. The ACP host-rewrite correctly covers all three dial sites and has no effect in Docker or production (env var absent). The empty-string deployment URL fallback and the crash exit-code were the two outstanding defects; both are fixed. The only remaining finding is that URL credentials are dropped by the netloc reconstructor — an edge case in a dev-tool path used exclusively with the local override.
  • No files require special attention beyond the credential-preservation note in agentex/src/utils/acp_url.py.

Important Files Changed

Filename Overview
agentex/src/utils/acp_url.py New utility that rewrites host.docker.internal to a local override in no-docker mode. Logic is clean and safe; minor issue with credentials being dropped when rewriting the netloc.
agentex/scripts/dev_nodocker/runner.py Orchestration layer that provisions datastores, runs migrations, and supervises the API + worker. Previously flagged crash-exit-code bug (returning 0) is correctly fixed to return 1. Clean teardown in finally block.
agentex/scripts/dev_nodocker/services.py Provisions embedded Postgres (pgserver), Redis (redislite), Temporal, MongoDB, and the OTel collector. Fail-fast on missing mongod with actionable install messages; OTel gracefully degrades. Teardown handles edge cases including the daemonized Redis PID.
agentex/scripts/dev_nodocker/supervise.py Subprocess helpers: spawn with PIPE, TCP readiness probing, HTTP health polling, graceful SIGTERM → SIGKILL terminate. Correct writer.wait_closed() usage.
agentex/scripts/dev_nodocker/config.py Pure config/env builder — no side effects, unit-testable. build_env properly removes env vars for optional services that weren't started. resolve_config covers ephemeral, custom data-dir, and default paths.
agentex/src/domain/use_cases/agents_acp_use_case.py Previously flagged empty-string deployment.acp_url regression is correctly addressed — new code uses truthiness (if not raw) throughout, so an empty string from a deployment falls through to agent.acp_url instead of being dialed as "". acp_url_override also correctly passes through resolve_acp_url.
agentex/src/domain/use_cases/agent_api_keys_use_case.py Applies resolve_acp_url to the agent-API-key proxy path so the host rewrite covers all three ACP dial sites.
agentex/src/temporal/activities/healthcheck_activities.py Applies resolve_acp_url to the Temporal healthcheck activity, completing the trio of ACP dial sites that need the host rewrite.
dev.sh Large but well-structured refactor. Adds no-docker subcommand with mode-file tracking, preflight_port_check, kill_tree, sweep_stray_frontends, and proper stop/status awareness. OTel binary installer proceeds without verification if the checksums file download fails (previously flagged P2, not yet addressed).
agentex/Makefile Adds dev-no-docker target. Includes a TODO without a ticket number.

Sequence Diagram

sequenceDiagram
    participant devsh as dev.sh no-docker
    participant runner as runner.run()
    participant pg as pgserver (embedded)
    participant redis as redislite (embedded)
    participant mongo as mongod (local)
    participant temporal as Temporal dev server
    participant api as uvicorn API
    participant worker as Temporal worker

    devsh->>runner: asyncio.run(run(cfg))
    runner->>pg: provision_postgres(cfg)
    pg-->>runner: (server, database_url)
    runner->>redis: provision_redis(cfg)
    redis-->>runner: (server, redis_url)
    runner->>mongo: provision_mongo(cfg)
    mongo-->>runner: (proc, mongo_uri)
    runner->>temporal: provision_temporal(cfg)
    temporal-->>runner: (env, temporal_address)
    runner->>runner: build_env(...)
    runner->>runner: run_migrations(cfg, env)
    runner->>api: spawn("api", uvicorn ...)
    runner->>worker: spawn("worker", run_worker.py)
    runner->>runner: wait_for_health(/healthz)
    runner->>runner: asyncio.wait([stop, proc exits])
    Note over runner: Ctrl-C / SIGTERM sets stop
    runner->>worker: terminate(SIGTERM → SIGKILL)
    runner->>api: terminate(SIGTERM → SIGKILL)
    runner->>temporal: env.shutdown()
    runner->>mongo: terminate(SIGTERM → SIGKILL)
    runner->>redis: teardown_redis()
    runner->>pg: pg_server.cleanup()
Loading

Reviews (11): Last reviewed commit: "Merge branch 'main' into aringuyen/run-l..." | Re-trigger Greptile

@aringuyen3
aringuyen3 requested a review from a team as a code owner July 9, 2026 14:48
Comment thread agentex/src/domain/use_cases/agents_acp_use_case.py Outdated
Stand up the full backend as host processes with embedded datastores — no
Docker daemon required — as a lighter alternative to the container stack.

`./dev.sh local` (also `make dev-local` / `python -m scripts.dev_local`) provisions:
- Postgres via bundled pgserver (unix socket) and Redis via bundled redislite
- a Temporal dev server + UI and the agentex worker (--no-temporal to skip)
- a local mongod, required for the full stack (--no-mongo / --lean to skip)
- an optional OpenTelemetry collector (--no-otel to skip)
then runs migrations, supervises uvicorn + the worker, and tears everything down
cleanly on SIGINT/SIGTERM. --lean is a minimal Postgres+Redis+API stack; --ephemeral
uses a throwaway data dir. The runner is a small scripts/dev_local package
(config / services / supervise / runner) so the pure config/env layer stays testable.

App-side changes to make the no-Docker path robust (all no-ops when Mongo is
configured, as it always is in Docker/prod):
- The Temporal worker no longer crashes when MongoDB is unavailable — the Mongo CRUD
  adapter tolerates an unset database and errors only on real use, so the worker
  degrades like the API instead of taking down the stack.
- Skip the Mongo connection entirely when MONGODB_URI is unset, removing a ~20s
  startup hang against the implicit localhost:27017 default.
- In local mode the backend rewrites agents' host.docker.internal ACP host to loopback
  (AGENTEX_ACP_HOST_OVERRIDE), so default-scaffolded agents work without manifest edits.

Also fix a frontend dev-server process leak in dev.sh (kill the whole make→npm→next
tree and sweep orphans; report status by listening port), correct the MongoDB and
OpenTelemetry install commands, and document local mode in README and CLAUDE.md.
@aringuyen3
aringuyen3 force-pushed the aringuyen/run-local-no-docker branch from 356b3eb to aa8bb61 Compare July 13, 2026 16:34
raw = acp_url_override

# Prefer the production deployment's URL when there's no explicit override.
if raw is None and agent.production_deployment_id:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@smoreinis can you take a look here just make sure it doesn't conflict with the preview workflow?

"""
# In docker-free local mode, rewrite host.docker.internal -> the host-reachable
# override so the healthcheck matches how the request path dials the agent.
acp_url = resolve_acp_url(acp_url)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

just making sure, no other places we need to do conversion right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, only running this mode

if acp_url_override:
return acp_url_override
"""Resolve the ACP URL for an agent, optionally overriding with a specific URL.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

did we lose the override?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hi, so ./dev.sh no-docker will pass _ACP_HOST_OVERRIDE_ENV variable, so the function resolve_acp_url will resolve this. Besides this, the logic of this function is kept the same

Returns the URL unchanged when the override env var is unset or the URL does
not use the Docker sentinel host, so it is safe to call on every ACP dial.
"""
override = os.environ.get(_ACP_HOST_OVERRIDE_ENV)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is this where the override moved?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, here we override the environment variable _ACP_HOST_OVERRIDE_ENV. This environment is set and passed from config.py file

Comment thread agentex/Makefile
Comment thread CLAUDE.md

> **MongoDB is required for the full local stack** and is always started — the Temporal
> worker builds Mongo-backed repositories at startup, so a missing/unreachable Mongo
> makes the runner fail fast (with an install message) rather than crash the worker.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ty for adding here

@danielmillerp danielmillerp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lots of small questions! also as far as testing on PC, not super sure. A lot of our clients do use PCs and I know there are Scaliens who have PCs. What problems do you anticipate?

@levilentz levilentz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall this is rad.

One UX suggestion: in addition dev.sh, I think this would be really nice to have in the agentex CLI. I.E. you can run a single agent inline. therefore to start running a single agent you can start quick rather than having both the agentex backend and cli running. then this setup would be suitable for someone testing a multiagent flow.

Not sure if that is possible given the abstraction and integration, but wanted to throw it out there as a north star.

@danielmillerp

Copy link
Copy Markdown
Collaborator

Overall this is rad.

One UX suggestion: in addition dev.sh, I think this would be really nice to have in the agentex CLI. I.E. you can run a single agent inline. therefore to start running a single agent you can start quick rather than having both the agentex backend and cli running. then this setup would be suitable for someone testing a multiagent flow.

Not sure if that is possible given the abstraction and integration, but wanted to throw it out there as a north star.

+1 to that!

@aringuyen3

Copy link
Copy Markdown
Contributor Author

lots of small questions! also as far as testing on PC, not super sure. A lot of our clients do use PCs and I know there are Scaliens who have PCs. What problems do you anticipate?

@danielmillerp So Im testing this on Windows (via AWS Workspaces) but the embedded Redis uses (the redislite package has no native Windows build (Redis ships no supported Windows server) so the dependency required to run locally without Docker won't even install on native Windows.

Comment thread agentex/scripts/dev_nodocker/runner.py
@aringuyen3 aringuyen3 closed this Jul 17, 2026
@aringuyen3 aringuyen3 reopened this Jul 17, 2026
@aringuyen3

Copy link
Copy Markdown
Contributor Author

Some findings from testing local dev on Windows:

  • I'm testing running locally on native Windows, but the embedded Redis uses the redislite package, which has no native Windows build (Redis ships no supported Windows server). So the dependency required to run locally won't even install on native Windows.
  • Workarounds today: run dev locally with Docker, or use the Docker-free local mode inside WSL2.
  • Open question for FDE / customer environments: can they install WSL2 in their env? I wouldn't assume it's always available.
  • Worth noting: I use AWS Workspace, which doesn't support WSL2 — so even the WSL2 path isn't universal internally.

@NiteshDhanpal had a clear view on how to scope this. His take: don't treat Windows-native local mode as a hard requirement for now (given the redislite/Redis limitation), and use a support matrix instead:

  • Mac/Linux → Docker-free local mode
  • Windows with WSL2 allowed → Docker-free local mode inside WSL2
  • Windows without WSL2 / locked-down envs → Docker-based local dev

His main concern is a reliable fallback so customers aren't blocked when local setup is painful or impossible, and he thinks a cloud/dev-environment fallback is the most reliable option for that.

@danielmillerp

Copy link
Copy Markdown
Collaborator

Some findings from testing local dev on Windows:

  • I'm testing running locally on native Windows, but the embedded Redis uses the redislite package, which has no native Windows build (Redis ships no supported Windows server). So the dependency required to run locally won't even install on native Windows.
  • Workarounds today: run dev locally with Docker, or use the Docker-free local mode inside WSL2.
  • Open question for FDE / customer environments: can they install WSL2 in their env? I wouldn't assume it's always available.
  • Worth noting: I use AWS Workspace, which doesn't support WSL2 — so even the WSL2 path isn't universal internally.

@NiteshDhanpal had a clear view on how to scope this. His take: don't treat Windows-native local mode as a hard requirement for now (given the redislite/Redis limitation), and use a support matrix instead:

  • Mac/Linux → Docker-free local mode
  • Windows with WSL2 allowed → Docker-free local mode inside WSL2
  • Windows without WSL2 / locked-down envs → Docker-based local dev

His main concern is a reliable fallback so customers aren't blocked when local setup is painful or impossible, and he thinks a cloud/dev-environment fallback is the most reliable option for that.

I'm aligned @levilentz does that make sense for customers? Also CC: @lucyakoroleva for context per today's standup discussion

@levilentz

Copy link
Copy Markdown
Contributor

I'm aligned @levilentz does that make sense for customers? Also CC: @lucyakoroleva for context per today's standup discussion

@danielmillerp @aringuyen3 I think broadly this is a good first step. I think assuming that the customer will not have WSL or docker is the way to test this as that it normally the problem.

can we just disable redis in this mode?

@danielmillerp

Copy link
Copy Markdown
Collaborator

I'm aligned @levilentz does that make sense for customers? Also CC: @lucyakoroleva for context per today's standup discussion

@danielmillerp @aringuyen3 I think broadly this is a good first step. I think assuming that the customer will not have WSL or docker is the way to test this as that it normally the problem.

can we just disable redis in this mode?

I like that! not necessary to go from 0 to 1 to stream

Default the no-docker embedded Redis to 6379 (was 6390) and the Temporal
UI to 8080 (was 8233) so both modes expose the same URLs and quick start
needs no port flags. Users can still pass --redis-port / --ui-port if a
Docker service already holds a port.

Also drop the now-unnecessary per-mode Temporal UI branching in dev.sh
and add a README note pointing users to wait for the readiness message
before starting an agent.
@aringuyen3

Copy link
Copy Markdown
Contributor Author

I'm aligned @levilentz does that make sense for customers? Also CC: @lucyakoroleva for context per today's standup discussion

@danielmillerp @aringuyen3 I think broadly this is a good first step. I think assuming that the customer will not have WSL or docker is the way to test this as that it normally the problem.
can we just disable redis in this mode?

I like that! not necessary to go from 0 to 1 to stream

@danielmillerp @levilentz Redis is required. Agentex uses Redis for streaming and messaging. Referred to this PR #343

Add a "Choosing a Setup" section to WINDOWS.md: run docker-free local
mode (./dev.sh no-docker) inside WSL2 when it's available, and fall back
to the Docker-based PowerShell flow when WSL2 isn't (locked-down or
managed environments). Includes a WSL2 quick-start subsection.

Expand the README's Windows pointer to surface the same decision from
the main entry point.
A previous dev stack whose supervisor died without reaping its children
leaves processes bound to the API/datastore ports. The next launch then
fails to bind a managed process and tears the whole stack back down, with
the real EADDRINUSE buried in a background log.

Add a preflight port-conflict check to both start paths that lists any
process already listening on a needed port (with PID + command) and offers
to kill it before launching:
- TTY: prompts [y/N] to kill and continue, else aborts with a clear hint.
- DEV_KILL_PORTS=1: kills without prompting (restart/CI).
- Non-interactive without the opt-in: warns and continues (unchanged).

Kills gracefully via kill_tree (TERM, then force-KILL survivors) so a
supervisor and its datastore children go down together. The no-docker
check honors the runner's port-override flags; Postgres is skipped there
since it uses a Unix socket.
@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Start a greploop in Cursor and it will work through the open comments and keep going until this PR reviews clean.

@socket-security

socket-security Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​pgserver@​0.1.464100100100100
Addedpypi/​redislite@​6.2.91218397100100100100

View full report

@socket-security

socket-security Bot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn Medium
Potential vulnerability: pypi redislite with risk level "medium"

Location: Package overview

From: agentex/pyproject.tomlpypi/redislite@6.2.912183

ℹ Read more on: This package | This alert | Navigating potential vulnerabilities

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: It is advisable to proceed with caution. Engage in a review of the package's security aspects and consider reaching out to the package maintainer for the latest information or patches.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore pypi/redislite@6.2.912183. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@aringuyen3
aringuyen3 merged commit f2c3fd9 into main Aug 10, 2026
46 checks passed
@aringuyen3
aringuyen3 deleted the aringuyen/run-local-no-docker branch August 10, 2026 22:44
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