-
Notifications
You must be signed in to change notification settings - Fork 0
Tamp Docker
Wrapper for the Docker 27.x CLI. The image-publish workflow is the focus today: login, build, tag, push, pull, logout. Compose / volume / network / swarm aren't in scope for the first cut — they're plausible additions when there's demand.
using Tamp.Docker.V27;Docker.Login(s => …) // docker login
Docker.Logout(s => …) // docker logout
Docker.Build(s => …) // docker build
Docker.Tag(s => …) // docker tag
Docker.Push(s => …) // docker push
Docker.Pull(s => …) // docker pullEach returns a CommandPlan. Common knobs (SetWorkingDirectory, EnvironmentVariables[k]=v) come from DockerSettingsBase.
The headline feature: when a Secret password is supplied, the wrapper uses --password-stdin so the password value never appears on the command line or in the OS process table.
[Secret("Container registry password", EnvironmentVariable = "REGISTRY_PASSWORD")]
Secret RegistryPassword;
Target RegistryLogin => _ => _.Executes(() =>
Docker.Login(s => s
.SetServer("registry.example.com")
.SetUsername("ci")
.SetPassword(RegistryPassword))); // Secret typeThis emits:
docker login --username ci --password-stdin registry.example.com
stdin: <password value>
The password value is automatically registered with the executor's redaction table — any subsequent log line that happens to echo it gets scrubbed.
Settings: Server, Username, Password (Secret).
Docker.Logout(s => s.SetServer("registry.example.com"));
// or: Docker.Logout() // → docker logout (default registry)Settings: Server.
Docker.Build routes to docker buildx build — BuildKit-backed builds with RUN --mount=type=cache, # syntax=docker/dockerfile:1.x frontends, named build contexts, and the rest of modern Dockerfile syntax. In 2026 reality the legacy builder is the exotic case; the wrapper default reflects that.
Docker.Build(s => s
.SetContext(".") // build context (positional last arg)
.SetDockerfile("Dockerfile.prod")
.AddTag("registry.example.com/myapp:latest")
.AddTag($"registry.example.com/myapp:{Git.Commit[..7]}")
.SetBuildArg("VERSION", "1.2.3")
.SetLabel("org.opencontainers.image.source", "https://github.com/tamp-build/tamp")
.AddPlatform("linux/amd64")
.AddPlatform("linux/arm64")
.AddCacheFrom("type=registry,ref=registry.example.com/myapp:cache")
.SetPush(true)
.SetPull(true));Settings (DockerBuildxBuildSettings): Context (defaults to .), Dockerfile, Tags, BuildArgs, Labels, Target, Platforms (collection), CacheFrom / CacheTo (collections), Outputs (collection), Push, Load, NoCache, Pull, Network, Progress, SecretsArgs, Metadata, Sbom, Provenance.
If your Dockerfile predates BuildKit-only syntax and you specifically need the legacy docker build (no buildx subcommand), use LegacyBuild:
Docker.LegacyBuild(s => s
.SetContext(".")
.AddTag("myapp:legacy")
.SetQuiet(true)
.SetOutputType("type=tar")); // single-value, legacy-only flagsSettings (DockerLegacyBuildSettings): same shape as the old 0.2.0 Docker.Build. Includes legacy-only flags (SetQuiet, SetPlatform (single value), SetOutputType (single value)) that BuildKit replaced with collection-shaped equivalents.
Migration from 0.2.0: compile errors point you at the right path. Most calls keep working as Docker.Build; SetQuiet / single-value SetPlatform / single-value SetOutputType need either LegacyBuild or the buildx-collection equivalents (AddPlatform, AddOutput).
BuildKit runs lint checks the legacy builder didn't. The most common one users hit on migration:
SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data
(ARG "REACT_APP_AUTH_MODE") (line 58)
SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data
(ENV "REACT_APP_AUTH_MODE") (line 65)
The check triggers on the name, not the value — any ARG/ENV named with SECRET, PASSWORD, KEY, AUTH, TOKEN lights it up, even if the value is a non-secret UI flag like REACT_APP_AUTH_MODE. Three options: rename the ARG to dodge the heuristic, use RUN --mount=type=secret if the value really is a secret, or accept the informational warning (BuildKit doesn't fail the build on lint warnings by default). Full discussion in Pitfalls → Buildx migration warnings.
Docker.Tag(s => s
.SetSource("myapp:latest")
.SetTarget("registry.example.com/myapp:1.2.3"));Both SetSource and SetTarget are required; missing either throws at plan-construction time.
Docker.Push(s => s
.SetImage("registry.example.com/myapp:1.2.3"));
// or push every tag of an image:
Docker.Push(s => s
.SetImage("registry.example.com/myapp")
.SetAllTags(true));Settings: Image (required), AllTags, Quiet, DisableContentTrust, Platform.
Two equivalent shapes:
a) Tag the same image twice, then push --all-tags:
Target PushBoth => _ => _.Executes(() => new[] {
Docker.Tag(s => s.SetSource("backend:latest").SetTarget($"registry/backend:{Sha}")),
Docker.Tag(s => s.SetSource("backend:latest").SetTarget("registry/backend:lab-latest")),
Docker.Push(s => s.SetImage("registry/backend").SetAllTags(true)),
});b) Buildx-time multi-tag-push (single layer build, multi-platform):
Target BuildAndPush => _ => _.Executes(() =>
Docker.Build(s => s // routes to buildx
.SetContext(".")
.AddTag($"registry/backend:{Sha}")
.AddTag("registry/backend:lab-latest")
.AddPlatform("linux/amd64")
.AddPlatform("linux/arm64")
.SetPush(true))); // push happens at build timeShape (b) is preferred when you're already building — saves one round-trip and avoids the intermediate retag.
This is not a wrapper flag — docker push doesn't have an --insecure mode. Insecure-registry trust is configured at the Docker daemon level, once:
// /etc/docker/daemon.json
{ "insecure-registries": ["localhost:32000", "registry.lab.local:5000"] }For buildx, configure the builder instance:
docker buildx create --use --driver-opt 'network=host' --buildkitd-flags '--allow-insecure-entitlement network.host' my-builderOnce the daemon (or buildx builder) trusts the registry, Docker.Push against localhost:32000/myapp:tag Just Works — no wrapper-side flag. Common case: in-cluster MicroK8s registry, lab harbors, ephemeral test registries.
The canonical pattern is Docker.Login first (typed Secret password, fed via --password-stdin, registered with the redaction table), then Docker.Push:
[Secret("Registry password", EnvironmentVariable = "DOCKER_PASSWORD")]
readonly Secret RegistryPassword = null!;
Target RegistryLogin => _ => _.Executes(() =>
Docker.Login(s => s
.SetServer("registry.example.com")
.SetUsername("ci")
.SetPassword(RegistryPassword)));
Target Push => _ => _
.DependsOn(RegistryLogin)
.Executes(() => Docker.Push(s => s.SetImage("registry.example.com/myapp:1.2.3")));The password never appears on the command line, never in the process table, and any echo in logs is redacted by the runner. Docker.Login returns a CommandPlan like every other verb — the executor handles the stdin feed.
For environments where docker login has already established credentials (e.g., ARC runner with mounted Docker config, or docker-credential-helper configured), no login call is needed — Docker.Push consults ~/.docker/config.json automatically. The wrapper doesn't fight that path.
Docker.Pull(s => s
.SetImage("alpine:3.20")
.SetPlatform("linux/arm64"));Settings: Image (required), AllTags, Quiet, DisableContentTrust, Platform.
[GitRepository] readonly GitRepository Git;
string ImageBase => "registry.example.com/myapp";
string ImageTag => $"{ImageBase}:{Git.Commit[..7]}";
string ImageLatest => $"{ImageBase}:latest";
Target BuildImage => _ => _.Executes(() =>
Docker.Build(s => s
.SetContext(".")
.AddTag(ImageTag)
.AddTag(ImageLatest)
.SetBuildArg("Version", "1.2.3")
.SetPull(true)));
Target PushImage => _ => _
.DependsOn(nameof(BuildImage), nameof(RegistryLogin))
.Executes(() => new[]
{
Docker.Push(s => s.SetImage(ImageTag)),
Docker.Push(s => s.SetImage(ImageLatest)),
});Target BuildMultiArch => _ => _.Executes(() =>
Docker.Build(s => s
.SetContext(".")
.AddTag(ImageTag)
.SetPlatform("linux/amd64,linux/arm64")
.SetOutputType("type=registry"))); // requires `docker buildx` and a builderTarget Cleanup => _ => _
.AssuredAfterFailure()
.DependsOn(nameof(PushImage))
.Executes(() => Docker.Logout(s => s.SetServer("registry.example.com")));AssuredAfterFailure() ensures Logout runs even when an earlier target in the pipeline failed — important so credentials don't linger in the runner's Docker config.
The package version pin is for the CLI surface that ships with Docker Desktop / Docker Engine 27.x. If your CI runner has an older Docker (24.x, 25.x, 26.x), most flags in this wrapper still work but a small subset are 27-only. Worth knowing:
-
--platformonpullrequires Docker 24+ -
--all-tagsonpushrequires Docker 23+ -
OutputType=type=registry(the buildx feature) requiresbuildxto be installed and a builder configured
For older runtimes that need a different CLI surface, a hypothetical Tamp.Docker.V24 could be published as a sibling package (per ADR 0002) — open an issue if you need this.
- Module Catalog
-
Parameter & Secret Injection —
[Secret]and stdin-based password passing -
Failure Handling —
AssuredAfterFailurefor cleanup pattern
The example(s) above use the fluent Set*-chain shape. Every wrapper verb on this page also accepts a new XxxSettings { ... } object-init form. Both produce identical CommandPlans; the fluent shape stays canonical in docs and the tamp init template. See Build Script Authoring → Two authoring styles for the side-by-side comparison.
Start here
Modules
- Module Catalog (canonical list, 50+ satellites)
- .NET toolchain
- Containers
- JS toolchain
- Supply-chain security
Analyzers
Tooling
Execution
CI integration
Editor integration
Migration
Reference
Project