> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dnzwithazee.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration as code

> Keep a proxy's routes in Git as YAML: export them, see what a file would change, and apply it from the dashboard, the CLI or a CI job.

A proxy's configuration can live in Git. Export it as a YAML file, change the file in a pull
request, and apply it again: DNZ shows what the file would **add, change and remove** before
anything is written, like `terraform plan`. Applying a file the proxy already matches changes
nothing, so a CI job can apply it on every push.

It works the same for Traefik, DNZ Proxy and Caddy.

Coming from Nginx? [Importing its configuration](https://docs.dnzwithazee.com/proxy/nginx-import.md) gives you such a file to start from.

## What the file holds

* **Routes**, as the route dialog shows them: hostnames, path, where they go (upstreams of their own
  or an upstream of the list), HTTPS, every switch (cache, firewall, rate limit, password,
  sign-in, address list, custom headers, upload limits, under attack, waiting room,
  [web application firewall](https://docs.dnzwithazee.com/proxy/waf.md)), the redirect and the group. Routes set up under
  YAML or the dialog's *Matching* come along with their conditions, priority and entry points.
  [TCP and UDP routes](#tcp-and-udp-routes) are routes too.
* **Upstreams** and **middlewares** the routes use, by name, with each upstream's connection settings
  (`transport`). Both are every proxy's: changing one in the file changes it on every proxy that uses
  it, and the plan says which. Files from before upstreams, with `services:` and `service:`, still
  apply.
* **Route defaults** of a DNZ Proxy: redirect to https\://, compression, security headers, the
  firewall on every route, upload limits.

Everything is named, never numbered: services, middlewares, the firewall and sign-in providers.

```yaml edge.yaml theme={null}
version: 2
proxy: edge
kind: dnz-proxy
defaults:
  redirect_https: true
  compress: true
  security_headers: false
routes:
- name: app-example-com
  hosts:
  - app.example.com
  servers:
  - http://10.0.1.20:3000
  - url: http://10.0.1.21:3000
    weight: 3
  - url: https://10.0.1.22:3443
    weight: 1
    skip_tls_verify: true   # any certificate from this server
  https: true
  group: Web app
  cache: 10m
  basic_auth:
  - admin
- name: api
  hosts:
  - api.example.com
  path: /v1
  upstream: backend
  https: true
  rate_limit:
    average: 50
    burst: 100
upstreams:
- name: backend
  servers:
  - http://10.0.2.11:8080
  health_check:
    path: /health
    interval: 10
  transport:
    serverName: backend.internal
    forwardingTimeouts:
      dialTimeout: 5s
```

| Key | Meaning |
| - | - |
| `version` | `2` since DNZ v2. Files with `version: 1` still load; a middleware or upstream the v2 upgrade renamed on this proxy is refused with its new name (*office of edge2 is office-edge2 since DNZ v2: export the proxy again*) |
| `proxy`, `kind` | The proxy it is for, by name, and its kind (`traefik`, `dnz-proxy`, `caddy`), for the reader. A file applies to another proxy, of another kind too, as long as it uses nothing that kind lacks |
| `defaults` | A DNZ Proxy's route defaults; left out, they stay as they are |
| `routes` | The routes |
| `upstreams` | Upstreams by name: servers (a URL, or `url`, `weight`, `preserve_path`, `skip_tls_verify` for an `https://` server whose certificate isn't checked), balancing, sticky sessions, health check, and `transport`: its [connection settings](https://docs.dnzwithazee.com/proxy/upstreams.md#connection-settings) in the shape of Traefik's servers transport (server name, `rootCAs`, client `certificates`, timeouts). `from_dns` instead of `servers` takes them from a [DNS name](#servers-from-a-dns-name) |
| `middlewares` | Middlewares by name: `kind` (the form, such as `stripPrefix` or `dnzFirewall`) and `config` in Traefik's shape. Every proxy's: one another proxy uses stays when this file prunes it, and a change reaches every proxy using it (the plan says which) |

A route without a `name` is named after its first hostname (`app.example.com` becomes
`app-example-com`); the name is how a route is found again, so the same name updates the same route.
Unknown keys are refused, so a typo such as `cahce: 10m` is an error instead of being ignored.

### Servers from a DNS name

A load balancer whose servers come from [every address of a DNS name](https://docs.dnzwithazee.com/proxy/upstream-discovery.md) has
`from_dns` instead of `servers`. The servers DNZ found are never in the file, so a plan doesn't change
when the name's addresses do.

```yaml theme={null}
upstreams:
- name: swarm
  from_dns:
    mode: dns
    name: swarm-nodes.example.local
    port: 8080
    scheme: http          # https or h2c; TCP upstreams take tls: true instead
    family: v4            # v6 or both
    resolve_on: dnz       # proxy: each DNZ Proxy node and Caddy look it up (tasks.<service>)
    min_interval_secs: 30
```

## TCP and UDP routes

A route with `protocol: tcp` or `protocol: udp` is a [TCP or UDP route](https://docs.dnzwithazee.com/proxy/upstreams.md#tcp-and-udp-routes)
(Traefik and DNZ Proxy; Caddy has none). It goes to `servers` of its own (`host:port`) or to an
`upstream` of the list with the same `protocol`. The TCP middlewares it runs are listed under
`middlewares` with `protocol: tcp`, shared by every proxy's TCP routes like the HTTP ones.

```yaml theme={null}
routes:
- name: db
  protocol: tcp
  entry_points: [pg]
  hosts: [db.example.com]   # by TLS hostname (SNI); left out: every connection on the port
  tls: passthrough          # or terminate (the proxy holds the certificate); left out: no TLS
  servers: [10.0.0.7:5432]
  client_ips: [10.0.0.0/8]
  middlewares: [office-only]
- name: resolvers
  protocol: udp
  entry_points: [dns]
  upstream: resolvers
upstreams:
- name: resolvers
  protocol: udp
  servers: [10.0.0.53:53]
middlewares:
- name: office-only
  protocol: tcp
  kind: ipAllowList
  config:
    ipAllowList:
      sourceRange: [10.0.0.0/8]
```

| Key | Meaning |
| - | - |
| `protocol` | `tcp` or `udp`; left out: an HTTP route |
| `entry_points` | The TCP or UDP entry point (a port of the proxy) it listens on |
| `hosts` | TCP with TLS: the hostnames it takes (SNI), in DNZ's zones; left out: every connection |
| `tls` | TCP: `terminate` (the proxy ends TLS, with `cert_resolver` as for HTTP) or `passthrough` (the servers end it, the proxy reads only the hostname) |
| `client_ips` | TCP: only these addresses or ranges |
| `priority` | TCP: the higher one wins between routes on one entry point |
| `middlewares` | TCP: TCP middlewares (`ipAllowList`, `inFlightConn`) by name |

UDP routes take no hostnames, TLS, client addresses or middlewares. The route dialog's checks apply:
routing by hostname needs TLS, and a name another route of the proxy takes is refused. A
`version: 1` file has no TCP or UDP routes: applying it, even with `--prune`, leaves the proxy's as
they are.

## Export and import in the dashboard

**1. Export**

On the proxy's page, open **YAML → Export**. The file downloads as `<proxy>.yaml`.

**2. Change it**

Edit it, or keep it in a Git repository and change it there.

**3. Import**

Open **YAML → Import…**, choose the file or paste it, and click **Show changes**. Each route,
upstream and middleware that would change is listed with its fields before and after.

**4. Apply**

Click **Apply**. Turn on **Remove what the file doesn't have** to delete routes, services and
middlewares the file leaves out.

## From the CLI

```bash theme={null}
dnz proxy export edge > edge.yaml           # one proxy
dnz proxy export --all > proxies.yaml       # every proxy, --- between them
dnz proxy plan -f edge.yaml                 # what would change; nothing is written
dnz proxy apply -f edge.yaml                # apply it
dnz proxy apply -f edge.yaml --prune        # and remove what the file doesn't have
dnz proxy apply -f edge.yaml --proxy edge-2 # the same routes on another proxy
```

```text theme={null}
edge: 1 to add, 1 to change, 0 to remove.

+ route shop-example-com
    hosts: ["shop.example.com"]
    servers: ["http://10.0.3.5:8080"]
    https: true
~ route app-example-com
    cache: 10m → 1h
+ dns shop.example.com
    a new record pointing at edge
```

## How applying works

* **Nothing half applied.** Every check the route dialog makes runs first: hostnames in your zones,
  one route per hostname, upstreams and middlewares that exist, sign-in providers, switches the
  proxy's kind runs. An error names the route and its line, for example
  `route shop-example-com (line 14): unknown key cahce`, and nothing changes.
* **Nothing removed unless you ask.** Routes, services and middlewares the file leaves out stay;
  `--prune` (or **Remove what the file doesn't have**) deletes them, and their hostnames stop going
  through the proxy. An upstream or middleware a route still uses (on any proxy) is never removed.
* **DNS follows.** A new route's hostnames go through the proxy, with a new record pointing at it
  when a name has none. `dns_record: false` on a route leaves DNS alone.
* **Applying twice changes nothing.**
* **History kept.** Each route a file adds, changes or removes gets a version in
  [route history](https://docs.dnzwithazee.com/proxy/route-history.md), and every apply is in the audit log.

## Secrets

> [!NOTE]
>
> Password hashes are left out of an export: each basic auth user is listed by name only, and
> applying keeps the hash the proxy already has for that user. A user the proxy doesn't have needs
> one, written `name:HASH` (`htpasswd -nbB name password` makes it). **Export with password
> hashes** (`--secrets`) puts them in, for a copy that recreates the users on another server. Keep
> such a file out of public repositories.

The **keys of an upstream's client certificates** (`transport.certificates`) are left out the same
way: applying a certificate without its key keeps the key DNZ has, and `--secrets` puts them in.

Sign-in routes name their provider; the provider and its client secret stay under
**Settings → Proxy → Sign-in** and are never exported. The proxy itself (its addresses, token and
Traefik API password), DNZ Proxy's node settings, TLS certificates and entry points (the proxy's
**Settings**) aren't in the file and are never touched by it, with or without prune.

## GitOps with GitHub Actions

Keep the files in a repository. Pull requests show the plan; a merge to `main` applies it. Give the
job an [API token](https://docs.dnzwithazee.com/cli/authentication.md) of a user who manages proxies and your DNZ server's address.

```yaml .github/workflows/proxies.yml theme={null}
name: proxies
on:
  pull_request:
    paths: ["proxies/**"]
  push:
    branches: [main]
    paths: ["proxies/**"]
jobs:
  apply:
    runs-on: ubuntu-latest
    env:
      DNZ_SERVER: https://dnz.example.com
      DNZ_TOKEN: ${{ secrets.DNZ_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - name: Install dnz
        run: curl -fsSL https://raw.githubusercontent.com/terrific-ci/dnz/main/install.sh | sudo sh
      - name: Plan
        run: for f in proxies/*.yaml; do dnz proxy plan -f "$f"; done
      - name: Apply
        if: github.event_name == 'push'
        run: for f in proxies/*.yaml; do dnz proxy apply -f "$f"; done
```

> [!TIP]
>
> A scheduled `dnz proxy plan -f proxies/edge.yaml --exit-code` exits with 2 when someone changed
> the proxy in the dashboard and it no longer matches the file: a drift check.

## API

| Call | What it does |
| - | - |
| `GET /api/proxies/{id}/export` | The proxy as YAML (`?secrets=true`: with password hashes) |
| `GET /api/proxies/export` | Every proxy, one YAML document each |
| `POST /api/proxies/{id}/plan` | `{"content": "<YAML>", "prune": false}`: what would change, nothing written |
| `POST /api/proxies/{id}/apply` | The same body: applies it and answers with what changed |

An AI assistant can export a proxy and plan a file it drafted (`proxy_config_export`,
`proxy_config_plan`); applying stays with people. See [AI assistants](https://docs.dnzwithazee.com/integrations/ai-assistants.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.