This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Dockflow is a CLI-first deployment framework for Docker Swarm. A single TypeScript binary handles building, deploying, and managing Docker Swarm stacks via direct SSH — no runtime dependencies beyond the binary itself. Ansible is only used for one-shot machine provisioning (dockflow setup).
Stack at a glance:
cli/— TypeScript CLI (Bun runtime) + embedded Angular WebUI — handles all deploy logic via ssh2ansible/— Ansible roles for machine provisioning only (configure_host.yml)docs/— Next.js 15 + Nextra documentation sitepackages/— MCP server, npm CLI wrappertesting/e2e/— End-to-end tests using Docker-in-Docker Swarm
cli/ # TypeScript CLI application (Bun)
cli/ui/ # Angular 21 WebUI (PrimeNG + Tailwind)
ansible/ # Ansible roles for machine provisioning (setup only)
docs/ # Next.js 15 + Nextra documentation site
packages/ # Additional packages (MCP server, npm CLI wrapper)
scripts/ # Build & version management scripts
testing/e2e/ # End-to-end tests (run from WSL/CI only)
bun install # Install dependencies
bun run typecheck # TypeScript validation (tsc --noEmit)
bun run dev <command> [args] # Run CLI locally in dev mode
bun run build # Build all platform binaries
bun run build:linux # Build Linux x64 binary only
bun run build:windows # Build Windows x64 binary only
bun run ui:build # Build Angular UI (cd ui && pnpm build)pnpm install # Install dependencies
pnpm build # Production build
pnpm start # Dev serverpnpm install # Install dependencies
pnpm dev # Next.js dev server
pnpm build # Production build + LLM text generation + Pagefind indexing./scripts/lint-shell.sh # ShellCheck on all .sh filescd testing/e2e && bun test tests/ # Full test suite (spins up Docker Swarm in containers)
cd testing/e2e && bun run teardown.ts # Cleanup test VMsReleases are fully automated via CI. Push a git tag to trigger a build, GitHub Release, and npm publish:
git tag 2.1.0 && git push origin 2.1.0The CI sets the version in all package.json files from the tag before building.
All CLI operations (deploy, build, backup, logs, exec, shell, status) connect directly to remote nodes via the ssh2 library. Connection credentials come from .env.dockflow (or CI secrets). There is one SSH context — the CLI's machine must be able to reach all target hosts.
Ansible is only used for dockflow setup (one-shot machine provisioning via configure_host.yml). It runs inside a Docker container for that command only.
Commands live in cli/src/commands/ and follow this structure:
- Export a
register<Name>Command(program: Command)function - Use Commander.js
.command(),.option(),.description(),.action(withErrorHandler(...)) - Commands throw errors — never call
process.exit()directly - The
withErrorHandler()wrapper (fromutils/errors.ts) catches, formats, and exits
Entry point cli/src/index.ts registers all commands and sets up the --verbose flag via a global preAction hook.
Custom error classes in cli/src/utils/errors.ts:
CLIError (base — has code, suggestion, cause)
├─ ConfigError (codes 10-19)
├─ ConnectionError (codes 30-39)
├─ DockerError
├─ DeployError
├─ ValidationError
└─ BackupError
Always throw these typed errors from commands. The withErrorHandler wrapper displays the message + suggestion and exits with the error code. Stack traces only show in DEBUG/CI or for unexpected errors.
cli/src/services/ contains type-safe service classes for Docker Swarm operations:
Read/monitoring services:
StackService— stack lifecycle (getServices, exists, scale, etc.). Also holdsfindContainerForService()which searches all Swarm nodes in parallel viaPromise.any().ExecService— remote command execution in containersLogsService— log streamingMetricsService— container stats (read) +MetricsWriteService(write deployment metrics)BackupService— backup/restore for accessories
Deploy services (replace former Ansible roles):
ComposeService— template rendering (Nunjucks), YAML load/serialize, Swarm deploy config injection, Traefik label injection, image tag updatesSwarmDeployService— creates external networks/volumes, deploys stacks viadocker stack deploy -c -, waits for convergence, deploys accessories with hash-based change detectionBuildService— local/remote Docker image builds. Parses compose YAML to extract build targets, assembles tar contexts in memoryDistributionService— image distribution to Swarm nodes (base64 chunked transfer over SSH), registry login/pushHealthCheckService— Swarm internal health checks (auto-rollback detection) + HTTP endpoint checks with retryReleaseService— release directory management, rollback, cleanup of old releasesHookService— pre/post build/deploy hooks (local via Bun.spawn, remote via SSH)LockService— deployment lock management (acquire/release with stale detection)AuditService— deployment audit log entries on remote managerHistorySyncService— replicates audit/metrics to non-manager nodes
Services use the Result<T, E> type pattern (ok() / err()) from cli/src/types/.
Multi-node awareness: Services that need to find or operate on containers (BackupService, ExecService) accept an allConnections: SSHKeyConnection[] parameter alongside the manager connection. This is required because in a multi-node Swarm, a container may run on any worker — not just the manager. Always pass getAllNodeConnections(env) when creating these services.
All CLI output goes through cli/src/utils/output.ts helpers. Never use console.log directly.
Key helpers: printSuccess, printError, printWarning, printInfo, printDebug (verbose-only), printDim, printBlank, printJSON, printRaw, printHeader, printSection, printTableRow.
Formatters: formatDuration(seconds), formatBytes(bytes), formatRelativeTime(iso).
Verbose mode controlled by setVerbose() / isVerbose().
- Zod schemas:
cli/src/schemas/config.schema.ts— runtime validation of.dockflow/config.yml - TypeScript interfaces:
cli/src/utils/config.ts—DockflowConfig,ServersConfig, etc. - Both must stay in sync when adding/changing config fields.
- Config loading:
loadConfig()finds the.dockflow/directory by walking up from CWD viagetProjectRoot().
Typed with ssh2 ConnectConfig in cli/src/utils/ssh.ts. Connection types in cli/src/types/connection.ts:
SSHKeyConnection— host, port, user, privateKeySSHPasswordConnection— host, port, user, password
Keys are passed in-memory (never written to temp files). Core functions: sshExec() (collect output), sshExecStream() (stream with callbacks), sshShell() (interactive).
The dockflow deploy command executes entirely in TypeScript via ssh2:
- Load config, resolve server connections, acquire deployment lock
- Render Nunjucks templates (docker-compose, env files)
- Prepare compose: load YAML, update image tags, inject Swarm defaults, inject Traefik labels
- Build images (local or remote), distribute to Swarm nodes (base64 transfer or registry push)
- Create release directory on manager, upload compose file
- Deploy accessories (hash-based change detection) then app stack via
docker stack deploy -c - - Wait for Swarm convergence, run health checks (internal + HTTP endpoints)
- Cleanup old releases, write audit/metrics, sync history to all nodes
- Release lock (always, even on failure)
All steps are in cli/src/commands/deploy.ts using the services layer.
Ansible is only used for dockflow setup via ansible/configure_host.yml. Remaining roles:
geerlingguy.docker— multi-distro Docker installnginx— reverse proxy setupportainer— Portainer stack deployment
All directories under /var/lib/dockflow/ are created by the deploy command (via SwarmDeployService) with proper ownership so the deploy user can write to them directly via SSH without sudo. The dockflow setup command also creates the base /var/lib/dockflow directory.
Directory constants are defined in cli/src/constants.ts (DOCKFLOW_STACKS_DIR, DOCKFLOW_LOCKS_DIR, DOCKFLOW_AUDIT_DIR, DOCKFLOW_METRICS_DIR, DOCKFLOW_BACKUPS_DIR, DOCKFLOW_ACCESSORIES_DIR).
cli/src/api/server.ts — Bun HTTP server with WebSocket support. Serves the Angular UI (embedded in binary via ui-manifest.generated, or from ui/dist/ in dev).
Routes in cli/src/api/routes/:
- REST:
/api/servers,/api/services,/api/config,/api/deploy,/api/operations,/api/accessories,/api/backup, etc. - WebSocket:
/ws/ssh/:serverName(interactive SSH),/ws/exec/:serviceName(docker exec)
WebSocket sessions include heartbeat (30s), idle timeout (15min), and watchdog cleanup (60s).
Response helpers: jsonResponse(), errorResponse() — both include CORS headers.
Angular 21 standalone components with lazy-loaded routes in cli/ui/src/app/app.routes.ts:
- 12 feature modules: dashboard, servers, services, logs, deploy, build, accessories, monitoring, resources, topology, settings
settingsroute has anunsavedChangesGuard- Shared components (sidebar, header) in
cli/ui/src/app/shared/
Key values in cli/src/constants.ts: DOCKFLOW_VERSION (from package.json), DEFAULT_SSH_PORT (22), LOCK_STALE_THRESHOLD_MINUTES (30), CONVERGENCE_TIMEOUT_S (300), CONVERGENCE_INTERVAL_S (5), directory paths (DOCKFLOW_STACKS_DIR, DOCKFLOW_LOCKS_DIR, etc.).
Tests run in Docker-in-Docker: a manager container (dockflow-test-manager) and a worker (dockflow-test-worker-1) form a real Swarm cluster. Tests are run via bun test (test files are in testing/e2e/tests/) and cover deployment, Traefik routing, backup/restore, and remote builds.
E2E tests run from WSL. The .env.dockflow file uses localhost:222x port mappings to reach the Docker-in-Docker containers from the host.
- build-cli.yml — Triggered by version tags. Builds multi-platform binaries (linux-x64/arm64, macos-x64/arm64, windows-x64), creates GitHub Release, publishes to npm (
@dockflow-tools/cli). - deploy-docs.yml — Documentation site deployment. Installs CLI and runs
dockflow deploydirectly. - e2e-tests.yml — Runs on push to main/develop and PRs. Executes
bun test tests/intesting/e2e/. - shell-lint.yml — ShellCheck validation.
CI/CD integration is handled entirely by the CLI itself — no reusable workflows or external templates needed. The CLI auto-detects environment and version from CI provider env vars (GitHub Actions, GitLab CI, Jenkins, Buildkite) when dockflow deploy or dockflow build are called without arguments. Users generate a standalone CI workflow via dockflow init.
CI secrets format: {ENV}_{SERVER}_{CONNECTION} = base64-encoded user@host:port|privateKey|password.
- Typecheck before committing: Run
bun run typecheckincli/— zero errors required. - Use centralized output helpers: Never add raw
console.log/console.errorin CLI commands or API routes. - Config schema + interface parity: Update both Zod schema and TypeScript interface when adding config fields.
- Error handling: Throw typed
CLIErrorsubclasses from commands. Never catch-and-exit manually. - Services for Docker ops: Use the services layer (
cli/src/services/) for Docker Swarm interactions, not raw SSH commands in command handlers. - Multi-node services: When creating
BackupServiceorExecService, always passgetAllNodeConnections(env)as the third argument so container lookups work on worker nodes too. - New directory paths: Add constants in
cli/src/constants.tsand ensure the deploy command creates them on the remote host.
After implementing any feature or fix, always ask:
- Is the logic correct? Re-read the code with fresh eyes. Check edge cases: empty inputs, missing fields, format variations (e.g. port formats
host:containervsip:host:container). - Is it consistent with the rest of the codebase? Patterns, naming, error handling, output style.
- Did I break anything? Run
bun run typecheck. Think about what else calls the code I changed. - Is this the simplest approach? If the implementation feels complex, step back — there's often a simpler path.
- Are there silent failure modes? Check for unhandled Promise rejections, empty SSH outputs, missing config fields.
Every new user-facing feature must be documented before the task is considered done.
- New page: The feature is a standalone concept with its own config block, workflow, or set of options (e.g.
proxy,registry,hooks). - Update existing page: The change adds a field to an existing concept (e.g. adding a flag to
health_checks).
New pages in docs/app/configuration/ or docs/app/ should follow this order:
- One-line intro — what this feature does and why it matters
- Minimal working example — the simplest config that makes it work
- All options — table with field, type, description, default
- How it works — brief explanation of the mechanism (use
<Steps>for multi-step flows) - Edge cases / caveats — things that can go wrong,
<Callout type="warning">for important ones - Full example — realistic config using
<Tabs>when it spans multiple files
- Use
<Callout type="info">for tips,<Callout type="warning">for gotchas - Code blocks always have a language tag and a comment showing the file path (
# .dockflow/config.yml) - Tables for option references:
| Field | Type | Description | Default | - Link to related pages at the end with "See also" or inline contextual links
- No marketing language. Direct, technical, factual.
After creating a new page:
- Add its slug to
docs/app/configuration/_meta.ts(or the relevant_meta.ts) so it appears in the sidebar - Add a
<Cards.Card>entry in the parent index page (docs/app/configuration/page.mdx) - Add the entry to
docs/scripts/generate-llms-txt.tsso LLM context stays up to date