Domains & TLS

By default kimbap just runs on this host, on its own address — reach it directly, or put your own reverse proxy or tunnel in front of it. If you'd rather kimbap handle a real public domain and HTTPS certificate for you automatically, turn on its built-in Traefik instead.

Running without Traefik (the default)

Out of the box, kimbap doesn't manage any reverse proxy — it just listens on KIMBAP_ADDR (127.0.0.1:<port> by default, so only reachable from this host). To make it reachable from outside, either:

KIMBAP_ADMIN_DOMAIN still matters in this mode even without Traefik — it's the hostname your own reverse proxy/tunnel forwards to kimbap, used for the OAuth issuer and secure-cookie scoping.

Projects can't be routed to a domain in this mode either, since there's no reverse proxy for kimbap to configure — route those through your own proxy too. (Details on exactly what that restricts: API reference.)

See Configuration for every other setting kimbap has.

Running with Traefik

Turn this on if you'd rather kimbap handle HTTPS for you — one instance fronts both kimbap's own web UI and any domains you route to your projects, with certificates issued and renewed automatically.

Give kimbap a public domain

  1. Point your domain's DNS record at this server.
  2. Run:
    kimbap configure --set KIMBAP_ADMIN_DOMAIN=admin.example.com \
      --set KIMBAP_TRAEFIK_ENABLED=true \
      --set KIMBAP_ACME_EMAIL=you@example.com
    
  3. kimbap restarts. If the domain isn't pointing here yet, it'll say so and refuse to start rather than come up broken — fix the DNS record and restart again.
  4. On the System page, install Docker if you haven't already, then click Provision Traefik.

That's it — kimbap is now reachable at your domain over HTTPS, and any domain you add to a project afterward gets the same treatment.

(Setting this up at install time instead of afterward? kimbap install takes the same settings as flags — see Install.)

Why this needs two different routing mechanisms

Traefik discovers what to route in two ways here, because kimbap and its projects aren't the same kind of thing:

Provisioning

From the System page (or provision_traefik via MCP/API, admin-only, confirmation-gated), kimbap:

  1. Creates the kimbap-public Docker network, if it doesn't exist.
  2. Renders traefik.yml (static config: entrypoints, providers, ACME resolver) from KIMBAP_ACME_EMAIL/KIMBAP_ACME_STAGING.
  3. Renders dynamic/kimbap-ui.yml (the file-provider route for kimbap's own UI) from KIMBAP_ADMIN_DOMAIN and the port kimbap listens on.
  4. Creates an empty acme.json (mode 0600) if one doesn't exist yet.
  5. Writes Traefik's own docker-compose.yml and runs docker compose up -d.

Re-running provisioning (e.g. after changing the ACME email or admin domain) is idempotent — safe to do any time, including as part of every kimbap boot to reconcile drift.

How kimbap's own UI gets routed

Traefik's container gets extra_hosts: host.docker.internal:host-gateway, and the file-provider route points at http://host.docker.internal:<kimbap's port>:

http:
  routers:
    kimbap-admin:
      rule: "Host(`admin.example.com`)"
      entryPoints: ["websecure"]
      service: kimbap-admin
      tls:
        certResolver: letsencrypt
  services:
    kimbap-admin:
      loadBalancer:
        servers:
          - url: "http://host.docker.internal:8080"

Because providers.file.watch: true, editing the admin domain and re-provisioning hot-reloads this route with no Traefik restart — useful if you ever want to change kimbap's own domain without dropping in-flight project traffic.

How project domains get routed

Adding a domain to a project (UI, POST /api/projects/{slug}/domains, or the add_domain MCP tool) does not edit your docker-compose.yml. Instead, kimbap maintains a separate, auto-generated docker-compose.kimbap-labels.yml override per project, regenerated on every domain change:

services:
  web:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp-web-app-example-com.rule=Host(`app.example.com`)"
      - "traefik.http.routers.myapp-web-app-example-com.entrypoints=websecure"
      - "traefik.http.routers.myapp-web-app-example-com.tls.certresolver=letsencrypt"
      - "traefik.http.services.myapp-web-app-example-com.loadbalancer.server.port=80"
    networks:
      - kimbap-public
networks:
  kimbap-public:
    external: true

kimbap deploys with both files layered (-f docker-compose.yml -f docker-compose.kimbap-labels.yml) whenever the label file exists and Traefik is enabled, and attaches the routed service to the shared kimbap-public network — Traefik's Docker provider needs real L3 connectivity to a container, not just its labels, to route to it. (See "Running without Traefik" above: the label file is never layered in once Traefik is disabled, even if one still exists on disk.)

If a project is currently deployed when you add/remove a domain, kimbap re-runs docker compose up -d immediately so the label change takes effect — Traefik's Docker provider then picks it up live via Docker events, no Traefik restart needed.

Why a separate file instead of editing your compose.yml directly? Label injection is churny (every domain add/remove touches it), and mixing kimbap-owned and user-owned content in one file risks clobbering hand-edits or generating diff noise if you track the file in git yourself. A pure compose-native multi-file merge avoids all of that, and makes "detach kimbap from this project" as simple as deleting one generated file.

Network isolation as a side effect

Only services with at least one domain get attached to kimbap-public — everything else in a project (a database, an internal cache, ...) stays only on the project's own default compose network, so giving a service a public domain is also the thing that makes it reachable from outside the project at all. A domain-routed service stays on both networks — its own project's default network (so it can still reach its own sibling services, e.g. its own database or cache by service name) and kimbap-public (so Traefik can reach it). Losing the first one silently breaks a project's internal connectivity while looking otherwise fine, so kimbap is explicit about listing both in the generated label override rather than relying on compose to merge them for you.

Let's Encrypt / ACME

Static-config changes (ACME email/staging, entrypoints) require a Traefik restart to take effect — re-provisioning's docker compose up -d handles that automatically. Domain/label changes are hot-reloaded via the file/Docker providers, as described above — no restart.

What you can only verify on a real host

Real Let's Encrypt certificate issuance requires a publicly resolvable domain with an A record pointing at your server, reachable on port 80 for the HTTP-01 challenge. Local testing (e.g. with *.localtest.me, which resolves to 127.0.0.1) validates the entire routing/label mechanism end-to-end, but certificate issuance itself will fail unless your host is actually reachable from the public internet on that domain — which is expected, not a bug.