kappal
npx skills add https://github.com/sandys/kappal --skill kappal
Agent 安装分布
Skill 文档
Kappal Skill â Docker Compose CLI for Kubernetes
0. Self-Update
On the first kappal-related task in a conversation, fetch the latest version of this skill:
WebFetch: https://raw.githubusercontent.com/sandys/kappal/main/skills/kappal/SKILL.md
- If the fetch succeeds, overwrite the local
skills/kappal/SKILL.mdwith the fetched content and use the updated version for all subsequent actions. - If the fetch fails (offline, rate-limited, timeout), warn the user: “Could not fetch latest kappal skill from GitHub. Proceeding with local copy.” Then continue with the local content.
- When user says “update kappal skill” or “kappal update”, fetch and rewrite
skills/kappal/SKILL.mdunconditionally.
1. Overview
Kappal runs docker-compose.yaml on Kubernetes (K3s) without requiring any Kubernetes knowledge. Docker is the only prerequisite.
2. How to Invoke Kappal
Zero installation footprint. No aliases, no shell changes, no wrapper scripts. Invoke via full docker run command every time.
Always pull the latest image first:
docker pull ghcr.io/sandys/kappal:latest
Base command template:
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "<project-root>:/project" \
-w /project/<compose-dir> \
-e KAPPAL_HOST_DIR="<project-root>" \
--network host \
ghcr.io/sandys/kappal:latest <command>
The -v mount and -w working directory vary by scenario â see Section 3.
3. Scenario Templates
Scenario A â Simple (compose file in project root)
The compose file is at the project root (e.g., ./docker-compose.yml).
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd):/project" \
-w /project \
-e KAPPAL_HOST_DIR="$(pwd)" \
--network host \
ghcr.io/sandys/kappal:latest <command>
Scenario B â Compose in subdirectory
The compose file is in a subdirectory (e.g., deploy/docker-compose/docker-compose.yml), but all build contexts are within the project root.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd):/project" \
-w /project/deploy/docker-compose \
-e KAPPAL_HOST_DIR="$(pwd)" \
--network host \
ghcr.io/sandys/kappal:latest <command>
Project root is mounted; working directory is set to the compose file’s directory.
Scenario C â Monorepo (build contexts reference parent directories)
The compose file uses build: context: ../.. or similar parent-directory references. The mount point must be the highest ancestor that any build.context references.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "/absolute/path/to/monorepo/root:/project" \
-w /project/deploy/docker-compose \
-e KAPPAL_HOST_DIR="/absolute/path/to/monorepo/root" \
--network host \
ghcr.io/sandys/kappal:latest <command>
IMPORTANT: Always confirm the detected mount root path with the user before running.
4. Autonomous Workflow
When the user says “make this run in kappal”, “deploy this with kappal”, or similar:
Drop-in replacement policy (mandatory)
- Treat the existing
docker-composefile and env files as the source of truth. - Start with the command equivalent of what the user would run in Docker Compose (
up -d,up --build -d, etc.). - Do not rewrite the compose file, add kappal-specific services, or port shell scripts into compose unless the user explicitly asks for that transformation.
- Treat
kappal upcompatibility findings (Compatibility check: ...) as first-class diagnostics and report them before suggesting any compose edits. - If deployment fails, debug with
kappal inspect,kappal ps,kappal logs, andkappal execfirst; propose compose edits only when the same stack is not runnable as written.
Step 1: Find the compose file
Search for docker-compose.yml, docker-compose.yaml, compose.yml, compose.yaml in:
- Current working directory
- Common subdirectories:
deploy/,docker/,deploy/docker-compose/,.docker/
Step 2: Read and analyze the compose file
Parse it for:
- Services with
build:â these need the--buildflag build.contextpaths â check if any reference parent directories (..). This triggers monorepo detection (Scenario C)env_file:references â verify these files exist at the expected paths relative to the compose file- Port mappings â note which ports will be exposed
- Named volumes â note persistent data
- Writable bind mounts â note bind mounts without
read_only; kappal auto-enables init-time permission prep for these deploy.replicasâ note scaling configurationrestart: "no"â these services will run as one-shot Jobs (migrations, seeds, etc.)depends_onwith conditions â note anyservice_completed_successfully(Jobs) andservice_healthy(healthcheck-based) dependencieshealthcheck:â note services with healthchecks (translated to K8s readiness probes)profiles:â these services will be excluded from defaultup
Step 3: Detect scenario
- No
build.contextgoing above compose dir â Scenario A (if compose at root) or Scenario B (if in subdirectory) - Any
build.contextreferencing parent dirs â Scenario C (monorepo)
Construct the full docker run command using the appropriate template from Section 3.
Step 4: Confirm with user (monorepo only)
If Scenario C is detected, show the user:
- The detected monorepo root path (mount point)
- The compose directory (working directory)
- Ask for confirmation before proceeding
Step 5: Pull the image
docker pull ghcr.io/sandys/kappal:latest
Step 6: Run setup (automatic)
Run setup silently â do not ask the user about this step:
<kappal-docker-run> --setup
Step 7: Show deployment plan
Tell the user:
- Which services will start
- Which services will be built (those with
build:) - Which services will run as one-shot Jobs (
restart: "no") - Which services are excluded due to profiles
- Any dependency ordering (
depends_onwithservice_completed_successfullyorservice_healthy) - What ports will be exposed
- Any volumes that will be created
Step 8: Deploy on user approval
On approval:
- If any services have
build:contexts:<kappal-docker-run> up --build -d - If all services use pre-built images:
<kappal-docker-run> up -d - For complex stacks with many sequential Jobs (migrations, seeds): add
--timeout 600or higher
Step 9: Verify
Run <kappal-docker-run> ps and report service status to the user.
5. Command Reference
| Docker Compose Equivalent | Kappal Command | Notes |
|---|---|---|
docker compose up -d |
<kappal> up -d |
Start services detached (timeout is a warning, not fatal) |
docker compose up --build -d |
<kappal> up --build -d |
Build images + start |
| N/A | <kappal> up --timeout 600 -d |
Custom readiness timeout in seconds (default 300) |
docker compose down |
<kappal> down |
Stop services, preserve volumes |
docker compose down -v |
<kappal> down -v |
Stop + remove volumes |
docker compose ps |
<kappal> ps |
List running services |
docker compose logs <svc> |
<kappal> logs <svc> |
View logs for a service |
docker compose logs -f <svc> |
<kappal> logs --follow <svc> |
Stream logs |
docker compose exec <svc> sh |
<kappal> exec <svc> sh |
Shell into a service |
docker compose build |
<kappal> build |
Build all images |
docker compose build <svc> |
<kappal> build <svc> |
Build a specific service |
| N/A | <kappal> clean |
Remove kappal workspace + K3s for current project |
| N/A | <kappal> clean --all |
Remove ALL kappal resources system-wide |
| N/A | <kappal> eject -o tanka/ |
Export as standalone Tanka workspace |
| N/A | <kappal> inspect | Machine-readable JSON state of the entire project |
Additional Flags
| Flag | Scope | Description |
|---|---|---|
-f <path> |
Global (before command) | Specify compose file path |
-p <name> |
Global (before command) | Override project name (default: <basename>-<8-char-hash> from compose dir path) |
ps -o json |
ps | JSON output |
up --timeout 600 |
up | Readiness timeout in seconds (default 300) |
logs --tail 50 |
logs | Last N lines |
exec -it |
exec | Interactive TTY |
exec --index 2 |
exec | Target specific replica |
5a. Programmatic Inspection (kappal inspect)
kappal inspect outputs a self-documenting JSON object combining compose file service definitions with live K8s and Docker runtime state. Use it instead of ps when you need machine-readable data â ports, pod IPs, replica counts, healthcheck config, or K3s container info. If K3s is running but the API is unreachable, services are listed with status "unavailable". Services in the compose file but not deployed show status "missing". For Deployments, only Running/Pending pods are shown (historical completed/failed pods are filtered out). For Jobs, all pods are shown including Succeeded/Failed to reflect execution history.
JSON Structure
{
"_schema": { "...": "field descriptions (see below)" },
"project": "myapp",
"k3s": {
"container": "kappal-myapp-k3s",
"status": "running",
"network": "kappal-myapp-net"
},
"services": [
{
"name": "web",
"kind": "Deployment",
"image": "myapp-web:latest",
"status": "running",
"replicas": { "ready": 2, "desired": 2 },
"ports": [
{ "host": 8080, "container": 80, "protocol": "tcp" }
],
"healthcheck": {
"test": ["CMD-SHELL", "curl -f http://localhost/health"],
"interval": "10s",
"timeout": "5s",
"retries": 3,
"start_period": "30s"
},
"pods": [
{ "name": "web-abc123", "status": "Running", "ip": "10.42.0.5" },
{ "name": "web-def456", "status": "Running", "ip": "10.42.0.6" }
]
}
]
}
Field Reference (from _schema)
| Field Path | Description |
|---|---|
project |
Compose project name, derived from directory name or -p flag. Also used as the K8s namespace. |
k3s.container |
Docker container name running this project’s K3s instance (format: kappal-<project>-k3s). |
k3s.status |
K3s container state. Values: running, stopped, not found. |
k3s.network |
Docker bridge network isolating this project (format: kappal-<project>-net). |
services[].name |
Service name from docker-compose.yaml. Used as K8s Deployment/Job name and DNS hostname. |
services[].kind |
K8s workload type. Deployment for long-running services, Job for run-to-completion (restart: no). |
services[].image |
Container image. For locally-built images: <project>-<service>:latest. |
services[].status |
Aggregated health. Deployment: running, waiting, partial. Job: completed, running, failing, failed, pending. Other: missing (in compose but not in K8s), unavailable (K8s API unreachable). |
services[].replicas.ready |
Number of pods running and passing readiness checks. |
services[].replicas.desired |
Target replica count from deploy.replicas (default 1). |
services[].ports[].host |
Port number on the Docker host. Use for external access (curl, browser). |
services[].ports[].container |
Target port for the K8s Service and container (the compose target value). Kappal sets both the K8s Service port and targetPort to this value. |
services[].ports[].protocol |
Transport protocol: tcp or udp. |
services[].healthcheck |
Compose healthcheck definition, mapped to a K8s readiness probe. Only present if the service defines a healthcheck. |
services[].healthcheck.test |
Healthcheck command. Format: ["CMD-SHELL", "command"] or ["CMD", "arg1", ...]. |
services[].healthcheck.interval |
Time between probe attempts (e.g. 10s). Maps to K8s readinessProbe.periodSeconds. |
services[].healthcheck.timeout |
Max time for a single probe (e.g. 5s). Maps to K8s readinessProbe.timeoutSeconds. |
services[].healthcheck.retries |
Consecutive failures before marking unhealthy. Maps to K8s readinessProbe.failureThreshold. |
services[].healthcheck.start_period |
Grace period before probes count (e.g. 30s). Maps to K8s readinessProbe.initialDelaySeconds. |
services[].pods[].name |
K8s pod name (auto-generated with random suffix). |
services[].pods[].status |
K8s pod phase. Deployment pods: Running, Pending. Job pods: Running, Pending, Succeeded, Failed, Unknown. |
services[].pods[].ip |
Pod’s cluster-internal IP on the K3s overlay network. |
Common jq Recipes
# Get host port for a specific service
<kappal> inspect | jq '.services[] | select(.name=="web") | .ports[0].host'
# Check if all services are running
<kappal> inspect | jq '[.services[] | .status] | all(. == "running")'
# List pod IPs for a service
<kappal> inspect | jq '.services[] | select(.name=="api") | .pods[].ip'
# Find services that are not fully ready
<kappal> inspect | jq '.services[] | select(.status != "running" and .status != "completed")'
# Get K3s status
<kappal> inspect | jq '.k3s.status'
When to Use inspect vs ps
| Need | Use |
|---|---|
| Quick human-readable status check | kappal ps |
| Machine-readable JSON for scripting | kappal inspect |
| Get published host ports | kappal inspect (only source of port data in JSON) |
| Check pod IPs or replica counts | kappal inspect |
| Verify K3s container is running | kappal inspect |
Integration Pattern
Use inspect to dynamically resolve ports before making HTTP requests:
PORT=$(<kappal> inspect | jq '.services[] | select(.name=="web") | .ports[0].host')
curl http://localhost:$PORT/health
6. Compose Feature Support
Fully Supported
services, image, build (context + dockerfile + args), ports (TCP/UDP), volumes (named + bind), environment, env_file, secrets, configs, networks, command, entrypoint, deploy.replicas, labels, restart, depends_on (including service_completed_successfully and service_healthy), healthchecks (mapped to K8s readiness probes), profiles, one-shot services (Jobs)
Key Behaviors
restart: "no"â Services with this setting run as Kubernetes Jobs instead of Deployments. They execute once and stop cleanly (no CrashLoopBackOff). Use for migrations, seeds, setup tasks.depends_onwithcondition: service_completed_successfullyâ Kappal injects an init container that waits for the dependency Job to complete before starting the dependent service. This works for both Job-to-Job and Job-to-Deployment dependencies.depends_onwithcondition: service_healthyâ Kappal injects an init container that waits for the dependency’s pod to reachReadystatus (healthcheck passing). The dependency service must define ahealthcheck.healthcheckâ Compose healthcheck definitions are translated to K8s readiness probes (exec-based). BothCMD-SHELLandCMDformats are supported.interval,timeout,retries, andstart_periodmap to K8s probe parameters.- Compatibility checker on
upâ Kappal analyzes active services before deploy and printsCompatibility check: ...notes for high-signal Compose/K8s mismatch risks. - Writable bind mounts â For writable bind mounts, Kappal injects init-time path preparation so non-root workloads can write without compose-side chmod helper services.
- Failed Job pods â When K8s retries a failed Job, old failed pods don’t block readiness. Only the latest attempt’s status matters.
- Detach mode timeout â When
-dis used, readiness timeout is a warning (exit 0), not a fatal error. Use--timeout <seconds>to adjust for complex stacks with sequential job chains. profilesâ Services withprofiles:are excluded fromkappal upby default, matching Docker Compose behavior. Profile activation is not yet supported.
Not Supported
extends, resource limits (mem/cpu), log drivers, profile activation (--profile)
7. Build Args vs Runtime Environment (Critical)
These are separate concerns â never confuse them:
| Directive | When It Applies | Purpose |
|---|---|---|
build.args: |
Build time only | Passed as --build-arg during docker build |
environment: |
Runtime only | Injected when the container starts |
env_file: |
Runtime only | Loaded into environment: at container start |
NEVER pass runtime environment variables as build args. If a build fails because it needs a runtime variable like DATABASE_URL, the fix belongs in the app’s Dockerfile or configuration â not in kappal.
8. Project Naming
Kappal derives the project name from the compose file’s directory: <sanitised-basename>-<8-char-sha256-hash>. This makes names worktree-safe â two directories with the same basename (e.g. git worktrees, multiple clones) get different project names and never share K3s containers or networks. When running directly on the host, symlinks to the same physical directory produce the same name. In Docker wrapper mode, KAPPAL_HOST_DIR should be a resolved path (see Pitfall #6).
Override with -p <name> when you need a fixed name (e.g. scripting, CI).
Important for AI agents: Do not hard-code project names derived from directory basenames alone. Always use kappal inspect | jq '.project' to discover the actual project name at runtime.
9. Common Pitfalls
-
Monorepo mount path â If any
build.contextgoes above the compose directory, the-vmount must start from the highest needed ancestor. Getting this wrong causes “file not found” during builds. -
depends_on conditions â Kappal supports both
service_completed_successfully(Job dependencies) andservice_healthy(healthcheck-based readiness). The dependency service must define ahealthcheckforservice_healthyto work. -
YAML anchors â
x-*extension fields and YAML anchors work fine. They are parsed by compose-go. -
Volume persistence â
kappal downpreserves volume data. Onlykappal down -vremoves volumes. This is the expected behavior â don’t use-vunless the user wants a clean slate. -
Port conflicts â Kappal uses
--network host, so published ports bind directly to the host. If a port is already in use, the service will fail to start. Check withss -tlnporlsof -i :<port>before deploying. -
KAPPAL_HOST_DIRenv var â Required when running kappal viadocker run. It tells kappal the real host path of the project directory so the project name is derived from the host path (not the container’s/project). Without it, all projects would get the same name. Always include-e KAPPAL_HOST_DIR="<project-root>"in docker run commands. Important:KAPPAL_HOST_DIRshould be a resolved (non-symlinked) path. Symlink resolution only works when kappal runs directly on the host; inside Docker, the caller must pass the canonical path. -
Duplicate port/protocol â If a compose file maps the same container port and protocol twice (e.g. two services both expose
80/tcp), kappal will return an error instead of silently overwriting. -
Premature compose patching â For third-party projects, do not edit compose files before trying the drop-in path. Run
up, capture compatibility notes, and inspect runtime state first.