Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dbf3fa4
chore: update .gitignore to exclude Chati.dev runtime files
mateusmetzker Mar 18, 2026
b5792d2
feat: migrate local metrics to gopsutil v4
mateusmetzker Mar 18, 2026
32f9403
feat: extract SSH utilities into internal/ssh package
mateusmetzker Mar 18, 2026
58f00ca
feat: introduce CmdRunner abstraction and LocalRunner
mateusmetzker Mar 18, 2026
a3eb943
refactor: wire CmdRunner through Executor and Runner
mateusmetzker Mar 18, 2026
48bbeb7
feat: add TunnelMonitor with automatic reconnection and SSE status
mateusmetzker Mar 18, 2026
96b5e24
fix: resolve terminal deadlock and harden WebSocket origin check
mateusmetzker Mar 18, 2026
2bbe6b4
fix: add remote metrics cache and SSH timeout
mateusmetzker Mar 18, 2026
5833cfe
fix: harden HTTP handlers — body limits, method corrections, JSON val…
mateusmetzker Mar 18, 2026
174aee4
feat: add semver comparison and cross-device-safe updater
mateusmetzker Mar 18, 2026
95087f9
fix: client-side terminal clear, SSE POST streams, toast error details
mateusmetzker Mar 18, 2026
61b15f8
ci: add CI workflow and add tests to release workflow
mateusmetzker Mar 18, 2026
2c3f388
test: add 155 unit tests across all packages
mateusmetzker Mar 18, 2026
de7ddc6
feat: add label-based container detection with network fallback
mateusmetzker Mar 18, 2026
419bbd2
feat: adds dual donate options for Brazilian and international suppor…
mateusmetzker Mar 18, 2026
9d90895
docs: adds v0.3 changelog
mateusmetzker Mar 18, 2026
49285b6
feat: adds self-update via dashboard with SSE progress and restart
mateusmetzker Mar 18, 2026
310a018
chore: installs binary to ~/.local/bin for user-writable self-update
mateusmetzker Mar 18, 2026
d79d529
fix: adds scp recursive flag for directory sync
mateusmetzker Mar 18, 2026
3619bb8
chore: generates release body from changelog and expands README
mateusmetzker Mar 18, 2026
8934b4a
docs: adds prerequisites, v0.3 highlights, and removes phantom -open …
mateusmetzker Mar 18, 2026
57982ce
docs: updates CLI help example to use generic service name
mateusmetzker Mar 23, 2026
e56e867
docs: moves v0.3 changelog entries back to unreleased
mateusmetzker Mar 23, 2026
decaf31
chore: adds dev branch to CI workflow triggers
mateusmetzker Mar 23, 2026
71c8fbb
Merge pull request #4 from getkaze/feat/reliability-improvements
mateusmetzker Mar 23, 2026
a476c8f
docs: tags changelog entries as v0.3 release
mateusmetzker Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
push:
branches: [master, dev]
pull_request:
branches: [master, dev]

jobs:
test:
name: Test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Run tests
run: go test ./... -race -count=1
82 changes: 65 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,25 @@ permissions:
contents: write

jobs:
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Run tests
run: go test ./... -race -count=1

build:
needs: test
name: Build (${{ matrix.os }}/${{ matrix.arch }})
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -70,27 +88,57 @@ jobs:
with:
path: artifacts

- name: Create release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
body: |
## keel ${{ github.ref_name }}
- name: Build release body
run: |
VERSION="${{ github.ref_name }}"
SEMVER="${VERSION#v}"

# Extract the section for this version from CHANGELOG.md
NOTES=$(awk -v ver="$SEMVER" '
/^## \[/ {
if (found) exit
if (index($0, "[" ver "]")) { found=1; next }
}
found && /^---$/ { exit }
found { print }
' CHANGELOG.md)

# Build the release body
cat > /tmp/release-body.md << 'HEADER'
## What's new
HEADER

if [ -n "$(echo "$NOTES" | tr -d '[:space:]')" ]; then
echo "$NOTES" >> /tmp/release-body.md
else
echo "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/master/CHANGELOG.md) for details." >> /tmp/release-body.md
fi

cat >> /tmp/release-body.md << FOOTER

### Installation
---

```bash
curl -fsSL https://getkaze.dev/keel/install.sh | sudo bash
```
## Installation

### Manual download
\`\`\`bash
curl -fsSL https://getkaze.dev/keel/install.sh | sudo bash
\`\`\`

| Platform | Link |
|---|---|
| Linux amd64 | [keel-linux-amd64](${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/keel-linux-amd64) |
| Linux arm64 | [keel-linux-arm64](${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/keel-linux-arm64) |
| macOS amd64 | [keel-darwin-amd64](${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/keel-darwin-amd64) |
| macOS arm64 | [keel-darwin-arm64](${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/keel-darwin-arm64) |
## Manual download

| Platform | Link |
|---|---|
| Linux amd64 | [keel-linux-amd64](${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/keel-linux-amd64) |
| Linux arm64 | [keel-linux-arm64](${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/keel-linux-arm64) |
| macOS amd64 | [keel-darwin-amd64](${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/keel-darwin-amd64) |
| macOS arm64 | [keel-darwin-arm64](${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/keel-darwin-arm64) |
FOOTER

- name: Create release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
body_path: /tmp/release-body.md
files: |
artifacts/keel-linux-amd64/keel-linux-amd64
artifacts/keel-linux-arm64/keel-linux-arm64
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
bin/
data/

# Chati.dev runtime files (session lock — not committed)
.chati/memories/*/session/
CLAUDE.local.md
CLAUDE.md
chati.dev/
.vscode/
.claude/
.chati/
46 changes: 45 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,49 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.3] — 2026-03-23

### Added

- CmdRunner abstraction — local and remote Docker execution behind a unified interface (@mateusmetzker)
- LocalRunner and ReloadableRunner with hot-swap support for target config changes (@mateusmetzker)
- SSH utilities extracted into `internal/ssh` package, shared across all SSH consumers (@mateusmetzker)
- TunnelMonitor with automatic reconnection, exponential backoff, and health checks (@mateusmetzker)
- SSE endpoint for tunnel status (`GET /api/tunnel/status`) with live status dot in the topbar (@mateusmetzker)
- Label-based container detection (`keel.managed=true`) with network fallback for backward compatibility (@mateusmetzker)
- Semver comparison in updater — correctly handles `0.10.0 > 0.9.0` (@mateusmetzker)
- Cross-device-safe updater — temp file created in same directory as binary (@mateusmetzker)
- IP validation for `keel hosts setup` — rejects invalid addresses before modifying `/etc/hosts` (@mateusmetzker)
- Body size limits: 64 KB for service creation, 1 MB for config save (@mateusmetzker)
- CI workflow with `go test -race` on push/PR; test job added as prerequisite in release workflow (@mateusmetzker)
- 155 unit tests across all packages (@mateusmetzker)
- Dual donate options: Stripe for Brazilian supporters, Buy Me a Coffee for international (@mateusmetzker)

### Changed

- Migrated local metrics (CPU, memory, disk, load average, uptime) from manual `/proc` parsing to gopsutil v4 (@mateusmetzker)
- `start-all`, `stop-all`, and seeder run endpoints changed from GET to POST (@mateusmetzker)
- SSE error events renamed from `error` to `app-error` to avoid conflicts with `EventSource.onerror` (@mateusmetzker)
- SSE streams now support POST via `fetch + ReadableStream` for mutation endpoints (@mateusmetzker)
- Template rendering buffered — errors return clean HTTP 500 instead of partial HTML (@mateusmetzker)
- Health check handler reuses a shared `http.Client` instead of creating one per request (@mateusmetzker)
- Remote metrics cached for 10 seconds with background refresh — no more blocking SSH calls per HTTP request (@mateusmetzker)
- Log navigation uses `htmx:afterSettle` instead of `setTimeout` for reliable service pre-selection (@mateusmetzker)
- GHCR login now pipes PAT over stdin instead of shell interpolation (@mateusmetzker)
- SSH options hardened: `StrictHostKeyChecking=accept-new` replaces `StrictHostKeyChecking=no` (@mateusmetzker)
- WebSocket origin check validates same-host/localhost instead of accepting all origins (@mateusmetzker)

### Fixed

- Terminal deadlock: `Session.Close` is now idempotent via `sync.Once`; close called before `wg.Wait` (@mateusmetzker)
- Terminal ANSI clear race condition: moved from server-side PTY write to client-side `term.clear()` on WebSocket open (@mateusmetzker)
- Update toast now shows error details when pull fails, instead of always showing "UPDATE COMPLETE" (@mateusmetzker)
- Log viewer path traversal: file paths validated against configured log source directories (@mateusmetzker)
- Config editor: `saveServiceConfig` uses `io.ReadAll` + `json.Valid` instead of broken `fmt.Fscan` (@mateusmetzker)
- Destructive update prevention: failed `docker pull` no longer removes the running container (@mateusmetzker)
- Seeder card CSS selector corrected from `.seeder-card` to `.seeder-item` (@mateusmetzker)
- Executor `dockerStream` gains idle timeout and non-blocking channel sends with log-on-drop (@mateusmetzker)

---

## [0.2] — 2026-03-15
Expand Down Expand Up @@ -110,7 +153,8 @@ Initial public release (@mateusmetzker).
- Data directory: `/var/lib/keel` (Linux) or `~/.keel` (macOS)
- Install script: `curl -fsSL https://getkaze.dev/keel/install.sh | sudo bash`

[Unreleased]: https://github.com/getkaze/keel/compare/v0.2...HEAD
[Unreleased]: https://github.com/getkaze/keel/compare/v0.3...HEAD
[0.3]: https://github.com/getkaze/keel/compare/v0.2...v0.3
[0.2]: https://github.com/getkaze/keel/compare/v0.1.1...v0.2
[0.1.1]: https://github.com/getkaze/keel/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/getkaze/keel/releases/tag/v0.1.0
86 changes: 75 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@

<br/>

[Install](#install) · [Usage](#usage) · [Features](#features) · [Seeders](#seeders) · [Dev Mode](#dev-mode) · [Remote Targets](#remote-targets) · [Stack](#stack) · [Build](#build)
[Prerequisites](#prerequisites) · [Install](#install) · [Usage](#usage) · [Features](#features) · [Seeders](#seeders) · [Dev Mode](#dev-mode) · [Remote Targets](#remote-targets) · [Service Config](#service-config) · [Stack](#stack) · [Build](#build) · [Data Directory](#data-directory)

</div>

---

## What is Keel

**Keel** (the keel of a ship — the hidden structure that keeps everything aligned) is a self-hosted web dashboard for managing Docker environments — local or remote via SSH — from a single Go binary (~10MB, no external dependencies)..
**Keel** (the keel of a ship — the hidden structure that keeps everything aligned) is a self-hosted web dashboard for managing Docker environments — local or remote via SSH — from a single Go binary (~10MB, no external dependencies).

```
keel
Expand All @@ -32,13 +32,21 @@ That's it. Open `http://localhost:60000` and you have a full dashboard with live

---

## Prerequisites

- **Docker** — local install or remote host with Docker via SSH
- **SSH key pair** — required for remote targets
- **sudo** — only for `keel hosts setup` (modifies `/etc/hosts`)

---

## Install

```bash
curl -fsSL https://getkaze.dev/keel/install.sh | sudo bash
```

This installs the binary to `/usr/local/bin/keel` and creates the data directory at `/var/lib/keel`.
This installs the binary to `~/.local/bin/keel` and creates the data directory at `/var/lib/keel`. The binary is owned by your user, enabling self-update from the dashboard without sudo.

---

Expand All @@ -51,8 +59,10 @@ keel
# Container operations
keel start # start all services
keel start redis mysql # start specific services
keel start infra # start all services in a group
keel stop # stop all services
keel stop traefik # stop specific service
keel stop tools # stop all services in a group
keel reset --all # destroy and recreate all containers
keel reset redis # recreate a single service

Expand Down Expand Up @@ -156,7 +166,16 @@ Each seeder is a JSON file in `data/seeders/`:
|-------|-------------|
| `target` | Container name to exec into |
| `order` | Execution order (lower = first) |
| `commands` | Ordered list of `{ name, command }` steps |
| `commands` | Ordered list of steps (see below) |

Each command entry supports:

| Field | Description |
|-------|-------------|
| `name` | Step identifier |
| `command` | Single command to execute via `docker exec` |
| `script` | Filename of a script in the seeders directory (alternative to `command`) |
| `interpreter` | Interpreter to pipe the script into — e.g. `bash`, `python3` (used with `script`) |

Seeders can be run from the UI (Seeders page) or via CLI:

Expand Down Expand Up @@ -207,23 +226,43 @@ Example service config:

Keel supports multiple Docker targets — local or remote via SSH tunnel.

<!-- /var/lib/keel/data/targets.json -->
```json
// /var/lib/keel/data/targets.json
{
"targets": {
"local": { "host": "127.0.0.1" },
"ec2": { "host": "user@1.2.3.4", "ssh_key": "~/.ssh/id_ed25519", "external_ip": "1.2.3.4" }
}
"ec2": {
"host": "1.2.3.4",
"ssh_user": "ubuntu",
"ssh_key": "~/.ssh/id_ed25519",
"ssh_jump": "ec2-user@bastion.example.com",
"external_ip": "1.2.3.4",
"port_bind": "0.0.0.0",
"description": "AWS EC2 Ubuntu"
}
},
"default": "local"
}
```

| Field | Description |
|-------|-------------|
| `host` | IP address or hostname |
| `ssh_user` | SSH user for remote targets (omit for local) |
| `ssh_key` | Path to SSH private key (supports `~/`) |
| `ssh_jump` | Bastion/jump host for multi-hop SSH |
| `external_ip` | External IP used by `keel hosts setup` |
| `port_bind` | Bind interface for ports — `127.0.0.1` (default) or `0.0.0.0` |
| `description` | Human-readable target label |
| `default` | Root-level field — default target name |

```bash
keel target ec2 # switch to remote
keel start # commands now execute on ec2 via SSH
keel target local # switch back
```

For remote targets, an SSH tunnel is opened automatically, forwarding the remote Docker socket to a local Unix socket (`/tmp/keel-docker-<target>.sock`).
For remote targets, an SSH tunnel is opened automatically, forwarding the remote Docker socket to a local Unix socket (`/tmp/keel-docker-<target>.sock`). The tunnel is monitored with automatic reconnection and exponential backoff — a live status dot in the topbar shows the connection state via SSE.

---

Expand All @@ -237,11 +276,13 @@ Each service is a JSON file in `data/services/`. Full example:
"group": "database",
"hostname": "keel-redis",
"image": "redis:7",
"registry": "dockerhub",
"network": "keel-net",
"ports": { "internal": 6379, "external": 6379 },
"environment": { "REDIS_ARGS": "--maxmemory 256mb" },
"volumes": ["keel-redis-data:/data"],
"command": "redis-server --save 60 1",
"files": ["data/config/redis.conf:/etc/redis/redis.conf"],
"start_order": 1,
"ram_estimate_mb": 256,
"dashboard_url": "http://localhost:8001",
"health_check": {
Expand All @@ -262,6 +303,26 @@ Each service is a JSON file in `data/services/`. Full example:
}
```

| Field | Description |
|-------|-------------|
| `name` | Unique service identifier |
| `group` | Logical grouping — `infra` starts first, then seeders, then the rest |
| `hostname` | Docker container hostname |
| `image` | Docker image `name:tag` |
| `registry` | Set to `ghcr` to auto-login with stored credentials (omit for public images) |
| `network` | Docker network (defaults to `keel-net`) |
| `ports` | `{ internal, external }` port mapping |
| `environment` | Environment variables passed to the container |
| `volumes` | Volume mounts — named volumes, bind mounts, or config files |
| `command` | Override container CMD |
| `files` | Config files mounted read-only into the container; synced via `scp` on remote targets (`local:container`) |
| `start_order` | Startup priority (lower = earlier, 0 = last) |
| `ram_estimate_mb` | Display hint for the dashboard |
| `dashboard_url` | External URL — shows an **OPEN** button in the UI |
| `health_check` | HTTP or command-based health check config |
| `logs` | Log sources — `docker` or `file` with optional `host_path` |
| `dev` | Development mode config — `dockerfile`, `command`, `cap_add` |

---

## Stack
Expand All @@ -273,7 +334,7 @@ Each service is a JSON file in `data/services/`. Full example:
| Design | Kaze design system — Recursive variable font |
| Assets | `go:embed` — single binary, ~10MB |
| Icons | Lucide v0.469.0 (self-hosted SVG sprite) |
| Metrics | `/proc/stat`, `/proc/meminfo`, `syscall.Statfs`, `docker stats` |
| Metrics | gopsutil v4, `docker stats`, remote cache (10s) |

---

Expand All @@ -298,8 +359,11 @@ sudo bash install-dev.sh
# Run with live asset reloading
keel -dev

# Run tests
# Run tests (155 unit tests)
go test ./...

# Run with race detection
go test -race ./...
```

---
Expand Down
Loading
Loading