Documentation

Running a server·Updated 13 Oct 2026

Installing

Installing Kribble on a Linux server or with Docker Compose, putting it behind a TLS proxy, signing in for the first time and updating

You can install Kribble directly on a Linux host with systemd, or run it with Docker Compose. Both use the same two binaries and the same config.toml. Overview explains what each part does.

Before you start

You need:

  • A Linux x86-64 host. The release tarball has linux-amd64 binaries only.

  • A DNS name for the server (this page uses kribble.example.com) and a TLS certificate for it. Kribble has to be served over HTTPS, because its session cookies are marked Secure. Over plain HTTP, sign-in appears to work, but you are signed out again on the next page load.

  • Somewhere to keep files: a directory on the host, or an S3-compatible bucket pair (Cloudflare R2, AWS S3, MinIO, Backblaze B2…). See Configuration under [storage].

  • For PDF export, Chromium or Google Chrome on the host that runs kribble-web.

The release tarball

pnpm build:release in the repository builds dist/kribble-linux-amd64.tar.gz. The Release GitHub workflow builds the same tarball and attaches it to a draft release. It contains:

File

What it is

kribble-web-linux-amd64

The web server, with the web app embedded

kribble-builder-linux-amd64

The site builder and clean-up jobs

kribble-www-linux-amd64

The optional downloads site (see The CLI and desktop downloads)

config.toml.example

The configuration template

install.sh

The bare-metal installer for kribble-web and kribble-builder

install-www.sh

The installer for kribble-www

docker-compose.yml

The Docker Compose stack

DEPLOY.md

The deployment notes from the repository

downloads/

The kribble CLI for every platform, the editor extensions and, when added, the desktop apps

Install on a Linux host

These steps use Debian or Ubuntu package names. Run them as root or with sudo.

Install PostgreSQL and create the database

Install PostgreSQL 16 or newer and create a user and database for Kribble:

sudo apt install postgresql
sudo -u postgres psql -c "CREATE USER kribble WITH PASSWORD '<db-password>';"
sudo -u postgres createdb kribble -O kribble

Kribble creates and updates its tables itself when it starts.

Install Meilisearch

Install Meilisearch v1.12 or newer and run it as a service, listening on localhost only. Generate a master key with openssl rand -hex 32 and use it in place of <meili-master-key>. You also put it in config.toml later.

curl -L https://install.meilisearch.com | sh
sudo mv ./meilisearch /usr/local/bin/
sudo useradd --system --shell /usr/sbin/nologin --create-home --home-dir /var/lib/meilisearch meilisearch
sudo mkdir -p /var/lib/meilisearch/data
sudo chown -R meilisearch:meilisearch /var/lib/meilisearch

sudo tee /etc/systemd/system/meilisearch.service > /dev/null <<'EOF'
[Unit]
Description=Meilisearch
After=network.target

[Service]
User=meilisearch
Group=meilisearch
WorkingDirectory=/var/lib/meilisearch
ExecStart=/usr/local/bin/meilisearch --db-path /var/lib/meilisearch/data --env production --master-key <meili-master-key> --http-addr 127.0.0.1:7700
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now meilisearch
curl http://127.0.0.1:7700/health   # {"status":"available"}
Optional: install Redis and Chromium

Redis is only needed if you will run more than one kribble-web process. Keep it on localhost:

sudo apt install redis-server
sudo sed -i 's/^bind .*/bind 127.0.0.1 -::1/' /etc/redis/redis.conf
sudo systemctl restart redis-server
redis-cli ping   # PONG

For PDF export, install Chromium (Debian: chromium) or Google Chrome. Kribble finds chromium, chromium-browser, google-chrome and google-chrome-stable on PATH or in /usr/bin. Anything else needs [pdfgen].chrome_path.

The fonts PDFs are set in (Inter, JetBrains Mono for code, and Fraunces and IBM Plex Mono for templates made in the PDF designer) are built into Kribble. Chromium uses the server's own fonts only for characters those lack, such as Chinese or Japanese, and for the designer's Serif (system) and Sans (system). Install a font package such as fonts-noto if those print as empty boxes.

Run the installer

Copy the tarball to the server, unpack it and run install.sh:

tar xzf kribble-linux-amd64.tar.gz
sudo bash install.sh

The installer:

  • creates the system user kribble with home /opt/kribble

  • copies kribble-web and kribble-builder to /opt/kribble/

  • replaces /opt/kribble/downloads/ with the tarball's downloads/

  • creates /opt/kribble/config.toml from config.toml.example if it doesn't exist yet, owned by kribble with mode 600

  • writes /etc/systemd/system/kribble-web.service and kribble-builder.service if they don't exist yet. Existing units are left alone.

  • starts both services only if config.toml already has a postgres:// DSN

INSTALL_DIR changes the install location (default /opt/kribble). KRIBBLE_WEB_BIN and KRIBBLE_BUILDER_BIN point the installer at binaries in another place. KRIBBLE_PORT and BUILDER_PORT set the ports in a new config.toml; after that, change [server].port and [builder].port there.

Edit config.toml

Open /opt/kribble/config.toml and set at least the following. Generate each secret with openssl rand -hex 32.

[server]
frontend_url = "https://kribble.example.com"   # used in emails and site links

[security]
jwt_secret     = "<64 hex characters>"
refresh_secret = "<another 64 hex characters>"

[database]
dsn = "postgres://kribble:<db-password>@localhost:5432/kribble?sslmode=disable"

[meili]
host = "http://127.0.0.1:7700"
key  = "<meili-master-key>"

[storage]
backend = "disk"
dir     = "/opt/kribble/data"

[storage.s3]
# Also used by the disk backend: where public sites load their assets from.
public_bucket_url = "https://kribble.example.com/files-public"

For S3 storage, set backend = "s3" and fill in [storage.s3] instead. Configuration lists every setting.

Start the services
sudo systemctl enable --now kribble-web kribble-builder
systemctl status kribble-web kribble-builder
curl -i http://127.0.0.1:8080/healthz    # 200
curl -i http://127.0.0.1:8081/healthz    # 200, the builder

If a service doesn't start, journalctl -u kribble-web -n 50 shows the reason. See Troubleshooting.

Put the reverse proxy in front

Configure nginx or Caddy as described under Reverse proxy below, then open https://kribble.example.com.

The systemd units

install.sh writes both units with the same settings:

[Service]
Type=simple
User=kribble
Group=kribble
WorkingDirectory=/opt/kribble
ExecStart=/opt/kribble/kribble-web
Restart=on-failure
RestartSec=5
Environment=GIN_MODE=release
Environment=KRIBBLE_CONFIG_PATH=/opt/kribble/config.toml
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/kribble
PrivateTmp=true

ProtectSystem=strict makes the whole file system read-only for Kribble except /opt/kribble, and ProtectHome=true hides /home, /root and /run/user. So:

  • Keep the disk storage directory under /opt/kribble (for example /opt/kribble/data), or allow its path explicitly.

  • A [[publish_targets.local]] directory such as /var/www/docs must be allowed in both units: the builder writes to it, and kribble-web empties it when a site is unpublished.

  • Don't keep SSH keys for publish targets under /home. /etc/kribble/keys works, because files there stay readable.

To allow another path, add a drop-in to each unit:

sudo systemctl edit kribble-builder
[Service]
ReadWritePaths=/var/www/docs

Repeat for kribble-web, then run sudo systemctl restart kribble-web kribble-builder. The same drop-in mechanism sets environment variables such as LOG_LEVEL (see Configuration).

Install with Docker Compose

docker-compose.yml runs five containers from two images: kribble-web and kribble-builder (both from the image kribble:latest), kribble-db (PostgreSQL 16), kribble-search (Meilisearch v1.12) and kribble-redis (Redis 7, used only if [redis].url is set).

Build the image

The release does not include a Docker image. Build it from a checkout of the repository, at the commit you want to run:

docker build -f deploy/Dockerfile -t kribble:latest --build-arg VERSION=$(git describe --tags --always) .

The image contains kribble-web, kribble-builder and the kribble CLI for every platform, in /app/downloads. It does not contain the editor extensions or the desktop apps.

To move the image to a server without building it there:

docker save kribble:latest | gzip > kribble-docker.tar.gz
# on the server:
docker load < kribble-docker.tar.gz
Create the compose directory

On the server, put docker-compose.yml (from the tarball or deploy/) and config.toml.example in a directory, for example /opt/kribble-docker, and create config.toml from the example:

cp config.toml.example config.toml
chmod 600 config.toml
Create .env

Docker Compose reads .env next to docker-compose.yml to fill in the database password, the Meilisearch key and the published port. Kribble itself never reads this file, so the same values go into config.toml in the next step.

cat > .env <<'EOF'
POSTGRES_USER=kribble
POSTGRES_PASSWORD=<db-password>
POSTGRES_DB=kribble
MEILI_KEY=<meili-master-key>
KRIBBLE_PORT=8080
EOF
chmod 600 .env

KRIBBLE_PORT is the host port. Inside the container, Kribble always listens on 8080, so leave [server].port at its default.

Edit config.toml

Inside the Compose network, the services are reached by their service names:

[server]
frontend_url = "https://kribble.example.com"

[security]
jwt_secret     = "<64 hex characters>"
refresh_secret = "<another 64 hex characters>"

[database]
dsn = "postgres://kribble:<db-password>@db:5432/kribble?sslmode=disable"

[meili]
host = "http://meilisearch:7700"
key  = "<meili-master-key>"

[storage]
backend = "disk"
dir     = "/app/data"

[storage.s3]
public_bucket_url = "https://kribble.example.com/files-public"

# Only if you run several kribble-web replicas:
# [redis]
# url = "redis://redis:6379/0"
Start the stack
docker compose up -d
docker compose ps
docker compose logs -f kribble-web

All services have restart: unless-stopped, so they come back after a reboot as long as Docker starts at boot.

The image includes Chromium for PDF export (CHROME_BIN is set), so [pdfgen] needs no setting.

The Compose file keeps data in named volumes: kribble_files (uploads and published sites, mounted at /app/data in both Kribble containers), postgres_data (the database), meili_data (the search index) and redis_data (cache snapshots, safe to delete).

Reverse proxy

Put a reverse proxy with TLS in front of kribble-web. It needs to:

  • Terminate TLS. Outside dev mode, Kribble's cookies are Secure.

  • Pass the host and scheme. Kribble writes the server's address into the CLI install scripts and the JetBrains plugin repository, using the Host (or X-Forwarded-Host) and X-Forwarded-Proto headers.

  • Pass the client's address in X-Forwarded-For. Rate limits are per client IP: 10 requests a minute on the sign-in endpoints, with an increasing delay after failed logins. Kribble believes the header only from a trusted proxy: one on the same machine or a private network, unless you list others in trusted_proxies (Configuration). Without the header, every user shares the proxy's address and sign-ins start failing with 429 Too Many Requests.

  • Allow large uploads. Attachments can be up to 500 MB.

  • Allow long requests. PDF export can take up to 90 seconds, and Kribble's own read and write timeouts are 5 minutes.

There are no WebSockets and no sticky sessions to configure. /mcp uses ordinary HTTP requests.

server {
    listen 443 ssl;
    server_name kribble.example.com;

    ssl_certificate     /etc/letsencrypt/live/kribble.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/kribble.example.com/privkey.pem;

    client_max_body_size 500m;
    proxy_read_timeout   300s;
    proxy_send_timeout   300s;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header X-Real-IP         $remote_addr;
    }
}

server {
    listen 80;
    server_name kribble.example.com;
    return 301 https://$host$request_uri;
}
kribble.example.com {
    reverse_proxy 127.0.0.1:8080
}

Caddy gets the certificate, redirects HTTP to HTTPS and sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host itself. It has no request size limit or proxy timeout by default.

Behind Cloudflare, use the SSL mode "Full (strict)". The proxy then sees Cloudflare's addresses, so restore the visitor's address before it reaches Kribble (for nginx, the real_ip module with Cloudflare's address ranges and real_ip_header CF-Connecting-IP). Otherwise the rate limits apply per Cloudflare edge address.

First sign-in and the first admin

An instance admin can open Instance admin in the user menu (/admin). There are three ways to get the first one:

  1. The first account. If [admin].emails is empty and there is no instance admin yet, the next account created becomes one. That applies to a self-registered account and to an account created by an LDAP login. Open https://kribble.example.com, register, and you are the admin.

  2. [admin].emails. Every listed email is made an instance admin each time kribble-web or kribble-builder starts. This only works for accounts that already exist, so register first, then add your email and restart: sudo systemctl restart kribble-web. Listing any email turns off the first-account rule.

  3. Another admin promotes you in Instance admin → Users.

If email delivery is configured (see Authentication), registration sends a verification link, and the account is only created when you open it. Without email, the account is created immediately.

With [auth].registration = "invite_only", nobody can self-register, and invitations need an existing account. On a fresh install without LDAP, register the first account with registration = "open", then switch to invite_only and restart.

Updating

Back up first (Backups and upgrades). Database migrations run automatically when the new binaries start.

Unpack the new tarball and run the installer again:

tar xzf kribble-linux-amd64.tar.gz
sudo bash install.sh

It stops kribble-builder and kribble-web, keeps the previous binaries as kribble-web.bak and kribble-builder.bak, installs the new binaries and downloads/, and starts both services again. It doesn't change config.toml or the systemd units.

Installs from before the web/builder split (a single kribble.service) are converted: the old unit is removed and the old binary is kept as /opt/kribble/kribble.bak.

Build or load the new image, then recreate the containers:

docker build -f deploy/Dockerfile -t kribble:latest --build-arg VERSION=$(git describe --tags --always) .
# or: docker load < kribble-docker.tar.gz
docker compose up -d

Compose recreates the containers whose image changed. The data volumes are kept.

Useful commands

systemctl status kribble-web kribble-builder
journalctl -u kribble-web -f          # web server log
journalctl -u kribble-builder -f      # builder log
sudo systemctl restart kribble-web kribble-builder
curl -s http://127.0.0.1:8080/api/v1/config   # version and registration mode
docker compose ps
docker compose logs -f kribble-web
docker compose logs -f kribble-builder
docker compose restart kribble-web kribble-builder
docker compose down                   # stop everything; volumes are kept
curl -s http://127.0.0.1:8080/api/v1/config

GET /api/v1/config returns the server's version and registration mode without signing in.