# Dokploy (/docs/deployment/dokploy)



Ductor ships a Dokploy-flavored compose file, `docker-compose.dokploy.yml`, that
runs the **production** image behind Dokploy's Traefik ingress. It serves the
REST/Connect API and the Scalar docs on `:8080` and exports OTLP traces to
VictoriaTraces; Traefik routes the public hostnames and issues TLS. The
hostnames below (`api.ductor.io`, `docs.ductor.io`, …) are the reference
deployment's — treat them as examples and substitute your own.

```mermaid
flowchart LR
  client["Client"] -->|HTTPS| traefik["Traefik ingress"]
  traefik -->|":8080"| api["ductor-api"]
  api --> pg[("timescale")]
  api --> df[("dragonfly")]
  api -.->|OTLP| traces["victoriatraces"]
```

## What's different from the dev compose [#whats-different-from-the-dev-compose]

* The `ductor&#x60; service builds the &#x2A;*`production`** target (not `development`).
* Passwords come from Dokploy environment variables, not hardcoded defaults.
* The full VictoriaMetrics observability stack is included and tracing is on.
* The service joins both the internal `ductor` network and Dokploy's
  `dokploy-network` (with a stable `ductor-api` alias) so Traefik and in-cluster
  callers can route to it.

## The single :8080 surface [#the-single-8080-surface]

The API listener serves everything on `:8080`:

| Path            | Serves                                                |
| --------------- | ----------------------------------------------------- |
| `/docs`         | Scalar API reference UI                               |
| `/openapi.yaml` | The OpenAPI spec                                      |
| `/health`       | Liveness endpoint (used by the container healthcheck) |

The `ductor` service command and key configuration:

```yaml
# Overrides the baked Dockerfile CMD to drop the removed --grpc-addr flag —
# the current `serve` command no longer defines it (gRPC rides Connect on :8080).
command: ["serve", "--http-addr=:8080", "--metrics-addr=:9090"]
environment:
  DUCTOR_DATABASE_URL: postgres://ductor:${POSTGRES_PASSWORD}@timescale:5432/ductor?sslmode=disable
  DUCTOR_CACHE_URL: redis://:${DRAGONFLY_PASSWORD}@dragonfly:6379
  DUCTOR_API_ENABLED: "true"
  DUCTOR_API_ADDR: ":8080"
  DUCTOR_API_ALLOWED_HOSTS: "localhost,127.0.0.1,::1,0.0.0.0,api.ductor.io,docs.ductor.io,ductor-api"
  DUCTOR_CONNECTOR_ENCRYPTION_KEY: ${CONNECTOR_ENCRYPTION_KEY}
  DUCTOR_ROUTER_QUEUE_ENABLED: "true"
  DUCTOR_ROUTING_DAG_BRIDGE_ENABLED: "true"
  DUCTOR_AGGREGATOR_BACKEND: "redis"
  DUCTOR_TRACING_ENABLED: "true"
  DUCTOR_TRACING_ENDPOINT: "victoriatraces:4317"
  DUCTOR_TRACING_GRPC: "true"
networks: [ductor, dokploy-network]
```

<Callout type="warn" title="allowed_hosts must include your domains and loopback">
  `DUCTOR_API_ALLOWED_HOSTS` is a DNS-rebinding allowlist with exact-match
  semantics — requests whose `Host` header isn't on it are rejected. It must
  include **every** hostname Traefik forwards *and* loopback
  (`localhost`/`127.0.0.1`), because the container's healthcheck hits
  `http://localhost:8080/health`. Drop loopback and the container is marked
  unhealthy and Traefik stops routing to it.
</Callout>

## Required environment [#required-environment]

Provide these as Dokploy environment variables (they come from the Dokploy
Environment tab and are never committed):

| Variable                   | Purpose                                                  |
| -------------------------- | -------------------------------------------------------- |
| `POSTGRES_PASSWORD`        | TimescaleDB password (used in `DUCTOR_DATABASE_URL`)     |
| `DRAGONFLY_PASSWORD`       | Dragonfly password (used in `DUCTOR_CACHE_URL`)          |
| `CONNECTOR_ENCRYPTION_KEY` | Base64 32-byte AEAD master key for connector credentials |
| `GF_ADMIN_PASSWORD`        | Grafana admin password                                   |
| `DUCTOR_AUTH_API_KEYS`     | DB/static API keys, if you enable key auth (optional)    |
| `DUCTOR_VERSION`           | Image version tag (optional; defaults to `prod`)         |

The reference deployment boots app-open (`DUCTOR_AUTH_ENABLED` defaults to
`false`); lock it down with Traefik basic-auth in front, or switch to OIDC /
DB-backed API keys via the auth env vars. For a production posture, follow the
[production checklist](/docs/deployment/production).

## Deploy steps [#deploy-steps]

<Steps>
  <Step>
    **Create the app** in Dokploy from the repository, selecting Docker Compose
    and `docker-compose.dokploy.yml`.
  </Step>

  <Step>
    **Set the environment variables** above.
  </Step>

  <Step>
    **Add domains** in Dokploy for the `ductor` service (e.g. your API host),
    which provisions Traefik routing and TLS. Ensure those hosts are in
    `DUCTOR_API_ALLOWED_HOSTS`.
  </Step>

  <Step>
    **Deploy.** The compose file sets `DUCTOR_DATABASE_AUTO_MIGRATE: "true"`, so
    the schema migrates on first boot. For stricter control, set it to `false`
    and run `ductor migrate up` as a one-off (see
    [Migrations](/docs/operations/migrations)).
  </Step>

  <Step>
    **Verify** by hitting `https://<your-domain>/health` and the Scalar reference
    at `https://<your-domain>/docs`.
  </Step>
</Steps>

## Backups and observability [#backups-and-observability]

Dokploy can schedule Postgres backups of the `timescale` service directly. The
compose file also brings up the full observability stack (Grafana on `:3000`),
so metrics, logs, and traces are available out of the box — see
[Observability](/docs/operations/observability) and
[Backup & restore](/docs/operations/backup-restore).
