> ## Documentation Index
> Fetch the complete documentation index at: https://docs.celum.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Deploy Celum with Docker Compose: the dashboard, API, and database behind your reverse proxy.

Celum ships as two container images — the dashboard (`app`) and the API (`backend`) — alongside a PostgreSQL database. This guide deploys all three with Docker Compose and connects your first supervisor.

## Prerequisites

<CardGroup cols={2}>
  <Card title="Docker + Compose" icon="docker">
    A host with Docker Engine and the Compose plugin.
  </Card>

  <Card title="Registry access" icon="box">
    Credentials for the registry that hosts the Celum images.
  </Card>

  <Card title="OIDC application" icon="key">
    A client registration with your identity provider (client ID, secret, issuer).
  </Card>

  <Card title="TLS + DNS" icon="lock">
    Two hostnames (app + API) terminating TLS at a reverse proxy.
  </Card>
</CardGroup>

You also need at least one **kubeconfig** for each Cluster API supervisor you want to manage.

<Warning>
  Celum sets the session cookie with the `Secure` flag, so authentication only works over **HTTPS**. Run Celum behind a TLS-terminating reverse proxy and use `https://` URLs throughout — plain HTTP will fail to log in.
</Warning>

<Note>
  Angle-bracket values below are placeholders — `<tenant-id>`, `<version>`, `<registry>`, and so on. Substitute the values for your deployment; nothing here is a working default.
</Note>

## 1. Configure environment

Create a `.env` file next to your `docker-compose.yml`. Compose interpolates these values into the stack.

```bash .env theme={null}
# --- Public URLs (HTTPS, served by your reverse proxy) ---
FRONTEND_URL=https://app.example.com
BACKEND_PUBLIC_URL=https://api.example.com
OIDC_REDIRECT_URL=https://api.example.com/auth/callback
COOKIE_DOMAIN=example.com

# --- OIDC ---
OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
OIDC_CLIENT_ID=<client-id>
OIDC_CLIENT_SECRET=<client-secret>
# Which token claim maps to IAM groups: "groups" or "roles"
OIDC_GROUPS_CLAIM=groups

# --- Sessions & authorization ---
# Generate a strong random key, e.g. `openssl rand -base64 48`
SESSION_SECRET=<random-secret>
IAM_ENABLED=true

# --- Database ---
POSTGRES_PASSWORD=<db-password>
```

<Note>
  Register `OIDC_REDIRECT_URL` (`https://<your-api-host>/auth/callback`) as a redirect URI on your OIDC application.
</Note>

Every variable the backend reads is listed in [Environment variables](/reference/environment-variables).

## 2. Add supervisor kubeconfigs

Place one kubeconfig per supervisor in a `kubeconfigs/` directory next to the compose file. **The filename becomes the supervisor name** shown in the UI.

```text theme={null}
kubeconfigs/
  <supervisor-a>        # → supervisor "<supervisor-a>"
  <supervisor-b>        # → supervisor "<supervisor-b>"
```

## 3. Define the stack

```yaml docker-compose.yml theme={null}
services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: k8sgate
      POSTGRES_USER: k8sgate
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

  backend:
    image: <registry>/vks-manager/backend:<version>
    depends_on:
      - db
    environment:
      PORT: "8080"
      DATABASE_URL: postgres://k8sgate:${POSTGRES_PASSWORD}@db:5432/k8sgate?sslmode=disable
      KUBECONFIGS_DIR: /app/kubeconfigs
      OIDC_ISSUER: ${OIDC_ISSUER}
      OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
      OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
      OIDC_REDIRECT_URL: ${OIDC_REDIRECT_URL}
      OIDC_GROUPS_CLAIM: ${OIDC_GROUPS_CLAIM}
      SESSION_SECRET: ${SESSION_SECRET}
      FRONTEND_URL: ${FRONTEND_URL}
      COOKIE_DOMAIN: ${COOKIE_DOMAIN}
      IAM_ENABLED: ${IAM_ENABLED}
    volumes:
      - ./kubeconfigs:/app/kubeconfigs:ro
    restart: unless-stopped

  app:
    image: <registry>/vks-manager/app:<version>
    depends_on:
      - backend
    environment:
      BACKEND_URL: http://backend:8080
      BACKEND_PUBLIC_URL: ${BACKEND_PUBLIC_URL}
    restart: unless-stopped

volumes:
  pgdata:
```

Pin the image tag to the version you were provided. Route your reverse proxy so that `FRONTEND_URL` reaches the `app` service on port `3000` and `BACKEND_PUBLIC_URL` reaches the `backend` service on port `8080`.

## 4. Start the stack

```bash theme={null}
docker compose pull
docker compose up -d
```

The backend applies database migrations automatically on startup. Once the reverse proxy is routing both hostnames, open your dashboard URL:

```text theme={null}
https://app.example.com
```

## What happens on first load

<Steps>
  <Step title="Sign in">
    Unauthenticated requests are redirected to sign in through your OIDC provider. On return, the backend issues a signed session cookie.
  </Step>

  <Step title="Default supervisor">
    The root route resolves the first supervisor you can access and redirects to `/s/{supervisor}/`. If you can access none, you land on `/no-access`.
  </Step>

  <Step title="Grant access">
    With `IAM_ENABLED=true`, access is governed by IAM. A fresh database ships an `Administrators` group carrying the `K8sGateAdmin` policy, mapped to the OIDC claim value `krn:vks:admin` — assign that claim to yourself, or edit the group to match a claim you already have. See [Authentication & IAM](/get-started/authentication).
  </Step>
</Steps>

## What commonly goes wrong

| Symptom                                                                | Cause                                                                                                                                |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Login loops back to the sign-in page                                   | The stack is served over plain HTTP. The session cookie is `Secure`, so the browser never stores it.                                 |
| `403` on every page after signing in                                   | `IAM_ENABLED=true` and your OIDC claim matches no group. Nothing in IAM grants you anything, so evaluation ends in the default deny. |
| Everyone is logged out after a restart                                 | `SESSION_SECRET` was left unset, so the backend generated a random one at startup and the old cookies no longer verify.              |
| The supervisor list is empty                                           | No file in `KUBECONFIGS_DIR` and no kubeconfig uploaded to the database.                                                             |
| Backend refuses to start, naming `GITLAB_TOKEN` or `GITLAB_PROJECT_ID` | `GITLAB_URL` is set, which makes both of those mandatory.                                                                            |

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication & IAM" icon="key" href="/get-started/authentication">
    Configure OIDC mapping and policies in depth.
  </Card>

  <Card title="Supervisors & clusters" icon="layer-group" href="/concepts/supervisors-and-clusters">
    How Celum discovers supervisors and what it reads from them.
  </Card>

  <Card title="Create a cluster" icon="circle-plus" href="/clusters/create">
    Provision a tenant cluster from a template.
  </Card>

  <Card title="Environment variables" icon="sliders" href="/reference/environment-variables">
    Every variable the backend and dashboard read.
  </Card>
</CardGroup>
