A small continuous-deployment dashboard on top of Komodo: see the state of every stack, pull or redeploy them by hand, and trigger the same actions from CI with an API key.
Website and docs: nonetss.github.io/Komodo-CD
- Overview: the key figures of the instance at a glance: stacks by state, which ones need attention or have something new, image vulnerabilities, and the deploys of the last 30 days per day with their success rate, followed by the running and stopped stacks
- Stacks: a searchable, filterable list (running, stopped, with issues) next to the page of each stack: services, images with a new version, deployed and latest commit, and one-click Pull, Redeploy or Pull + Redeploy, also on several stacks at once
- CI snippets: a ready-to-paste, syntax-highlighted
curlper stack and action - History: every action launched from the dashboard or from CI, with who ran it and how it ended
- Security: every image used by a stack scanned with Trivy: vulnerabilities by severity, which ones have a fix, and which stacks use each image. Images are scanned when they first show up, after every deploy (including the ones Komodo makes on its own, such as auto updates), every 24 hours to catch new CVEs, and on demand; private registries are tried and reported as "no access" when the scan cannot pull them
- Failure alerts: optional ntfy notifications (ntfy.sh or self-hosted, with an optional access token) whenever a deploy fails
- API keys for pipelines, a single Komodo connection, English and Spanish UI, dark and light themes
Monorepo (Turborepo + Bun workspaces), plus the production deployment using images published on GHCR.
| Path | Stack | |
|---|---|---|
| Backend | apps/backend |
Bun + Hono (thin server: auth, oRPC, OpenAPI) |
| Frontend | apps/frontend |
Astro 7 (SSR) + React + TanStack Query |
| Gateway | apps/gateway |
Caddy: the only published port, routes to both |
| Site | apps/site |
Astro 7 (static), published to GitHub Pages |
| Trivy | trivy service in compose.yml |
Upstream aquasec/trivy server for the image scans (no workspace) |
| Package | Description |
|---|---|
@komodo-cd/api |
oRPC routers (v0), Komodo, ntfy and Trivy services |
@komodo-cd/auth |
Better Auth config + session resolver (cookie or x-api-key) |
@komodo-cd/db |
Drizzle v1 (SQLite/libsql): schema, relations, migrations, seed |
@komodo-cd/env |
Validated server env (t3-env + zod) |
@komodo-cd/logger |
Shared pino logger |
@komodo-cd/config |
Shared tsconfig |
| Stacks | Deploy |
|---|---|
![]() |
![]() |
| History | Security |
|---|---|
![]() |
![]() |
| Connection | API keys |
|---|---|
![]() |
![]() |
The screenshots use made-up stacks and URLs.
- Docker + Docker Compose v2
- Internet access to pull images
Run it in the folder where the stack should live. It asks for the host port,
public URL and initial admin, generates .env with a random
BETTER_AUTH_SECRET (mode 600), downloads compose.yml and optionally
starts the stack:
mkdir komodo-cd && cd komodo-cd
curl -fsSL https://raw.githubusercontent.com/Nonetss/Komodo-CD/main/scripts/bootstrap.sh | bashOr from a clone:
git clone https://github.com/Nonetss/Komodo-CD.git
cd Komodo-CD
./scripts/bootstrap.shPin a release with KCD_REF=v1.0.0 (defaults to main). The script never
overwrites an existing .env.
Images are published as latest (the main branch) and with the release
version (1.0.0, 1.0). To pin one, change the tags in compose.yml, for
example ghcr.io/nonetss/komodo-cd-backend:1.0.0.
# Better Auth secret (required)
openssl rand -base64 32
# Initial admin password (required)
openssl rand -base64 16cp .env.example .envEdit .env and fill at least these three variables:
APP_URL=https://your-domain.com
BETTER_AUTH_SECRET=<output of first openssl command>
SEED_ADMIN_PASSWORD=<output of second openssl command>docker compose up -dThe app will be available on the port configured in PORT (default 80).
services:
backend:
image: ghcr.io/nonetss/komodo-cd-backend:latest
init: true
restart: unless-stopped
volumes:
- db_data:/data
environment:
DATABASE_URL: "file:/data/db.sqlite"
BETTER_AUTH_URL: "${APP_URL:?APP_URL is required}"
BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET is required}"
SEED_ADMIN_EMAIL: "${SEED_ADMIN_EMAIL:-admin@example.com}"
SEED_ADMIN_NAME: "${SEED_ADMIN_NAME:-Admin}"
SEED_ADMIN_PASSWORD: "${SEED_ADMIN_PASSWORD:?SEED_ADMIN_PASSWORD is required}"
TRIVY_SERVER_URL: "${TRIVY_SERVER_URL:-http://trivy:4954}"
TRIVY_RESCAN_INTERVAL_HOURS: "${TRIVY_RESCAN_INTERVAL_HOURS:-24}"
healthcheck:
test: ["CMD", "bun", "-e", "fetch('http://localhost:3000/health-check').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
networks:
- komodo_net
frontend:
image: ghcr.io/nonetss/komodo-cd-frontend:latest
init: true
restart: unless-stopped
environment:
BACKEND_URL: "http://backend:3000"
healthcheck:
test: ["CMD", "bun", "-e", "fetch('http://localhost:4321/login').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
depends_on:
backend:
condition: service_healthy
networks:
- komodo_net
gateway:
image: ghcr.io/nonetss/komodo-cd-gateway:latest
init: true
restart: unless-stopped
ports:
- "${PORT:-80}:80"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
depends_on:
backend:
condition: service_healthy
frontend:
condition: service_healthy
networks:
- komodo_net
# Trivy server for the Security page: keeps the vulnerability database and
# receives the scans the backend sends with its `trivy` client (same
# version as TRIVY_VERSION in apps/backend/Dockerfile).
trivy:
image: aquasec/trivy:0.75.0
init: true
restart: unless-stopped
command: ["server", "--listen", "0.0.0.0:4954", "--cache-dir", "/var/lib/trivy"]
volumes:
- trivy_cache:/var/lib/trivy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4954/healthz"]
interval: 10s
timeout: 5s
retries: 5
# The first start downloads the database before listening
start_period: 120s
networks:
- komodo_net
networks:
komodo_net:
volumes:
db_data:
trivy_cache:| Variable | Required | Description |
|---|---|---|
APP_URL |
✅ | Public app URL, without trailing slash |
BETTER_AUTH_SECRET |
✅ | Secret used to sign sessions. openssl rand -base64 32 |
SEED_ADMIN_PASSWORD |
✅ | Password for admin user created on startup |
PORT |
— | Host exposed port. Default 80 |
SEED_ADMIN_EMAIL |
— | Admin email. Default admin@example.com |
SEED_ADMIN_NAME |
— | Admin name. Default Admin |
TRIVY_SERVER_URL |
— | Trivy server for image scanning. Default the trivy service (http://trivy:4954) |
TRIVY_RESCAN_INTERVAL_HOURS |
— | Hours before an image is scanned again on its own, to catch new CVEs. Default 24; 0 turns off this periodic rescan only |
The trivy service downloads its vulnerability database from the Internet on
first start (and keeps it updated), and the backend pulls the images to scan
straight from their registries, so both need outbound access. Until the
database is ready, scans fail and can be retried from the Security page.
Full example: image build and push + automatic deploy to a Komodo CD stack.
name: Build, Publish and Deploy
on:
push:
branches:
- main
- "v*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,prefix={{branch}}-,format=short
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Pull + Redeploy stack
run: |
curl -X POST ${{ secrets.KOMODO_CD_URL }}/api/v0/deploy \
-H "x-api-key: ${{ secrets.KOMODO_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"stack":"${{ vars.STACK_NAME }}","action":"pull-redeploy"}'Required repository secrets and variables:
| Key | Type | Description |
|---|---|---|
KOMODO_CD_URL |
Secret | URL of your Komodo CD instance (example: https://komodo-cd.example.com) |
KOMODO_API_KEY |
Secret | API key generated from the dashboard |
STACK_NAME |
Variable | Stack name in Komodo |
The same API key reads the Trivy results. GET /api/v0/security/images
returns every image with its stacks and its counts per severity (images never
scanned are queued by that same call), GET /api/v0/security/image?image=<ref>
returns the vulnerabilities of one image, and POST /api/v0/security/scan
queues a rescan ({"images": [...]}, or every image with an empty body).
Every successful deploy already queues a scan of that stack's images, in the
background; so does a deploy made inside Komodo (auto update, Komodo's UI, a
procedure), picked up within about five minutes. To fail a job while the stack's images have critical
vulnerabilities, wait until none is queued or scanning, then check:
for i in $(seq 1 60); do
images=$(curl -sf "$KOMODO_CD_URL/api/v0/security/images" -H "x-api-key: $KOMODO_API_KEY" \
| jq --arg s "$STACK_NAME" '[.images[] | select(.stacks | index($s))]')
echo "$images" | jq -e 'all(.status != "queued" and .status != "scanning")' > /dev/null && break
sleep 10
done
echo "$images" | jq -e 'map(.counts.critical) | add == 0'Requirements: Bun ≥ 1.4.
bun install
bun run setup:dev # writes .env with a random secret and admin password
bun run dev # Docker dev stack on http://localhost:4321 (hot reload, Ctrl+C stops)
bun run dev:down # remove its containers (-v also drops its database)
bun run dev:local # the same without Docker: backend (:3000) + frontend (:4321) with turbo watch
bun run dev:backend # only backend, without Docker
bun run dev:frontend # only frontend, without Docker
bun run dev:site # only the project website (:4322)| Script | Description |
|---|---|
bun run build |
Build all apps (turbo build) |
bun run check-types |
Type-check all apps |
bun run format |
Format with Biome |
bun run check |
Biome lint + format with autofix |
bun run db:generate |
Generate Drizzle migrations (packages/db) |
bun run db:studio |
Open Drizzle Studio (packages/db) |
bun run docker:up |
Build images from source and start the stack |
The Dockerfiles live in apps/*/Dockerfile but the build context is always the repo root:
docker build -f apps/backend/Dockerfile -t komodo-cd-backend .
docker build -f apps/frontend/Dockerfile -t komodo-cd-frontend .
docker build -f apps/gateway/Dockerfile -t komodo-cd-gateway ..github/workflows/docker-build.yml builds and pushes the three images to ghcr.io/nonetss/komodo-cd-backend, ghcr.io/nonetss/komodo-cd-frontend and ghcr.io/nonetss/komodo-cd-gateway, rebuilding only the ones whose files changed:
| Trigger | Tags |
|---|---|
Push to main |
latest, main, main-<sha> |
Tag vX.Y.Z |
X.Y.Z, X.Y |
The trivy service is not built here: it runs the upstream aquasec/trivy image, at the same version as TRIVY_VERSION in apps/backend/Dockerfile.
To publish a release, tag the commit and create the GitHub release:
git tag -a v1.0.0 -m "v1.0.0" && git push origin v1.0.0
gh release create v1.0.0 --generate-notesdocker compose pull
docker compose up -d# Show logs in real time
docker compose logs -f
# Show backend logs only
docker compose logs -f backend
# Restart one service
docker compose restart backend
# Stop everything
docker compose down
# Stop and remove DB volume (⚠️ deletes all data)
docker compose down -vSQLite DB is stored in Docker volume db_data, mounted at /data inside the backend container. Data survives restarts and image updates. The Trivy vulnerability database lives in the trivy_cache volume; it can be deleted at any time and is downloaded again.
To make a manual backup:
docker run --rm \
-v komodo_db_data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/db-backup.tar.gz -C /data .To restore:
docker run --rm \
-v komodo_db_data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/db-backup.tar.gz -C /data






