Contributing to Docker Commander
August 7, 2026 · View on GitHub
Thanks for your interest in improving Docker Commander! This guide covers how to build, test and submit changes.
By participating you agree to follow our Code of Conduct.
Ways to contribute
- Report bugs and request features via issues (use the templates). For security problems, do not open a public issue — see SECURITY.md.
- Improve docs under
docs/or the README. - Send pull requests — for anything non-trivial, please open an issue first so we can agree on the approach.
Project layout
cmd/dockercmd/ # main: wiring, config, server bootstrap
internal/ # Go backend (api, auth, store, docker, monitor, mcp, ws, history,
# templates, backup, selfupdate, tlscert, service, config, crypto)
web/ # React + TypeScript SPA (Vite, Tailwind); built into web/dist and embedded
docs/ # per-feature user manual
deploy/ # systemd unit + config example
The production artifact is a single CGO-free binary with the UI embedded
(go:embed web/dist).
Development setup
You need Go ≥ 1.25, Node.js ≥ 18 (to build the UI) and a running Docker daemon (the app talks to it; some tests use it).
git clone https://github.com/koduj-dev/docker-commander.git
cd docker-commander
make build # builds the UI, then the binary with the UI embedded
./dockercmd # http://127.0.0.1:8470
For UI work, run the API and the Vite dev server side by side:
make dev # API on :8470 (dev mode, permissive CORS)
cd web && npm ci && npm run dev # UI on :5173, proxies /api → :8470
The committed
web/distmatters. It letsgo build ./...work without Node, and it is whatgo installembeds. If you change anything underweb/src, rebuild it withmake uiand commit the regeneratedweb/distas part of your PR — CI compares the committed bundle against its own rebuild and fails if they differ.
Use
npm cirather thannpm install. An npm older than the one that wrotepackage-lock.jsonsilently drops fields it does not recognise, and the edit then rides along in your next commit.
Tests
docs/testing.md describes every tier, what each one proves and what isn't covered. The commands:
go test -short ./... # fast unit + adversarial tests — this is what CI runs
go test ./... # also runs integration tests (need Docker; some spin
# throwaway Redis / OpenLDAP / MailHog containers and
# skip cleanly when those aren't available)
npm run test --prefix web # frontend unit tests
cd web && npx tsc --noEmit # type-check the frontend
# Remote/multi-host paths, against real separate daemons over TCP and SSH:
scripts/remote-test-daemon.sh up 2
eval "$(scripts/remote-test-daemon.sh env)"
go test ./internal/docker/ -run 'RemoteBindDeploy|MultiHost' -count=1 -v
scripts/remote-test-daemon.sh down
Pass
-count=1for anything touching a real daemon: Go caches test results and an env-var change doesn't invalidate the cache, so a re-provisioned daemon will otherwise replay the previous verdict.
Before changing behaviour, skim docs/gotchas.md — it lists the things in this codebase that have already caught someone out. Local setup and the throwaway services the tests expect are in docs/dev-environment.md.
Adding a test? Prove it can fail. Break the thing it tests, watch it fail,
check it failed for the right reason, then restore. Tests in this repo have
passed while guarding nothing — usually because something else (compose's own
validation, a shared status code) was doing the rejecting, and more than once
because the test asked a neighbouring question, or the environment answered it.
docs/testing.md lists the specific failure modes and the fixture traps that go
with real-daemon tests. Restore from a copy you took first, not with
git checkout — mutation testing means editing real files, and that command has
eaten uncommitted work here.
Fixing a review finding? The fix needs reviewing too. On the 1.6.0 authentication work, four consecutive rounds each found a new defect inside the previous round's fix. Before calling one done, ask the inverse question — a check needs a matching record, a record needs someone reading it, state cleared on failure may need to be cleared only on success — and then ask what the fix makes worse.
⚠️ The integration tests run against your real local Docker daemon. They create and clean up their own throwaway resources, but never add a host-global operation (
docker {system,network,image,volume} prune) to a test — it would wipe the developer's own resources, not just the test's. If you only want the safe, deterministic run, usego test -short ./....
Please add or update tests for behaviour you change. New backend code should
come with coverage; heavy integration tests are gated behind testing.Short()
so the default CI run stays deterministic.
Review discipline
This app controls Docker daemons, so review changes accordingly — proportionally to the risk of the change:
- Before a commit — read your own diff for correctness and security (auth / permissions, input handling, secret exposure, unsafe defaults) and fix what you find.
- Before a PR — do a full code + security review of the whole branch, and for
any new attack surface (auth, parsers, endpoints, anything taking external
input) add adversarial tests asserting the attack is rejected — see the
TestPen_*cases in*_pentest_test.go(e.g.internal/mcp/pentest_test.go,internal/api/oauth_pentest_test.go). Keepgo test -short ./...green. - When a bug turns out to be an instance of a class, sweep the class. Patching
the three tools that authorized against the wrong host is half the job; the half
that lasts is a test enumerating every tool with that shape, which fails on a new
one nobody has decided about (
internal/mcp/tool_host_scope_coverage_test.go,tool_authz_coverage_test.go). Prefer a check derived from the real registry — routes, advertised tools — over a hand-maintained list, which goes stale silently.
This is guidance, not tooling, but it's how the security-sensitive parts of the codebase have been built.
Beyond per-change review, the tree is periodically swept end-to-end by an adversarial review on Claude Fable 5 — independent reviewers per lane (auth & crypto, authorization, untrusted input, backend correctness, frontend), each told to refute a finding before reporting it. It is not a smoke test and it is not a tier: it finds what to test, and a finding is only handled once it lands as a fix plus a test that fails without it. See docs/testing.md.
Code style
- Go must be
gofmt-clean. CI enforces it withgofmt -l $(git ls-files '*.go')— note that checks only tracked files, so rungofmt -waftergit add(or format before staging). Also keepgo vet ./...clean. - TypeScript must type-check (
tsc --noEmit); match the surrounding style. - Write code that reads like the code around it — match naming, comment density and idioms. Keep comments about why, not what.
A few project conventions
- Database migrations are additive. Add idempotent
ALTER TABLE … ADD COLUMN … DEFAULT …statements (tolerating "duplicate column"); never write destructive migrations. SQLite viamodernc.org/sqlite(no CGO). - Object refs (image/container names) contain
:and/, so pass them as query params, not chi path segments. - Request bodies use
DisallowUnknownFields()— strip read-only fields client-side.
Pull requests
- Branch off
main. - Keep commits focused; write imperative, descriptive messages (what & why).
- Make sure
go test -short ./...,go vet ./..., thegofmtgate and the frontend type-check all pass; rebuildweb/distif you touched the UI. - Update
docs/and add aCHANGELOG.mdentry for user-facing changes. - Open the PR against
mainand fill in the template. CI must be green.
License
By contributing, you agree that your contributions are licensed under the project's MIT License.