Skip to content

Tamp Docker

Scott Singleton edited this page May 15, 2026 · 4 revisions

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;

Verbs

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 pull

Each returns a CommandPlan. Common knobs (SetWorkingDirectory, EnvironmentVariables[k]=v) come from DockerSettingsBase.

Docker.Login

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 type

This 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

Docker.Logout(s => s.SetServer("registry.example.com"));
// or:  Docker.Logout()           // → docker logout (default registry)

Settings: Server.

Docker.Build (BuildKit by default, 0.3.0+)

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.

Docker.LegacyBuild (pre-BuildKit fallback)

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 flags

Settings (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).

Migration warnings to expect

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

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

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.

Multi-tag push in one shot

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 time

Shape (b) is preferred when you're already building — saves one round-trip and avoids the intermediate retag.

Pushing to an insecure (plain-HTTP) registry

This is not a wrapper flagdocker 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-builder

Once 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.

Credential handling for private 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

Docker.Pull(s => s
    .SetImage("alpine:3.20")
    .SetPlatform("linux/arm64"));

Settings: Image (required), AllTags, Quiet, DisableContentTrust, Platform.

Recipes

Build + tag + push (single-arch)

[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)),
    });

Multi-arch build (buildx setup is your responsibility)

Target BuildMultiArch => _ => _.Executes(() =>
    Docker.Build(s => s
        .SetContext(".")
        .AddTag(ImageTag)
        .SetPlatform("linux/amd64,linux/arm64")
        .SetOutputType("type=registry")));   // requires `docker buildx` and a builder

Cleanup with AssuredAfterFailure

Target 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.

Notes on V27

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:

  • --platform on pull requires Docker 24+
  • --all-tags on push requires Docker 23+
  • OutputType=type=registry (the buildx feature) requires buildx to 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.

See also

Settings authoring style

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.

Clone this wiki locally