# Running the panel inside an existing Dokploy

When you already run [Dokploy](https://dokploy.com), deploy **the panel itself as
a Dokploy Compose application** instead of using `install.sh`. Dokploy owns the
edge (its Traefik on 80/443) and the lifecycle; the panel rides on Dokploy's
shared `dokploy-network`, and every customer app the panel installs routes
through the *same* Traefik automatically.

This uses Dokploy's own projects/services for the **panel**. The customer apps
the panel deploys are **not** Dokploy projects — the panel remains their control
plane; they just borrow Dokploy's Traefik network for routing.

## Deploy

1. **Create a Compose service** in Dokploy.
   - Source: the LicensePanel git repo.
   - Compose path: `infra/panel-stack/docker-compose.dokploy.yml`.
   - The bootstrap init image **builds from the cloned repo** — no pre-published
     image is required.
2. **Set environment** (Dokploy → Environment). Copy
   [`.env.dokploy.example`](.env.dokploy.example) and fill it in. These are the
   only variables the compose reads — everything else the panel needs (`Panel__*`,
   `ConnectionStrings__PanelDb`, `ASPNETCORE_URLS`, the docker-proxy endpoint,
   the `dokploy` edge preset) is **pre-wired in the compose** and must not be set
   by hand.

   | Variable | Required | Default | Purpose |
   |---|---|---|---|
   | `POSTGRES_PASSWORD` | **yes** | — | Postgres password, baked into the cluster on first boot. Don't change it afterwards (the data volume keeps the original). |
   | `PANEL_HOST` | **yes** | — | The panel's domain, e.g. `panel.example.com`. Used in the Traefik `Host(...)` router rule. |
   | `PANEL_IMAGE` | no | `ditssa/licensepanel:latest` | Full panel image ref (`repo:tag`) in one var. Pin a version with the tag, e.g. `ditssa/licensepanel:1.6.1`, or point at a private mirror. Use ≥ `1.6.0` (see gotchas). |
   | `POSTGRES_IMAGE` | no | `postgres:17.2-alpine` | Full Postgres image ref. |
   | `DOCKERPROXY_IMAGE` | no | `tecnativa/docker-socket-proxy:0.3.0` | Full docker-socket-proxy image ref. |
   | `LICENSE_SERVER_BASE_URL` | no | `https://license-api.mod-sol-sa.com` | LicenseServer endpoint. The panel self-registers a free-tier license by fingerprint; no API key needed. Override only for an on-prem LicenseServer. |

   **Deliberately NOT set here** (unlike `install.sh`): `ACME_EMAIL` and the edge
   ports (`HTTP_PORT`/`HTTPS_PORT`/`BOOTSTRAP_PORT`) — **Dokploy's Traefik owns
   the edge and ACME**, so configure the Let's Encrypt email in Dokploy's Traefik
   settings, not here. `PANEL_DATA_ROOT` is fixed to `/var/lib/licensepanel`
   (the host bind + bootstrap depend on it); don't override it. There is no
   `Panel__Updates__*` self-update config — in Dokploy you update by bumping
   `PANEL_IMAGE`'s tag and redeploying.
3. **Attach the domain** `PANEL_HOST` in Dokploy's UI (or rely on the Traefik
   labels already in the compose). Either way the panel is served at
   `https://PANEL_HOST` via Dokploy's Traefik, TLS issued by the `letsencrypt`
   resolver.
4. **Deploy.** First boot runs `panel-bootstrap` (TLS material + state dirs),
   then panel + Postgres start.
5. **Initial Configuration.** Open `https://PANEL_HOST/` — a fresh install
   redirects to `/initial-configuration`; set the admin and submit.

## Initial Configuration & the bootstrap window

First boot opens a **60-minute bootstrap window** during which
`/initial-configuration` is reachable without auth. If it lapses **before** you
create an admin, `/initial-configuration` redirects to `/auth/signin` (status
`reactivation_required`) — this is expected, not a failure. Re-open it from the
host with one `docker exec` (the panel is the only container on the
`ditssa/licensepanel` image; Dokploy gives it a project-prefixed name like
`license-panel-…-panel-1`, so match by image, not a fixed name):

```bash
# Find the panel container (image = ditssa/licensepanel, not -installer):
docker ps --format '{{.Names}}\t{{.Image}}' | grep -i 'ditssa/licensepanel:'

cid=<that-name>
docker exec "$cid" /app/licensepanel server bootstrap status     # → reactivation_required
docker exec "$cid" /app/licensepanel server bootstrap reactivate  # → fresh 60-min window
```

Then reload `/initial-configuration` and submit promptly. `reactivate` refuses
(`Reactivated=false`) once an admin exists — at that point setup is already
done, so just sign in. A redeploy does **not** reopen the window (state persists
in the `panel_data` volume); only `reactivate` does.

## Updating a Dokploy panel

You update by **redeploying in Dokploy**, not via the in-app updater. The Dokploy
compose deliberately omits the `Panel__Updates__*` self-update wiring (that path
spawns an updater container and is for the standalone install). To update: bump
`PANEL_IMAGE`'s tag (or rely on `pull_policy: always`) and redeploy — `latest`
re-resolves and the new image lands.

The in-UI "update available" badge is driven by the release **feed**
(`license-install.mod-sol-sa.com/latest.json`), which is independent of the panel
and **does not affect** its operation — a `404`/`UPDATE_FEED_UNREACHABLE` is a
benign log line, not a health-degrade. If you want the badge to work, the
**installer-site** service (not the panel) needs `GH_TOKEN` set to a repo-read
PAT and at least one GitHub **Release** to exist (the feed is built from the
Releases API; a private repo 404s without the token). See
`infra/installer/host/README.md`.

## What the compose does (vs the standalone install)

| Concern | Standalone (`install.sh`) | Dokploy |
|---|---|---|
| Edge / TLS | bundled `panel-traefik` owns 80/443 | **Dokploy's Traefik** (no bundled Traefik) |
| Pre-flight (certs, dirs, KEK) | `install.sh` | `panel-bootstrap` init container (reuses the same `infra/installer/lib` scripts) |
| Customer-app routing | `panel-network` | `dokploy-network` via `Panel__Edge__Preset=dokploy` |
| Updates | panel self-update | redeploy in Dokploy (`pull_policy: always` re-pulls; set `PANEL_IMAGE` tag to pin) |

## Hard requirements / gotchas

- **`projects/` is a host bind, host-path == container-path**
  (`/var/lib/licensepanel/projects`). The panel writes each app's compose file
  there and shells `docker compose -f <path>` through the socket-proxy; the host
  daemon resolves that path on its own filesystem. A named volume would break
  customer deploys. Ensure that host path exists / is allowed on the Dokploy node.
- **Route to port `8080` (plain HTTP).** The panel serves HTTP on `:8080`;
  Dokploy's Traefik terminates TLS at the edge (`letsencrypt`) and proxies HTTP
  to it. If you attach the domain via Dokploy's UI instead of the labels, set the
  port to `8080` (scheme http — the default). This avoids a self-signed backend
  (no Traefik `insecureSkipVerify` needed).
- **Requires panel ≥ 1.6.0.** Traefik's docker provider will NOT route a
  container that isn't `healthy`, and panel < 1.6.0 flaps to `unhealthy` under
  the docker-proxy `ObjectDisposedException` bug — so it never gets a route.
  `latest` is fine; if you pin `PANEL_IMAGE`'s tag, use 1.6.0 or newer.
- **Stale `latest` → unhealthy.** Docker never re-pulls a `latest` it already
  has locally, so a node that pulled an older panel keeps running it (the
  pre-1.6.0 flap above → `/health` 503 → unhealthy → no Traefik route) even
  after a new `latest` is published. The compose sets `pull_policy: always` on
  the `panel` service so **every Dokploy redeploy re-pulls the current image**.
  If you already have a stuck container, just **redeploy** in Dokploy — it will
  now land the fixed image. (Verify with `docker image inspect` / the panel's
  startup log: a fixed image logs no `BootstrapListenerManager … Addresses
  cannot be modified` exception.)
- **`redirect-to-https@file`** — the panel's *customer-app* routers reference
  this middleware. Dokploy's Traefik provides HTTP→HTTPS redirection by default;
  if a customer app's HTTP router errors on a missing middleware, either define
  `redirect-to-https@file` in Dokploy's dynamic config or set the project's
  custom entrypoints so the panel skips the redirect pair.
- **Docker socket** — the panel reaches Docker only through the Tecnativa
  `dockerproxy` (verb allowlist), never the raw socket. The proxy mounts
  `/var/run/docker.sock:ro`; Dokploy permits this for Compose apps.
- **Validated on a Dokploy-equivalent harness** (Swarm + attachable
  `dokploy-network` overlay + Traefik v3.6 docker provider, matching Dokploy):
  the panel attaches to the overlay, Traefik discovers its labels, and
  `GET /api/version` + the SPA return 200 through the edge. Confirm on a real
  Dokploy node before production (Dokploy's own Traefik config + ACME for a real
  domain). Customer-app routing (the `dokploy` edge preset) needs a 1.6.0+ image
  and was not exercised in the harness.
