Traefik discovers `app` replicas from Docker labels instead of a static upstream, so
`docker compose up --scale app=N` load-balances without reconfiguring the proxy.

<Callout title="The one rule">
  Traefik never mounts the raw Docker socket. A filtered `tecnativa/docker-socket-proxy` companion
  exposes only containers / events / ping / version / networks on the internal compose network.
</Callout>

## Quick start

<Steps>

<Step>
### Pin the proxy

```typescript title="oke.config.ts"
images: {
  proxy: "traefik:v3.7",
},
```

</Step>

<Step>
### Hostname + ACME email

```bash title=".env.local"
OKE_PROXY_HOST=app.example.com
OKE_PROXY_ACME_EMAIL=admin@example.com
```

`OKE_PROXY_ACME_EMAIL` defaults to `admin@example.com` if unset — set a real mailbox
before production ACME.

</Step>

<Step>
### Scale the app

```bash
docker compose … up -d --scale app=3
```

App labels: `traefik.http.routers.app.rule=Host(...)`, TLS cert resolver `letsencrypt`,
load-balancer port `6530`.

</Step>

</Steps>

## Required env

| Variable                | Required?            | Meaning                                        |
| ----------------------- | -------------------- | ---------------------------------------------- |
| `OKE_PROXY_HOST`        | **Yes** (production) | Host rule + ACME identity; default `localhost` |
| `OKE_PROXY_ACME_EMAIL`  | **Yes** (production) | Let's Encrypt account email                    |
| `allowedHosts` (config) | Production           | Must include the public hostname               |

## Data and backup

| Volume              | Path                      | What it stores                                    |
| ------------------- | ------------------------- | ------------------------------------------------- |
| `proxy-letsencrypt` | `/letsencrypt`            | `acme.json` — certificates + ACME account         |
| Socket proxy only   | `/var/run/docker.sock:ro` | **Only** the `socket-proxy` companion mounts this |

**Backup means:** preserve the `proxy-letsencrypt` volume (especially `acme.json`). Losing
it forces re-issuance and can hit Let's Encrypt rate limits. The socket-proxy has no
durable state.

## Production note

Security posture is intentional: Traefik talks to `tcp://socket-proxy:2375` with a
filtered API (`CONTAINERS`, `EVENTS`, `PING`, `VERSION`, `NETWORKS` only). Never mount
raw `docker.sock` on Traefik — keep the socket-proxy companion in front.

HTTP entrypoint redirects to `websecure`. For a single instance with no scale plans,
[Caddy](/docs/recipes/caddy) is simpler (no labels, no companion).

## What the recipe configures

| Field       | Value                                              |
| ----------- | -------------------------------------------------- |
| Ports       | `80` + `443`                                       |
| Providers   | Docker via socket-proxy, `exposedbydefault=false`  |
| Companion   | `tecnativa/docker-socket-proxy:v0.5.0`             |
| Healthcheck | `traefik healthcheck --ping`, every 10s, 5 retries |

## Troubleshooting

<Accordions>
<Accordion title="404 / no backend — Traefik cannot see app">

Usually `socket-proxy` is down, or `app` lacks Traefik labels. Confirm
`depends_on: socket-proxy` and that `compose.proxy.yml` merged the app labels.

Logs showing Docker provider connection errors point at the socket-proxy, not Traefik
config syntax.

</Accordion>
<Accordion title="oke dev: service app has neither an image nor a build context">

`oke dev` starts infra only; the app stays on the host at `:6530`.
Traefik labels apply when `app` is in Compose — use `oke docker` for that.
Re-run `oke dev` after upgrading so compose drops the label-only `app`.

</Accordion>
<Accordion title="ACME email rejected / rate limited">

Replace the default `admin@example.com` with a real `OKE_PROXY_ACME_EMAIL`. If you wiped
`proxy-letsencrypt`, you may be rate-limited — restore `acme.json` from backup instead of
re-issuing repeatedly.

</Accordion>
</Accordions>

## Learn more

- [Caddy](/docs/recipes/caddy) — simpler single-instance HTTPS edge
- [nginx](/docs/recipes/nginx) — static HTTP reverse proxy
- [Security](/docs/reference/security) — `allowedHosts`

## Next

<Cards>
  <Card title="Caddy" description="Single-instance alternative." href="/docs/recipes/caddy" />
  <Card title="nginx" description="Static HTTP reverse proxy." href="/docs/recipes/nginx" />
  <Card
    title="PgDog"
    description="Pool Postgres behind the same stack."
    href="/docs/recipes/pgdog"
  />
</Cards>
