DOKPLOY_DEPLOY.md
Rama main · réplica de lectura del repositorio original.
# Deploy Kanera with Dokploy
This guide walks through deploying Kanera as a Docker Compose application in
Dokploy.
## Requirements
- A Dokploy server
- A git repository containing Kanera
- DNS records pointing your domains at the Dokploy server
- An SMTP account if you want default outbound email
Recommended domains:
- `kanera.example.com` for the web app
- `api.kanera.example.com` for the public integration API
- `mcp.kanera.example.com` for the remote MCP server, if you want AI agents to connect over Streamable HTTP
- `admin.kanera.example.com` for the optional management portal
## 1. Create the project
1. In Dokploy, create a new project.
2. Add a new application.
3. Choose Docker Compose as the application type.
4. Connect the Kanera git repository.
5. Set the compose file path to:
```text
docker-compose.yml
```
The compose file defines `postgres`, `valkey`, `api`, `worker`, `public-api`,
`mcp`, and `web`.
## 2. Add environment variables
In the Dokploy application's environment tab, add:
```bash
WEB_ORIGIN=https://kanera.example.com
COOKIE_DOMAIN=kanera.example.com
COOKIE_SECURE=true
KANERA_ENVIRONMENT=production
API_TRUST_PROXY=true
PUBLIC_API_TRUST_PROXY=true
API_REPLICAS=2
PUBLIC_API_REPLICAS=1
MCP_REPLICAS=1
REALTIME_WEBSOCKET_COMPRESSION_ENABLED=false
REALTIME_WEBSOCKET_COMPRESSION_THRESHOLD_BYTES=1024
POSTGRES_PASSWORD=<openssl rand -hex 32>
JWT_SECRET=<openssl rand -hex 32>
MCP_INTERNAL_SECRET=<openssl rand -hex 32>
MFA_ENCRYPTION_KEY=<openssl rand -hex 32>
MEDIA_SIGNING_SECRET=<openssl rand -hex 32>
SECRETS_ENCRYPTION_KEY=<openssl rand -hex 32>
KANERA_ALLOW_PRIVATE_NOTIFICATION_DESTINATIONS=false
```
Realtime compression is disabled by default to avoid compression work and delivery queues for
Kanera's typically small messages. Set `REALTIME_WEBSOCKET_COMPRESSION_ENABLED=true` to opt in when
bandwidth is constrained and large updates are common. When enabled, messages above the 1024-byte
threshold use fast deflate (level 1) without server context takeover. These settings apply to both
hosted and self-hosted deployments. Existing deployments explicitly set to `true` remain opted in;
change that setting to `false` and recreate the API containers to adopt the new default.
Compose forwards the analytics configuration to the app API, public API and worker. Recreate all three services after changing it. Consented first-touch acquisition is stored privately in `user_acquisition`; only allow-listed campaign dimensions, never advertising click IDs, accompany PostHog events. Apply migrations before deploying the API.
Product analytics remains disabled for ordinary self-hosted Dokploy installations. A
Kanera-operated hosted deployment may opt in with the four `ANALYTICS_*`/`POSTHOG_*` variables
documented in `DEPLOY.md`; use separate PostHog EU projects for staging and production. Browser and
server-side capture use the same public project token and ingestion host within each deployment.
Keep `POSTGRES_PASSWORD` URL-safe: Compose also inserts it into each service's internal database URL.
`API_TRUST_PROXY=true` is required for the app API when browser traffic reaches
Node through Dokploy/Traefik or another trusted non-Cloudflare proxy. It lets
auth rate limits use the real client IP instead of treating every user as the
proxy.
`PUBLIC_API_TRUST_PROXY=true` does the same for the public integration API when
you expose it through a Dokploy domain, so public API rate limits use the real
client IP.
For GitHub private repository link previews, register a GitHub App for this
deployment (see the GitHub App section below) and add the `GITHUB_APP_*`
variables. Leave them unset to instead bootstrap an App in-app via Settings →
Organisation after deploy. **Redeploy after saving any env var changes** — Dokploy
does not hot-reload running containers.
`API_PUBLIC_URL` defaults to `WEB_ORIGIN`. Set it only if signed media URLs
should use a different public origin:
```bash
API_PUBLIC_URL=https://media.kanera.example.com
```
The MCP service talks to the public API over the private Docker network by
default. Keep this internal URL unless you are running MCP outside the compose
network:
```bash
KANERA_PUBLIC_API_URL=http://public-api:3001
```
If you expose MCP publicly, set its browser/client-visible endpoint:
```bash
MCP_SERVER_PUBLIC_URL=https://mcp.kanera.example.com/mcp
PUBLIC_API_OAUTH_ISSUER=https://api.kanera.example.com
MCP_PUBLIC_URL=https://mcp.kanera.example.com/mcp
MCP_TRUST_PROXY=true
MCP_UPSTREAM_TIMEOUT_MS=15000
MCP_TOOL_OUTPUT_MAX_BYTES=1048576
```
For OpenAI plugin domain verification, set `OPENAI_APPS_CHALLENGE_TOKEN` to the
token supplied in the submission portal and redeploy the MCP service. Route
`/.well-known/openai-apps-challenge` on the MCP hostname to that service. The
endpoint returns the token as public plain text, without sign-in; leave the
variable unset on deployments that do not need verification.
Route both `/.well-known/oauth-protected-resource` and the path-specific
`/.well-known/oauth-protected-resource/mcp` to the MCP service as well as `/mcp`.
The public API domain must expose `/.well-known/oauth-authorization-server`
and `/oauth/*` so compatible clients can open Kanera's browser consent flow.
The MCP listener also enforces the body-size, rate, and HTTP timeout controls documented in
`.env.full.example`; keep those limits enabled even when Dokploy's proxy has its own limits.
Hosted mode is reserved for Kanera-operated deployments and is unavailable in
self-host installations.
Optional SMTP defaults:
```bash
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_USER=kanera@example.com
SMTP_PASSWORD=your-password
SMTP_FROM_EMAIL=kanera@example.com
SMTP_FROM_NAME=Kanera
# Optional EHLO/Message-ID domain; defaults to SMTP_FROM_EMAIL's domain.
SMTP_IDENTITY_DOMAIN=example.com
# Optional: comma-separated internal recipients for signup, invite-acceptance, and paid-invoice alerts.
INTERNAL_NOTIFICATION_EMAILS=ops@example.com,founder@example.com
# Optional (hosted): where lifecycle check-in emails invite customers to send feedback (default support@kanera.app).
LIFECYCLE_FEEDBACK_EMAIL=support@kanera.app
# Cross-tenant support access is started from the management portal (a superadmin opens an org and runs
# POST /admin/orgs/:clientId/support-session). SUPPORT_SESSION_TTL_MINUTES tunes the minted token
# lifetime (default 60; no refresh companion). The admin server signs this tenant token, so it also needs
# JWT_SECRET and WEB_ORIGIN. See "Management portal (optional)" below for deploying the portal itself.
SUPPORT_SESSION_TTL_MINUTES=60
# Optional: close public self-signup/new org creation while still allowing
# existing organisation invite links.
SIGNUPS_ENABLED=true
# Optional: require mailbox verification for signup, invite signup, and email changes.
# Leave false until SMTP is confirmed working.
EMAIL_VERIFICATION_ENABLED=false
```
Optional operational alerts for unhandled API errors, billing reconciliation failures,
and failed or stuck organisation deletions. A single Slack-compatible incoming webhook —
Slack, Zulip (`slack_incoming`), Mattermost, and Discord all work. Grafana reuses it
for its alerts too:
```bash
ALERT_WEBHOOK_URL=https://hooks.slack.com/services/YOUR_WEBHOOK_PATH
# or a Zulip slack_incoming webhook:
# ALERT_WEBHOOK_URL=https://zulip.example.com/api/v1/external/slack_incoming?api_key=XXXXXXXXXXXXXXXX&stream=alerts&topic=Kanera
OPS_ALERTS_ENABLED=true
OPS_ALERT_THROTTLE_MS=300000
SLOW_REQUEST_LOG_MS=2500
```
Kanera sends one success message only after Compose reports all core services healthy. Configure
Dokploy's deployment-failure notification for the same destination so build, migration, and failed
health-check rollouts produce one error; application runtime errors are sent by Kanera itself.
Keep these values stable across redeploys. Changing `JWT_SECRET` signs users out.
Changing `MEDIA_SIGNING_SECRET` invalidates existing signed media URLs.
## 3. Deploy once
Start the first deploy from Dokploy.
After it finishes, check that the services exist and are running:
- `postgres`
- `valkey`
- `api`
- `worker`
- `public-api`
- `mcp`
- `web`
Dokploy needs this first deploy before the compose services are available in the
domain service selector.
Keep the `worker` service replica/instance count at exactly `1`; it owns all
schedulers, webhook delivery, presence crash reaping, and realtime outbox
fallback fanout. The `api` service is safe to scale vertically across cores
because Socket.IO fanout, presence, and rate limits use Valkey.
Set `API_REPLICAS` in Dokploy's environment tab to the number of app API
processes you want. The compose file uses that value directly, so Dokploy
redeploys, reloads, and container restarts keep the desired API count without a
manual post-deploy `docker compose --scale` command. If unset, it defaults to
`2`. `PUBLIC_API_REPLICAS` and `MCP_REPLICAS` work the same way for those
services and default to `1`.
The `public-api` and `mcp` services have separate scaling considerations.
## 4. Configure domains
Create a domain for the `web` service:
| Setting | Value |
|---|---|
| Domain | `kanera.example.com` |
| Service | `web` |
| Container port | `80` |
| HTTPS | Enabled |
Create a second domain for the `public-api` service if you want the integration
API:
| Setting | Value |
|---|---|
| Domain | `api.kanera.example.com` |
| Service | `public-api` |
| Container port | `3001` |
| HTTPS | Enabled |
Create a third domain for the `mcp` service if you want remote MCP clients to
connect over Streamable HTTP:
| Setting | Value |
|---|---|
| Domain | `mcp.kanera.example.com` |
| Service | `mcp` |
| Container port | `3002` |
| HTTPS | Enabled |
Do not create a public domain for the `api` service. The `web` nginx service
proxies `/api/*` (including `/api/media/*`) and `/socket.io/*` to it over the
private Docker network.
After domains are configured, check:
- `https://kanera.example.com/api/health` returns a healthy response.
- `https://api.kanera.example.com/health` returns a healthy response if the
public API domain is configured.
- `https://mcp.kanera.example.com/health` returns a healthy response if the MCP
domain is configured.
## 5. Configure persistence
The compose file creates two Docker volumes:
| Volume | Stores |
|---|---|
| `kanera_pgdata` | PostgreSQL data |
| `kanera_uploads` | Uploaded files |
Make sure both volumes are backed up in Dokploy. If you use a managed Postgres
database, set `DATABASE_URL` for the `api`, `worker`, and `public-api` services,
set `DATABASE_SSL=true` if your provider requires SSL, and remove the bundled
`postgres` service from your compose setup. Valkey is also a hard dependency; if
you replace the bundled Valkey service with a managed Valkey or Redis-compatible
instance, set `REDIS_URL` for `api`, `worker`, and `public-api`.
The bundled Postgres service defaults to `work_mem=16MB`, configurable with
`POSTGRES_WORK_MEM` in Dokploy's Compose environment. Deploy the updated Compose
file so Dokploy recreates **postgres**, not just the API services. This applies
to existing data volumes and briefly restarts the database. After deployment,
verify with `docker compose exec -T postgres psql -U kanera -d kanera -c 'SHOW work_mem;'`.
Set `POSTGRES_WORK_MEM=4MB` and redeploy Postgres to roll back. The limit is per
sort/hash operation, so concurrent operations can multiply memory use. Managed
Postgres requires changing the default through the provider instead.
The bundled Postgres service only publishes to host loopback. For temporary remote access, tunnel
the loopback port over SSH (including over WireGuard) instead of exposing Postgres on an interface:
```bash
ssh -L 5433:127.0.0.1:5433 user@server
```
To use S3-compatible upload storage instead of the local `kanera_uploads`
volume, set these variables in Dokploy before deploying:
| Variable | Required | Notes |
|---|---:|---|
| `S3_REGION` | Yes | Use your provider's region, or `auto` where supported. |
| `S3_BUCKET` | Yes | Bucket used for Kanera uploads. |
| `S3_ACCESS_KEY_ID` | Yes | Access key with object read/write/delete permissions. |
| `S3_SECRET_ACCESS_KEY` | Yes | Secret key for the access key. |
| `S3_ENDPOINT` | No | Required for S3-compatible providers outside AWS. |
| `S3_PUBLIC_URL_PREFIX` | No | Optional CDN/public object URL prefix. |
When the required S3 values are configured, S3 takes precedence over
organisation-level storage settings and local disk storage.
You can also enable automated full Postgres backups to S3. The backup worker
creates a full dump three times per day, compresses it with gzip, encrypts it
with GPG symmetric AES-256, uploads it to S3, and keeps a rolling 14-day window:
```bash
DB_BACKUPS_ENABLED=true
DB_BACKUP_ENCRYPTION_PASSPHRASE=<openssl rand -hex 32>
DB_BACKUP_TIMES_UTC=00:15,12:15,16:45
DB_BACKUP_RETENTION_DAYS=14
DB_BACKUP_S3_PREFIX=backups/postgres
```
The backup service reuses the deployment-wide `S3_*` variables by default. Use
`DB_BACKUP_S3_BUCKET`, `DB_BACKUP_S3_REGION`, `DB_BACKUP_S3_ENDPOINT`,
`DB_BACKUP_S3_ACCESS_KEY_ID`, and `DB_BACKUP_S3_SECRET_ACCESS_KEY` only when
database backups should go to a separate bucket or credential set. Keep
`DB_BACKUP_ENCRYPTION_PASSPHRASE` somewhere durable outside Dokploy as well; it
is required to restore the `.sql.gz.gpg` files.
### Optional: GitHub App for link previews
To render private GitHub repository, pull request, and commit previews, register a
GitHub App for this deployment (Setup URL `${WEB_ORIGIN}/settings/org`, read
access to contents, metadata, and pull requests) and set the variables below.
Leave them unset to instead bootstrap an App from Settings > Organisation.
| Variable | Required | Notes |
|---|---:|---|
| `GITHUB_APP_ID` | No | GitHub App id. |
| `GITHUB_APP_SLUG` | No | GitHub App slug from its install URL. |
| `GITHUB_APP_CLIENT_ID` | Hosted | App OAuth client id. Enable "Request user authorization (OAuth) during installation" on the App and set its Callback URL to `${WEB_ORIGIN}/settings/org`; Kanera uses the resulting authorization to confirm the connecting user can see the installation. |
| `GITHUB_APP_CLIENT_SECRET` | Hosted | An OAuth client secret for the App. Required alongside `GITHUB_APP_CLIENT_ID`. |
| `GITHUB_APP_WEBHOOK_SECRET` | No | The App's webhook secret. Set the App's Webhook URL to `${API_PUBLIC_URL}/api/integrations/github/webhook` and subscribe it to "Pull request" events so pull requests naming a card key link to that card. |
| `GITHUB_APP_PRIVATE_KEY` | No | App private key PEM with newlines escaped as `\n`. Paste as a single line — Dokploy's env editor reads one value per line, so a raw multi-line PEM will be silently truncated. Run `awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' your-key.pem` to get the correctly escaped form. |
| `TRELLO_API_KEY` | No | Trello app API key for authenticated Trello attachment imports. See `trello_import.md`. |
## 6. Monitoring (optional)
Every app process registers a Prometheus `/metrics` endpoint (HTTP latency by route,
database query latency, Postgres pool saturation, Node runtime metrics). It returns
`404` unless a bearer token is configured:
```bash
METRICS_TOKEN=<openssl rand -hex 32>
```
Prometheus refuses to start without this token when the monitoring profile is enabled.
To run the bundled self-hosted observability stack (Prometheus + Grafana + Loki +
Alloy + `postgres-exporter` + `cadvisor` + `node-exporter` + `redis-exporter`),
add to the Dokploy environment tab and redeploy:
```bash
COMPOSE_PROFILES=monitoring
GRAFANA_ADMIN_PASSWORD=<choose a strong password>
METRICS_TOKEN=<openssl rand -hex 32>
ALERT_WEBHOOK_URL=<Slack-compatible incoming webhook>
# Must match the Compose project name Dokploy assigned to this application.
# Dokploy generates a name like `kanera-app-ggcqgp` (appName + random suffix) — NOT
# the bare `kanera` you might expect. Alloy uses this to filter Docker log discovery
# to containers in this deployment; wrong value = no logs in Loki.
# Discover the real value by running `docker compose ls` on the server — use the
# project name shown on the row with your app containers (running 10 or similar).
# The suffix is stable across redeploys; it only changes if you delete and recreate
# the Dokploy application, so re-verify with `docker compose ls` after recreation.
COMPOSE_PROJECT_NAME=kanera-app-ggcqgp # replace with actual value from `docker compose ls`
# Reach the dashboards over the internal WireGuard VPN (recommended, see below).
MONITORING_BIND_IP=172.30.0.102
# Optional:
PROMETHEUS_RETENTION=30d
PROMETHEUS_RETENTION_SIZE=5GB
GRAFANA_ROOT_URL=http://172.30.0.102:3000
```
These services are gated behind the `monitoring` Compose profile, so they only
start when `COMPOSE_PROFILES=monitoring` is set. Budget roughly **0.7–1.5 GB
RAM** for the stack. Prometheus discovers and scrapes every `api`/`public-api`
replica automatically via Docker DNS, and also scrapes the monitoring stack
itself (Grafana, Loki, Alloy, exporters) so its own health is alertable.
The app replica alert compares healthy targets with `API_REPLICAS`, `PUBLIC_API_REPLICAS`,
and one worker, and fires after a shortfall lasts three minutes. Temporary extra targets during a
deployment do not raise the expected count. Change replica counts through these Compose variables
(rather than an ad-hoc `--scale`) so monitoring and the deployment agree.
Grafana packages its provisioning files and bundled dashboards in its image rather than binding
them from the checkout. Dokploy replaces checkout directories on deployment; an unchanged
container would otherwise keep deleted directories mounted. Rebuild Grafana when changing
its provisioning files or dashboard JSON. Its database remains in the persistent data volume.
**Disk safety.** Prometheus is capped by `PROMETHEUS_RETENTION_SIZE`. Loki's
filesystem store has **no** hard size cap — retention deletes by age, so a burst can
fill the disk before compaction
([Grafana docs](https://grafana.com/docs/loki/latest/configure/storage/)). The Loki
config applies ingestion rate limits and frequent compaction, and a host
disk-space alert is the backstop, but for high volume put the monitoring volumes on
a dedicated data disk (or Loki on object storage). The provisioned alerts cover
target-down, low disk, Prometheus reload/TSDB failures, Loki ingestion errors
(5xx), rejected or dropped log entries (4xx), and emails that failed permanently.
### Reach Grafana + Prometheus over WireGuard (recommended)
Rather than expose dashboards on a public domain, publish Grafana and Prometheus
only on the server's WireGuard (`wg0`) interface. Set `MONITORING_BIND_IP` in the
Dokploy environment tab to the server's `wg0` address:
```bash
MONITORING_BIND_IP=172.30.0.102 # the server's wg0 address
# Optional overrides if these host ports are taken:
GRAFANA_BIND_PORT=3000
PROMETHEUS_BIND_PORT=9090
```
After redeploy, browse from a machine on the VPN to:
- `http://172.30.0.102:3000` — Grafana
- `http://172.30.0.102:9090` — Prometheus (targets, ad-hoc queries)
Until `MONITORING_BIND_IP` is set, both bind to `127.0.0.1` (loopback only).
**Never set `MONITORING_BIND_IP=0.0.0.0` in production**, and pair WireGuard access
with a host firewall rule that allows ports `3000`/`9090` only on `wg0`. Loki,
Alloy, and all exporters stay internal-only (no published ports) and are reached
through Grafana.
If you would rather use a public HTTPS domain instead of the VPN, leave
`MONITORING_BIND_IP` unset and create a Dokploy domain for the `grafana` service
(container port `3000`, HTTPS enabled). In that case add a Traefik basic-auth
middleware on top of the Grafana admin password, and do **not** create domains for
`prometheus`, `loki`, `alloy`, or any exporter.
Grafana ships with provisioned datasources, a "Kanera — App (RED + DB)" dashboard,
and starter alert rules. Alerts reuse the same `ALERT_WEBHOOK_URL` as the app (no
separate config), delivered via Grafana's Slack integration — which Slack and every
Slack-compatible endpoint (Zulip's `slack_incoming`, Mattermost, ...) accept.
Import community dashboards by ID for more depth (Node Exporter
Full `1860`, PostgreSQL `9628`, cAdvisor). Logs from all containers are searchable
in Grafana → Explore (Loki) by `requestId`, `userId`, route, level, and status.
`pg_stat_statements` is preloaded on the bundled Postgres. The `postgres` volume
is already initialised, so the `CREATE EXTENSION` in `init.sql` will not re-run —
create it once after the redeploy that adds the preload:
```bash
docker compose exec -T postgres psql -U kanera -d kanera \
-c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
```
`postgres-exporter` connects as `kanera_exporter`, a dedicated role that holds
only `pg_monitor` instead of the application role's full access. The
`postgres-exporter-init` service provisions that role on every monitoring
deploy, so no manual SQL is needed; set `POSTGRES_EXPORTER_PASSWORD` to give it
a credential separate from `POSTGRES_PASSWORD`.
If you use a managed Postgres instead of the bundled service, set
`POSTGRES_EXPORTER_DSN` to its connection string, ensure `pg_stat_statements` is
enabled there, and grant that role `pg_monitor` (or equivalent read-all-stats
rights) yourself — the init service only provisions the bundled database.
## 7. Enable auto deploys
In the application's deployment settings, leave **Trigger Type** set to the
default **On Push**. After that, pushing to the selected branch rebuilds and
redeploys Kanera automatically.
## Management portal (optional)
The management portal (`apps/admin-web` + the `admin-api` process) is a separate, cross-tenant admin
console for platform staff — org/user administration, ops visibility, and support access. It is bundled
in the API image but disabled by default. To enable it, add this in Dokploy's environment tab and
redeploy:
```bash
COMPOSE_PROFILES=admin
```
Before the first admin deploy, add these environment variables alongside the
values from step 2:
```bash
ADMIN_JWT_SECRET=<openssl rand -hex 32> # must differ from JWT_SECRET
ADMIN_WEB_ORIGIN=https://admin.kanera.example.com
# Optional: set when the admin hostname is outside COOKIE_DOMAIN, e.g.
# COOKIE_DOMAIN=kanera.example.com and ADMIN_WEB_ORIGIN=https://kanera-admin.example.com.
ADMIN_COOKIE_DOMAIN=
```
`ADMIN_TRUST_PROXY` defaults to `true` in Compose so the management portal can
run behind Dokploy/Traefik. Set it explicitly only if you are running the
admin-api without a trusted reverse proxy.
After redeploying with the admin profile enabled, create a Dokploy domain for
the `admin-api` service:
| Setting | Value |
|---|---|
| Domain | `admin.kanera.example.com` |
| Service | `admin-api` |
| Container port | `3002` |
| HTTPS | Enabled |
This does not conflict with the MCP service also using container port `3002`:
each service listens inside its own container, and Dokploy/Traefik routes by
domain to the selected service. It only conflicts if you publish both services
to the same host port manually.
The `admin-api` service serves both the management API and the bundled
`apps/admin-web` frontend, so do not create a separate domain for `admin-web`.
After the domain is configured, check
`https://admin.kanera.example.com/health` returns a healthy response.
**First superadmin.** There is no manual insert step — on every boot the admin server seeds exactly one
`superadmin` from `ADMIN_EMAIL`/`ADMIN_PASSWORD` if the `admin_user` table is still empty, and is a
permanent no-op afterward regardless of what those vars still hold. The worker also sends its weekly
system recap to `ADMIN_EMAIL` every Monday at 07:00 UTC:
```bash
ADMIN_EMAIL=ops@example.com
ADMIN_PASSWORD=<choose a strong password>
```
Set both before the admin-api's first boot. If `admin_user` is empty and these are unset, the console
boots with zero accounts rather than failing to start. After the first superadmin exists, invite
further admins from inside the console's Admins page instead of through env vars.
The superadmin-only **Demo data** page creates and hard-resets the bundled paid demo tenants. The
provided Compose service already shares the tenant upload volume and S3 settings with `admin-api`, so
seeded avatars and attachments work and the old storage namespaces can be purged before recreation.
The generated demo password is rotated and shown once after every reset.
## Updates
For normal updates, push or pull the latest code and redeploy the application in
Dokploy. Dokploy rebuilds the changed services from `docker-compose.yml`.
A Dokploy redeploy runs `docker compose up -d`, which recreates any service whose
image digest has moved. Because every third-party image is pinned to an exact patch
version, that cannot happen as a side effect of shipping application code — Postgres,
Valkey, and the monitoring stack only change when a commit changes the pinned tag.
Merge the weekly Dependabot image PRs to pick up upstream security patches; the next
redeploy then applies them, and reverting the commit rolls the version back. Database
contents live in the `kanera_pgdata` volume, so recreating the Postgres container on a
minor upgrade does not lose data.
## Troubleshooting
- If login works briefly and then users are signed out, confirm
`COOKIE_SECURE=true`, `COOKIE_DOMAIN`, and `WEB_ORIGIN` match the public HTTPS
domain.
- If admin login works until refresh/reload, confirm the admin host is covered
by the admin refresh cookie domain. For sibling hostnames like
`COOKIE_DOMAIN=kanera.example.com` and `ADMIN_WEB_ORIGIN=https://kanera-admin.example.com`,
set `ADMIN_COOKIE_DOMAIN=kanera-admin.example.com`.
- If `/api/health` fails on the web domain, confirm the domain routes to the
`web` service on port `80`, not directly to `api`.
- If the public integration API does not respond, confirm its domain routes to
`public-api` on port `3001`.
- If the MCP endpoint does not respond, confirm its domain routes to `mcp` on
port `3002`, `MCP_SERVER_PUBLIC_URL` points at the public `/mcp` URL, and
`KANERA_PUBLIC_API_URL` is `http://public-api:3001` inside compose.
- If a migration is missing, confirm the generated SQL was committed under
`apps/api/drizzle/`, then rebuild and redeploy the `api` service.