# Run Ductor locally (/docs/guides/run-locally)



This guide stands up a complete local Ductor stack — TimescaleDB, Dragonfly, and
the Ductor binary — using the repository's `docker-compose.yml`, then verifies
the API is healthy and serving.

<Callout type="info" title="This page is the environment reference; Getting Started is the journey">
  This guide covers bring-up mechanics and environment detail — service names,
  ports, `DUCTOR_*` variables, the compose stack, and the bare-binary path. If you
  want the guided walkthrough that seeds a tenant, defines a workflow, and proves
  durable execution by crashing a run, start with
  [Getting Started](/docs/getting-started).
</Callout>

## Prerequisites [#prerequisites]

* **Docker** and **Docker Compose v2** (`docker compose`, not the legacy
  `docker-compose`)
* The Ductor repository checked out locally

## Bring up the core stack [#bring-up-the-core-stack]

The compose project defines three core services:

| Service     | Image                                                               | Purpose                               | Host port               |
| ----------- | ------------------------------------------------------------------- | ------------------------------------- | ----------------------- |
| `ductor`    | `ductor:dev` (built from `docker/Dockerfile`, `development` target) | The application                       | `8080`, `50051`, `9090` |
| `timescale` | `timescale/timescaledb:2.25.0-pg16`                                 | PostgreSQL + TimescaleDB              | `5433` → 5432           |
| `dragonfly` | `dragonflydb/dragonfly:v1.39.0`                                     | Redis-compatible cache / queue / flow | `6379`                  |

<Callout type="warn" title="Service names, not host names">
  The Postgres service is named &#x2A;*`timescale`*&#x2A; and the Redis-compatible service
  is named &#x2A;*`dragonfly`** — there is no service called `postgres` or `redis`.
  Inside the compose network, Ductor reaches them at `timescale:5432` and
  `dragonfly:6379`.
</Callout>

Start everything:

```bash
docker compose up
```

Compose wires the two connection URLs and other development defaults into the
`ductor` service automatically:

```yaml
DUCTOR_DATABASE_URL: postgres://ductor:ductor@timescale:5432/ductor?sslmode=disable
DUCTOR_CACHE_URL: redis://:ductor@dragonfly:6379
DUCTOR_DATABASE_AUTO_MIGRATE: "true"   # dev convenience; run migrations explicitly in prod
DUCTOR_AUTH_ENABLED: "false"           # open API for local development
DUCTOR_AUTH_ALLOW_ANONYMOUS: "true"
DUCTOR_LOG_LEVEL: info
DUCTOR_LOG_FORMAT: json
```

Because `DUCTOR_DATABASE_AUTO_MIGRATE` is `true` in compose, the database schema
is migrated automatically on first boot. In production this flag defaults to
`false` and you run migrations explicitly — see
[Database migrations](/docs/operations/migrations).

## Run without Docker [#run-without-docker]

If you have Go 1.25 and a Postgres/Redis pair already, you can run the binary
directly. Pick the `dev` subcommand for fast local hacking, or `serve` for a
production-shaped run.

<Tabs items="[&#x22;dev (fastest)&#x22;, &#x22;serve (production-shaped)&#x22;]">
  <Tab value="dev (fastest)">
    The `dev` subcommand embeds an in-process Redis (miniredis) and forces
    development-friendly defaults (auth off, anonymous allowed, auto-migrate on):

    ```bash
    export DUCTOR_DATABASE_URL="postgres://ductor:ductor@localhost:5432/ductor?sslmode=disable"
    go run ./cmd/ductor dev
    ```

    <Callout title="dev still needs Postgres">
      `ductor dev` only replaces Redis with an in-process stand-in. A reachable
      PostgreSQL database is still required — set `DUCTOR_DATABASE_URL` or pass
      `--database-url`.
    </Callout>
  </Tab>

  <Tab value="serve (production-shaped)">
    Use `serve` with explicit URLs and run migrations yourself first:

    ```bash
    export DUCTOR_DATABASE_URL="postgres://ductor:ductor@localhost:5432/ductor?sslmode=disable"
    export DUCTOR_CACHE_URL="redis://localhost:6379"

    ductor migrate up      # apply schema
    ductor serve           # start the server
    ```
  </Tab>
</Tabs>

## Ports [#ports]

Ductor serves on three listeners by default:

| Port    | Protocol                                                        | Notes                                                                                     |
| ------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `8080`  | HTTP — REST (grpc-gateway) **and** Connect-RPC on the same port | `server.http_addr`                                                                        |
| `50051` | gRPC                                                            | exposed by the container; `api.addr` defaults to `:50052` for the native Connect listener |
| `9090`  | Prometheus metrics (`/metrics`)                                 | `server.metrics_addr`                                                                     |

## Verify it's healthy [#verify-its-healthy]

The container ships a healthcheck (`wget --spider http://localhost:8080/health`).
You can hit the same endpoints yourself:

```bash
# process-level check via the CLI
ductor health

# liveness / readiness over HTTP
curl -s http://localhost:8080/health
curl -s http://localhost:8080/ready
```

`/health` reports process liveness; `/ready` reports readiness (dependencies
reachable, migrations current). See
[Health checks & readiness](/docs/operations/health-checks) for what each probe
actually verifies and for the registered path aliases.

## Explore the API [#explore-the-api]

With `DUCTOR_AUTH_ENABLED=false` (the compose default), you can call the REST
surface without a token — every `/api/...` route is open on `:8080`. The
interactive API reference is served straight from the running instance at
`http://localhost:8080/docs` (Scalar), and the raw spec at
`http://localhost:8080/openapi.yaml`.

For a guided first request — seeding a tenant, defining a workflow, and running
it end to end — follow [Getting Started](/docs/getting-started).

## Turn on the observability stack (optional) [#turn-on-the-observability-stack-optional]

The compose file defines a full VictoriaMetrics-based observability stack behind
the `observability` profile (VictoriaMetrics, VictoriaLogs, VictoriaTraces,
vmagent, vmalert, Alertmanager, Vector, Grafana). Bring it up with:

```bash
docker compose --profile observability up
# or, if the repo Makefile is present:
make up-full
```

Grafana is then available at `http://localhost:3000` (`admin` / `admin` by
default). See [Observability](/docs/operations/observability) for how traces,
metrics, and logs are wired.

## Next steps [#next-steps]

<Cards>
  <Card title="Define & publish a workflow" href="/docs/guides/define-workflow">
    Author your first DAG definition.
  </Card>

  <Card title="Configuration reference" href="/docs/reference/configuration">
    Every `DUCTOR_*` variable, grouped by subsystem.
  </Card>
</Cards>
