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:
- Point your own reverse proxy or tunnel — a tunnel like seikan, or anything else — at kimbap's address, or
- Turn on kimbap's built-in Traefik instead — see "Running with Traefik" below.
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
- Point your domain's DNS record at this server.
- Run:
kimbap configure --set KIMBAP_ADMIN_DOMAIN=admin.example.com \ --set KIMBAP_TRAEFIK_ENABLED=true \ --set KIMBAP_ACME_EMAIL=you@example.com - 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.
- 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:
- Project containers are Docker containers. Traefik's Docker
provider (
exposedByDefault: false) auto-discovers them by label — standard, well-established Traefik usage. - kimbap itself is not a container. It's deliberately a native binary/systemd process (see Overview for why — it needs to be able to install Docker on a host where Docker doesn't exist yet). Traefik's Docker provider has nothing to discover here, so kimbap's own UI is routed via Traefik's file provider instead — a static route pointing at kimbap's host process.
Provisioning
From the System page (or provision_traefik via MCP/API, admin-only,
confirmation-gated), kimbap:
- Creates the
kimbap-publicDocker network, if it doesn't exist. - Renders
traefik.yml(static config: entrypoints, providers, ACME resolver) fromKIMBAP_ACME_EMAIL/KIMBAP_ACME_STAGING. - Renders
dynamic/kimbap-ui.yml(the file-provider route for kimbap's own UI) fromKIMBAP_ADMIN_DOMAINand the port kimbap listens on. - Creates an empty
acme.json(mode0600) if one doesn't exist yet. - Writes Traefik's own
docker-compose.ymland runsdocker 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
KIMBAP_ACME_EMAIL— required for real certificate issuance.KIMBAP_ACME_STAGING=true— use Let's Encrypt's staging directory while testing, to avoid hitting production rate limits.- The HTTP-01 challenge runs on the
webentrypoint (port 80), which also handles the plain-HTTP → HTTPS redirect.
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.