Skip to main content

Docker Deployment

Quick Start

docker run -d \
--name genkitkraft \
-p 8080:8080 \
-v genkitkraft-data:/data \
-e ENCRYPTION_KEY=$(openssl rand -base64 32) \
-e AUTH_CREDENTIALS=admin:changeme \
--restart unless-stopped \
ghcr.io/deej4y/genkitkraft:latest

For production, use Docker Compose:

services:
genkitkraft:
image: ghcr.io/deej4y/genkitkraft:latest
ports:
- "8080:8080"
volumes:
- genkitkraft-data:/data
environment:
PORT: 8080
DATABASE_PATH: /data/app.db
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
AUTH_CREDENTIALS: ${AUTH_CREDENTIALS}
restart: unless-stopped

volumes:
genkitkraft-data:

Use a .env file for secrets:

ENCRYPTION_KEY=your-generated-key-here
AUTH_CREDENTIALS=admin:strongpassword

Then: docker compose up -d

Persistent Storage

By default, GenKitKraft stores all data in a SQLite database at /data/app.db. Mount a Docker volume or bind mount to /data to persist data across container restarts.

Multi-instance deployments need two shared backends, not one:

  • A shared database — PostgreSQL, MySQL, or MariaDB via DATABASE_PROVIDER and DATABASE_URL.
  • A shared cache — Redis or Valkey via CACHE_PROVIDER and CACHE_URL. See Shared Cache below.

Both are required. A shared database alone still leaves sessions process-local, so logins will appear to fail at random as requests land on different instances.

See Environment Variables for the full reference.

PostgreSQL

services:
genkitkraft:
image: ghcr.io/deej4y/genkitkraft:latest
ports:
- "8080:8080"
environment:
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
AUTH_CREDENTIALS: ${AUTH_CREDENTIALS}
DATABASE_PROVIDER: postgres
DATABASE_URL: postgres://genkitkraft:${DB_PASSWORD}@db:5432/genkitkraft?sslmode=disable
depends_on:
db:
condition: service_healthy
restart: unless-stopped

db:
image: postgres:16-alpine
environment:
POSTGRES_USER: genkitkraft
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: genkitkraft
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U genkitkraft"]
interval: 5s
timeout: 5s
retries: 5

volumes:
db-data:

MySQL / MariaDB

Replace the db service image with mysql:8 or mariadb:11 and set DATABASE_PROVIDER accordingly:

services:
genkitkraft:
image: ghcr.io/deej4y/genkitkraft:latest
ports:
- "8080:8080"
environment:
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
AUTH_CREDENTIALS: ${AUTH_CREDENTIALS}
DATABASE_PROVIDER: mysql # or mariadb
DATABASE_URL: genkitkraft:${DB_PASSWORD}@tcp(db:3306)/genkitkraft?parseTime=true
depends_on:
db:
condition: service_healthy
restart: unless-stopped

db:
image: mysql:8
environment:
MYSQL_USER: genkitkraft
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: genkitkraft
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10

volumes:
db-data:
parseTime=true

The parseTime=true parameter is required in the MySQL/MariaDB DSN for correct timestamp handling.

Shared Cache

Session tokens and login rate-limit counters are cached in-process by default. Running more than one instance that way breaks authentication — a login handled by one instance is unknown to the others, so subsequent requests return 401.

Point every instance at one Redis or Valkey server to fix that. Add the service alongside your database:

services:
genkitkraft:
image: ghcr.io/deej4y/genkitkraft:latest
ports:
- "8080:8080"
environment:
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
AUTH_CREDENTIALS: ${AUTH_CREDENTIALS}
DATABASE_PROVIDER: postgres
DATABASE_URL: postgres://genkitkraft:${DB_PASSWORD}@db:5432/genkitkraft?sslmode=disable
CACHE_PROVIDER: valkey # or redis
CACHE_URL: valkey://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
restart: unless-stopped

cache:
image: valkey/valkey:8-alpine
command: ["valkey-server", "--maxmemory", "256mb", "--maxmemory-policy", "noeviction"]
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped

Swap the image for redis:7-alpine (and the healthcheck for redis-cli ping) to use Redis instead — set CACHE_PROVIDER: redis and a redis:// URL. The two are interchangeable.

No volume is mounted: the cache holds no durable data, and losing it only logs users out.

noeviction is deliberate. Every key here already has a TTL — 24h for sessions, 1 minute for rate-limit counters — so volatile-ttl protects nothing, and because it evicts the shortest TTLs first it would drop rate-limit counters before sessions, quietly resetting brute-force protection under memory pressure. With noeviction a full cache keeps serving reads, so existing sessions stay valid, and fails writes, so new logins return 503 rather than users being silently logged out. See CACHE_PROVIDER for sizing.

Results from the built-in web-fetch tool are not stored here — they stay in a process-local cache, so agent tool traffic cannot exhaust the cache that authentication depends on.

The connection is checked at startup, so a misconfigured CACHE_URL fails immediately rather than after traffic arrives.

Health Checks

GenKitKraft exposes health check endpoints:

EndpointDescription
GET /livezReturns 200 if the server is running
GET /readyzReturns 200 if the server is ready, 503 if not

Example Docker Compose health check:

healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/readyz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

Updating

docker compose pull
docker compose up -d

Your data is preserved in the named volume.

Building from Source

If you prefer to build the Docker image yourself:

git clone https://github.com/DEEJ4Y/genkitkraft.git
cd genkitkraft
docker build -t genkitkraft .

The Dockerfile uses a multi-stage build (Node.js for UI → Go for server → Alpine runtime).