jkunz 3614e91c58
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / metadata (push) Skipped
v3.4.2
2026-08-18 23:56:45 +00:00
2026-08-18 23:56:45 +00:00
2026-08-18 23:56:45 +00:00
2017-10-13 17:50:31 +02:00
2026-08-18 23:56:45 +00:00

@git.zone/tsdocker

🐳 The ultimate Docker development toolkit for TypeScript projects — build, test, and ship multi-arch containerized applications with zero friction.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

What is tsdocker?

tsdocker is a comprehensive Docker development and build tool that handles everything from testing npm packages in clean environments to building and pushing multi-architecture Docker images across multiple registries — all from a single CLI.

🎯 Key Capabilities

  • 🏗️ Smart Docker Builds — Automatically discover, sort, and build Dockerfiles by dependency
  • 🌍 True Multi-Architecture — Build for amd64 and arm64 simultaneously with Docker Buildx
  • 🚀 Multi-Registry Push — Ship to Docker Hub, GitLab, GitHub Container Registry, and more via OCI Distribution API
  • Parallel Builds — Level-based parallel builds with configurable concurrency
  • 🗄️ Persistent Local Registry — All images flow through a local OCI registry with persistent storage
  • 📦 Provenance-Aware Caching — Skip only images whose content, source metadata, labels, and tag ownership still match
  • 🎯 Dockerfile Filtering — Build or push only specific Dockerfiles using glob patterns
  • 🔁 Resilient Push — Operation-specific retry and timeout bounds for OCI registry requests
  • 🏭 CI-Safe Isolation — Unique sessions per invocation prevent collisions in parallel CI pipelines
  • 🔎 OCI Provenance — Label images with package version, exact Git revision, and a credential-free source URL
  • 🧪 Default-User Tests — Run copied Bash test scripts as the image's configured user without committing derivative images
  • 🔧 Zero Config Start — Works out of the box, scales with your needs

Installation

# Global installation (recommended for CLI usage)
pnpm add --global @git.zone/tsdocker

# Or project-local installation
pnpm add --save-dev @git.zone/tsdocker

Quick Start

🏗️ Build Docker Images

Got Dockerfile files? Build them all with automatic dependency ordering:

tsdocker build

tsdocker will:

  1. 🔍 Discover all Dockerfile* files in your project
  2. 📊 Analyze FROM dependencies between them
  3. 🔄 Sort them topologically
  4. 🏗️ Build each image in the correct order
  5. 📦 Push every image to a persistent local registry (.nogit/docker-registry/)

📤 Push to Registries

Ship your images to one or all configured registries:

# Push to all configured registries
tsdocker push

# Push to a specific registry
tsdocker push --registry=registry.gitlab.com

# Authenticate only registries used by private base images before building
tsdocker push --registry=registry.gitlab.com \
  --build-registries=registry.gitlab.com \
  --test

# Push a previously built, digest-bound candidate without rebuilding
export TSDOCKER_SESSION_ID=release-candidate-123
EXPECTED_DIGEST="$(tsdocker digest Dockerfile_##version## --json | jq -r '.digests[0].digest')"
tsdocker push --no-build Dockerfile_##version## --expected-digest="$EXPECTED_DIGEST"

An immutable registry can record a previously published exact manifest without rebuilding or moving its tag. This command is intended only for registries that atomically reject attempts to change an existing tag to another digest:

tsdocker reannounce-immutable Dockerfile_##version## \
  --registry=registry.example.com \
  --expected-digest=sha256:<64-lowercase-hex>

reannounce-immutable reads and hashes the registry's raw manifest bytes, requires the asserted digest, sends the same bytes and media type back to the same tag, and verifies the tag again. It does not build, copy blobs, or create deployment trust evidence; the destination registry remains the authority for authorization, immutable-tag enforcement, and any server-side evidence.

Under the hood, tsdocker push uses the OCI Distribution API to copy images directly from the local registry to remote registries. This preserves multi-arch manifest lists end-to-end. General registry calls retry network and 5xx failures up to six attempts with exponential backoff and a five-minute timeout; ordinary 4xx responses are returned without retry. Chunk PATCH failures, including non-202 responses, instead reconcile upload status and can retry the remaining bytes up to six times. Authentication, metadata, upload-status, and other upload requests use lower operation-specific bounds.

🎯 Build Only Specific Dockerfiles

Target specific Dockerfiles by name pattern — dependencies are resolved automatically:

# Build only the base image
tsdocker build Dockerfile_base

# Build anything matching a glob pattern
tsdocker build Dockerfile_app*

# Inspect exact source digests, then push specific images without rebuilding
mapfile -t EXPECTED_DIGESTS < <(
  tsdocker digest Dockerfile_api Dockerfile_web --json |
    jq -r '.digests[] | "\(.cleanTag)=\(.digest)"'
)
tsdocker push --no-build Dockerfile_api Dockerfile_web \
  "${EXPECTED_DIGESTS[@]/#/--expected-digest=}"

CLI Commands

Command Description
tsdocker Show usage / man page
tsdocker build Build all Dockerfiles with dependency ordering
tsdocker push Build + push images to configured registries
tsdocker pull <registry> Pull images from a specific registry
tsdocker reannounce-immutable Reannounce exact manifests to an immutable registry
tsdocker test Build + run container test scripts (test_*.sh)
tsdocker digest Inspect canonical local-registry top-level manifest digests
tsdocker login Authenticate with configured registries
tsdocker list Display discovered Dockerfiles and their dependencies
tsdocker config Manage global tsdocker configuration (remote builders, etc.)
tsdocker clean Interactively clean Docker environment
tsdocker prune Report or remove tsdocker-owned registry cache resources

Build Flags

Flag Description
<patterns> Positional Dockerfile name patterns (e.g. Dockerfile_base, Dockerfile_app*)
--platform=linux/arm64 Override build platform for a single architecture
--only-archs=amd64,riscv64 Strictly override the configured architecture list
--timeout=600 Timeout in seconds for each image build
--no-cache Force rebuild without Docker layer cache
--cached Skip unchanged Dockerfiles (content-hash based)
--verbose Stream raw docker build output
--parallel Enable level-based parallel builds (default concurrency: 4)
--parallel=8 Parallel builds with custom concurrency
--context=mycontext Use a specific Docker context

Push Flags

Flag Description
<patterns> Positional Dockerfile name patterns to select which images to push
--registry=<url> Push to a single specific registry instead of all configured
--build-registries=<csv> Authenticate only these configured registries before a build
--test Run configured image tests before destination publication
--no-build Skip the build phase; requires exact expected source digest(s)
--expected-digest=<value> Expected top-level source digest; repeat mappings for many images
--cached Skip unchanged Dockerfiles during the build phase

push --no-build and test --no-build require --expected-digest, or the TSDOCKER_EXPECTED_DIGEST fallback. A single selected Dockerfile accepts a bare sha256:<64 lowercase hex> digest. Multiple Dockerfiles require one repeated cleanTag=sha256:... flag per selected image. The environment fallback accepts comma- or newline-separated mappings. Missing, extra, unknown, duplicate, malformed, and mismatched expectations fail before tests or remote registry mutation.

reannounce-immutable [patterns...] accepts zero or more Dockerfile patterns; omitting patterns selects every discovered Dockerfile. It requires exactly one --registry destination and the same complete digest mapping. Do not use it with a mutable registry: safe concurrent operation depends on the destination atomically refusing an existing tag whose digest differs from the submitted manifest.

By default, a build-enabled push authenticates every configured registry so private base images remain available even when their registry differs from the destination. Set --build-registries to a unique comma-separated list of configured registry hosts to bound that pre-build authentication. The option does not add destinations and does not change which registries receive images. With --test, every selected image test must pass before the first destination registry is mutated. A test failure aborts the push.

Digest Inspection

# Human-readable mappings that can be passed back as --expected-digest values
tsdocker digest Dockerfile_api Dockerfile_web

# Clean JSON stdout for CI tooling
tsdocker digest Dockerfile_api Dockerfile_web --json

Digest inspection reads the persisted canonical local registry without rebuilding. JSON output has the shape {"sessionId":"...","digests":[{"cleanTag":"app:v1","digest":"sha256:..."}]}.

Config Subcommands

Subcommand Description
add-builder Add or update a remote builder node
remove-builder Remove a remote builder by name
list-builders List all configured remote builders
show Show the full global configuration

add-builder flags:

Flag Description
--name=<name> Builder name (e.g. arm64-builder)
--host=<user@ip> SSH host (e.g. armbuilder@192.168.1.100)
--platform=<p> Target platform (e.g. linux/arm64)
--ssh-key=<path> SSH key path (optional, uses SSH agent/config by default)

Clean Flags

Flag Description
--all Include all images and volumes (not just dangling)
-y Auto-confirm all prompts

Prune Flags

tsdocker prune is dry-run by default. It reports labeled tsdocker registry containers whose recorded owners are proven dead and marked project-local registry cache directories that are safe to remove.

Flag Description
--apply Remove proven-abandoned labeled registry containers and marked, inactive registry cache directories
--context=<name> Inspect a specific Docker context

Prune never deletes unmarked app data such as MongoDB, MinIO, or arbitrary Docker volumes. Container deletion requires complete tsdocker ownership labels and a proven-dead owner; the exact container ID and labels are re-inspected immediately before removal. Cache deletion requires a valid .gitzone-tool-cache.json marker, an allowlisted project path, and no active bind mount. Automatic abandoned-container cleanup never deletes the persistent registry data directory.

Configuration

Configure tsdocker in your .smartconfig.json under the @git.zone/tsdocker key:

{
  "@git.zone/tsdocker": {
    "registries": ["registry.gitlab.com", "docker.io"],
    "registryRepoMap": {
      "registry.gitlab.com": "myorg/myproject"
    },
    "buildArgEnvMap": {
      "NODE_VERSION": "NODE_VERSION"
    },
    "buildSecretEnvMap": {
      "npm_auth": "SZCI_TOKEN_NPM_1"
    },
    "platforms": ["linux/amd64", "linux/arm64"],
    "buildxMemory": "8g",
    "buildxMemorySwap": "8g",
    "buildxCpuQuota": 400000,
    "buildxCpuPeriod": 100000,
    "testDir": "./test",
    "requireTestFile": true
  }
}

Configuration Options

Build & Push Options

Option Type Default Description
registries string[] [] Registry URLs to push to
registryRepoMap object {} Map registries to different repository paths
buildArgEnvMap object {} Map Docker build ARGs to environment variables
buildSecretEnvMap object {} Map BuildKit secret IDs to required environment variables
platforms string[] ["linux/amd64"] Target architectures for multi-arch builds
buildxMemory string "8g" Memory limit applied independently to each BuildKit node
buildxMemorySwap string buildxMemory Memory + swap limit per node; the default disables swap
buildxCpuQuota number 400000 CPU quota applied to each BuildKit node
buildxCpuPeriod number 100000 CPU period applied to each BuildKit node
testDir string ./test Directory containing test scripts
requireTestFile boolean false Fail when neither a versioned nor generic test exists
autoCleanup boolean true Remove or prune local-only builders and attempt abandoned-topology removal; owned remote topology retirement and ownership safety remain mandatory
cleanupOnStart boolean true Prune the selected builder before a build
buildxPruneUntil string "168h" Prune selected-builder cache entries older than this duration; "0" disables the age rule
buildxPruneMaxUsedSpace string "2gb" Bound selected-builder cache usage; "0" disables the size rule
removeCiBuilders boolean true Remove local-only CI builders at command end; false retains them and enables pruning only while autoCleanup remains enabled; remote topologies are always retired

Architecture: How tsdocker Works

tsdocker uses a local OCI registry as the canonical store for all built images. This design solves fundamental problems with Docker's local daemon, which cannot hold multi-architecture manifest lists.

📐 Build Flow

┌─────────────────────────────────────────────────────┐
│  tsdocker build                                     │
│                                                     │
│  1. Start local registry (localhost:<dynamic-port>)  │
│     └── Persistent volume: .nogit/docker-registry/  │
│                                                     │
│  2. For each Dockerfile (topological order):         │
│     ├── Multi-platform: buildx --push → registry    │
│     ├── Explicit single platform: buildx --load     │
│     └── Default single platform: docker build       │
│                                                     │
│  3. Stop local registry (data persists on disk)      │
└─────────────────────────────────────────────────────┘

🔎 Image Provenance

Every standard, Buildx single-platform, and Buildx multi-platform build receives the same source labels:

Label Source
version Package version; retained for compatibility
org.opencontainers.image.version Package version
org.opencontainers.image.revision Exact Git HEAD
org.opencontainers.image.source Sanitized package.json repository URL

Repository credentials, query parameters, fragments, control characters, and unsafe path traversal are never copied into labels. Missing metadata is omitted with a warning rather than represented as unknown; release pipelines can require the complete three-key OCI set.

--cached validates the provenance fingerprint, configured target platform, inspected image OS/architecture/variant, actual image labels, image ID, and current tag mapping before skipping a build. Older or incomplete cache formats fail closed and rebuild. Configured singleton platforms stay on the standard Docker path with an explicit, quoted --platform and remain cacheable because the target and loaded image platform are both verified. Explicit --platform, --only-archs, and multi-platform override builds continue to ignore --cached.

📤 Push Flow

┌────────────────────────────────────────────────────────┐
│  tsdocker push                                         │
│                                                        │
│  1. Start local registry (loads persisted data)         │
│                                                        │
│  2. For each image × each remote registry:              │
│     └── OCI Distribution API copy (with retry):        │
│         ├── Verify expected source digest (--no-build)  │
│         ├── Fetch manifest (single or multi-arch)       │
│         ├── Copy blobs (skip if already exist)          │
│         ├── Retry up to 6× with exponential backoff    │
│         ├── Push manifest with destination tag          │
│         └── Verify destination tag has source digest    │
│                                                        │
│  3. Stop local registry                                │
└────────────────────────────────────────────────────────┘

🔑 Why a Local Registry?

Problem Solution
docker buildx --load fails for multi-arch images buildx --push to local registry works for any number of platforms
docker push only pushes single-platform manifests OCI API copy preserves full manifest lists (multi-arch)
Images lost between build and push phases Persistent storage at .nogit/docker-registry/ survives restarts
Redundant blob uploads on incremental pushes HEAD checks skip blobs that already exist on the remote

🔁 Resilient Push

The OCI Distribution API client applies operation-specific request bounds:

  • Timeouts — Five minutes for general registry/blob requests and 30 seconds for authentication, metadata, and upload-status calls
  • Automatic Retry — General requests use up to six attempts; chunk PATCH failures reconcile accepted bytes and can retry up to six times, while other upload substeps use lower one- or three-attempt limits
  • Retry Logic — General requests retry network errors (ECONNRESET, fetch failed) and 5xx server errors, but not 4xx responses; chunk PATCH treats every non-202 response as a resumable chunk failure
  • Token Cache — A 401 from a general registry request clears its cached token for a later independent request; upload requests do not perform that cache update

This lets transient connection and server failures resume within explicit bounds instead of silently hanging a multi-arch transfer.

Registry copy returns exact source and destination manifest evidence. A copy is successful only when the destination tag resolves to the same content-verified top-level digest as the source. Push also fails nonzero when no Dockerfiles are found, a pattern matches none, a requested registry is unavailable, or no destination registries are configured.

Multi-registry publication is sequential rather than transactional. If a later registry fails, an earlier verified destination can remain published; record the returned evidence and either complete or explicitly roll back that release. The same applies to finalization: if destination publication succeeds but exact SSH, local registry, or Buildx cleanup fails, the command exits nonzero even though the remote tag may already have been updated.

🏭 CI-Safe Session Isolation

Every tsdocker invocation gets a session and an internal ownership identity:

  • Session ID — Random 8-char hex (override with TSDOCKER_SESSION_ID)
  • Invocation ID — Cryptographically random 32-char hex, never caller-controlled
  • Project ID — Full SHA-256 of the current working-directory string
  • Registry port — Dynamically allocated (override with TSDOCKER_REGISTRY_PORT)
  • Registry container — Named tsdocker-registry-<projectId>-<invocationId> for every invocation
  • Builder suffix — Reusable local-only topologies use the project hash plus the public CI session when applicable; removable CI-local and every remote-node topology also include the internal invocation identity
  • Registry data path — Local runs use .nogit/docker-registry/; CI runs use .nogit/docker-registry/<sessionId>/

Registry cache directories are marked with .gitzone-tool-cache.json. Registry containers receive the project and invocation IDs plus exact owner host, PID namespace, PID, and process-start labels. Startup first rejects unsafe name, data-path, and port conflicts, then uses docker create, verifies the returned full container ID and canonical labels, binds that exact identity, and runs docker start <id>. It removes only an exact container owned by the current invocation or a same-project container whose owner is proven dead and whose automatic cleanup is enabled. A create or inspect outcome that cannot establish an exact binding remains indeterminate and never authorizes name-based cleanup or a port retry.

Remote topology names isolate concurrent invocations. A project registry intentionally has one owner at a time; concurrent commands in one local project, or CI commands sharing one project and session ID, are rejected instead of sharing its persistent registry. Auto-detected CI systems:

Environment Variable CI System
GITEA_ACTIONS Gitea Actions
GITHUB_ACTIONS GitHub Actions
GITLAB_CI GitLab CI
CI Generic CI

In local development, local-only builder suffixes are stable per project path, keeping a reusable builder while avoiding collisions between projects. Retained CI-local builders are stable per project and public session ID. CI-local builders selected for end-of-run removal, and every topology containing a remote node, are invocation-scoped so parallel runs cannot remove or replace each other's builders. Existing reusable local builders are never replaced automatically when their nodes or resource limits differ; setup fails and requires manual idle verification before removal.

Separate CI invocations that build, inspect, test, and push one unchanged candidate must set the same non-secret TSDOCKER_SESSION_ID and reuse the same persisted project workspace (specifically .nogit/docker-registry/<sessionId>/). Run those invocations sequentially: the project and session ID together determine the persistent registry data path. Container names remain invocation-scoped, while exact path-conflict checks prevent simultaneous ownership of the same persisted candidate. Identical session IDs in different projects remain isolated. A new release candidate must use a new session ID. Session IDs are limited to 1-64 letters, digits, dots, underscores, or hyphens. Registry passwords are passed through Docker's --password-stdin interface and are never interpolated into shell command strings.

🔍 Docker Context & Topology Detection

tsdocker automatically detects your Docker environment topology:

Topology Detection Meaning
local Default Standard Docker installation on the host
socket-mount /.dockerenv exists Running inside a container with Docker socket mounted
dind DOCKER_HOST starts with tcp:// Docker-in-Docker setup

Context-aware builder names (tsdocker-builder-<context>) prevent conflicts across Docker contexts. Rootless Docker configurations trigger appropriate warnings.

Registry Authentication

Environment Variables

# Pipe-delimited format (supports DOCKER_REGISTRY_1 through DOCKER_REGISTRY_10)
export DOCKER_REGISTRY_1="registry.gitlab.com|username|password"
export DOCKER_REGISTRY_2="docker.io|username|password"

# Individual registry format
export DOCKER_REGISTRY_URL="registry.gitlab.com"
export DOCKER_REGISTRY_USER="username"
export DOCKER_REGISTRY_PASSWORD="password"

Docker Config Fallback

When pushing, tsdocker will also read credentials from ~/.docker/config.json if no explicit credentials are provided via environment variables. This means docker login credentials work automatically. Docker Hub special cases (docker.io, index.docker.io, registry-1.docker.io) are all recognized.

Login Command

tsdocker login

Authenticates with all configured registries using the provided environment variables.

Advanced Usage

🔀 Multi-Architecture Builds

Build for multiple platforms using Docker Buildx:

{
  "@git.zone/tsdocker": {
    "platforms": ["linux/amd64", "linux/arm64"]
  }
}

tsdocker automatically:

  • Sets up Buildx nodes with host networking and bounded memory, swap, and CPU driver options
  • Pushes multi-platform images to the local registry via buildx --push
  • Copies the full manifest list (including all platform variants) to remote registries on tsdocker push

Each local or remote docker-container node defaults to 8 GiB of memory, no swap, and four CPU cores (cpu-quota=400000, cpu-period=100000). Limits apply independently to each node, not collectively across concurrent builders. Override them with buildxMemory, buildxMemorySwap, buildxCpuQuota, and buildxCpuPeriod. Environment overrides take precedence and are named TSDOCKER_BUILDX_MEMORY, TSDOCKER_BUILDX_MEMORY_SWAP, TSDOCKER_BUILDX_CPU_QUOTA, and TSDOCKER_BUILDX_CPU_PERIOD. Startup cache maintenance targets only the builder selected for the current project and platform topology; it never enumerates or wakes unrelated builders. If a reusable local builder already exists but its exact node topology or resource limits differ, tsdocker refuses to remove or replace it automatically.

Use --only-archs when the published architecture set should be explicit:

tsdocker push --only-archs=amd64,riscv64

The flag overrides the project's configured platforms for that build. It accepts the Linux GOARCH names 386, amd64, arm, arm64, loong64, mips, mipsle, mips64, mips64le, ppc64, ppc64le, riscv64, and s390x. The aliases x64amd64, aarch64arm64, and risc-vriscv64 are also accepted. It cannot be combined with --platform or --no-build.

The selection is strict: tsdocker does not probe builders or silently remove an architecture. If the selected Buildx topology cannot build every requested architecture, the build fails. Architecture-specific builds also ignore --cached because the content cache does not encode the selected platforms.

🖥️ Native Remote Builders

Instead of relying on slow QEMU emulation for cross-platform builds, tsdocker can use native remote machines via SSH as build nodes. For example, use a real arm64 machine for linux/arm64 builds:

# Add a remote arm64 builder
tsdocker config add-builder \
  --name=arm64-builder \
  --host=armbuilder@192.168.1.100 \
  --platform=linux/arm64 \
  --ssh-key=~/.ssh/id_ed25519

# List configured builders
tsdocker config list-builders

# Remove a builder
tsdocker config remove-builder --name=arm64-builder

# Show full global config
tsdocker config show

Global configuration is stored at ~/.git.zone/tsdocker/config.json.

How it works:

When remote builders match the effective platform selection (the project platforms or a CLI override), tsdocker automatically:

  1. Creates an exact Buildx node topology — local nodes cover only unmatched platforms; if remotes cover every selected platform, no unrestricted local node is added
  2. Opens SSH reverse tunnels so the remote builder can push to the local staging registry
  3. Builds matching remote platforms natively; unmatched selected platforms stay on the constrained local Buildx node and may require emulation
  4. Tears down tunnels and the invocation-owned remote Buildx topology after the command completes

Remote-node topologies are ephemeral and use a cryptographically random internal invocation fingerprint, even when commands reuse the same public TSDOCKER_SESSION_ID. Removing a topology also releases its remote BuildKit state instead of accumulating one cache volume per project on a shared native builder. Non-CI local-only builders remain persistent and receive bounded cache pruning. autoCleanup: false disables local-only builder removal/pruning and the best-effort Docker removal attempted for an abandoned topology; it cannot disable ownership checks, tombstone blocking, or retirement of a topology owned by the current process. In CI, removeCiBuilders: false retains and prunes local-only builders; remote-node topologies are always retired.

Before any Docker command can create a remote node, tsdocker writes a schema-2 ownership record below .nogit/tsdocker-buildx-ownership/ and durably changes its phase from reserved to creating. The record includes the exact builder, Docker context, and planned local or remote BuildKit container and volume names. A live creating owner deletes its record only after one successful, single-flight docker buildx rm; a live reserved owner may release its record after Buildx confirms it is absent.

A later run with automatic cleanup enabled can discard a dead same-host reserved record because no topology mutation could have started. With automatic cleanup disabled, that unused reservation is retained but does not authorize mutation. A dead creating record is intentionally fail-closed: tsdocker attempts exact Buildx removal when automatic cleanup is enabled, retains the record as a tombstone, prints the exact resources requiring verification, and refuses to create another remote topology. Verify every recorded container and volume is absent, then remove only that tombstone. Never delete it before completing that verification. A foreign-host or malformed record blocks remote setup because its owner state cannot be proven; a live same-host record is left untouched. An incomplete atomic .pending artifact also blocks setup until its exact resources are verified manually; a validated completed .release artifact is safe to finish removing during reconciliation. Ownership records require a persistent workspace across invocations.

SSH reverse tunnels have a separate journal below .nogit/tsdocker-ssh-ownership/. An opening record is fsynced before SSH starts; after tsdocker captures the exact directly owned child, the record becomes active. The record contains the host and random control-socket path but never the SSH key path. A later run releases a dead-owner active record directly when both the exact child and control socket are already absent. If the exact child is still alive with its same-UID owned socket, recovery uses ssh -O check, ssh -O exit, and closed-process/socket proof. It never signals a recovered numeric PID. Dead opening records, current-invocation conflicts, malformed records, foreign projects, and unverifiable owners block remote setup; an unrelated exact live owner remains untouched and nonblocking. Incomplete .pending artifacts block for manual verification, while validated .release artifacts are completed during reconciliation. autoCleanup: false disables abandoned SSH mutation without weakening a required block.

For a mixed local linux/amd64 and remote linux/arm64 selection:

[Local machine]                        [Remote arm64 machine]
  registry:2 on localhost:PORT  <──── SSH reverse tunnel ──── localhost:PORT
  BuildKit (amd64) ──push──>            BuildKit (arm64) ──push──>
     localhost:PORT                        localhost:PORT (tunneled)

Prerequisites for the remote machine:

  • Docker installed and running
  • A user with Docker group access (no sudo needed)
  • SSH key access configured

The machine running tsdocker must be Linux with usable /proc, machine identity, boot identity, and PID namespace metadata. Native remote setup fails closed when it cannot capture exact owner and SSH child process identities.

Parallel Builds

Speed up builds by building independent images concurrently:

# Default concurrency (4 workers)
tsdocker build --parallel

# Custom concurrency
tsdocker build --parallel=8

# Works with caching too
tsdocker build --parallel --cached

tsdocker groups Dockerfiles into dependency levels using topological analysis. Images within the same level have no dependencies on each other and build in parallel. Each level completes before the next begins.

📦 Dockerfile Naming Conventions

tsdocker discovers files matching Dockerfile*:

File Name Version Tag
Dockerfile latest
Dockerfile_v1.0.0 v1.0.0
Dockerfile_alpine alpine
Dockerfile_##version## Uses package.json version

🎯 Dockerfile Filtering

Build or push only the Dockerfiles you need. Positional arguments are matched against Dockerfile basenames as glob patterns:

# Build a single Dockerfile
tsdocker build Dockerfile_base

# Glob patterns with * and ? wildcards
tsdocker build Dockerfile_app*

# Multiple patterns
tsdocker build Dockerfile_base Dockerfile_web

# Push a specific image without rebuilding
tsdocker push --no-build Dockerfile_api \
  --expected-digest="api:v1.0.0=sha256:<64-lowercase-hex>"

When filtering for build, dependencies are auto-resolved: if Dockerfile_app depends on Dockerfile_base, specifying only Dockerfile_app will automatically include Dockerfile_base in the build order.

🔗 Dependency-Aware Builds

If you have multiple Dockerfiles that depend on each other:

# Dockerfile_base
FROM node:20-alpine
RUN npm install -g typescript

# Dockerfile_app
FROM myproject:base
COPY . .
RUN npm run build

tsdocker automatically detects that Dockerfile_app depends on Dockerfile_base, builds them in the correct order, and makes the base image available to dependent builds via the local registry (using --build-context for buildx).

🧪 Container Test Scripts

Create test scripts in your test directory:

# test/test_latest.sh
#!/bin/bash
node --version
npm --version
echo "Container tests passed!"

Run with:

tsdocker test

This builds all images, starts the local registry, and runs each matching script with Bash as the image's configured default USER. tsdocker copies a temporary mode-normalized script into an invocation-unique container, starts it attached, and removes both the container and host staging data in a finally path. It does not create a root-owned directory in the image and does not commit a derivative test image. Images tested this way must contain bash.

Docker result failures retain their exit code and bounded stderr/stdout, while spawn failures retain the operation that could not start. Cleanup failures never replace the primary test failure.

Test containers carry canonical project, invocation, container-name, image, and exact process-owner labels. Before creating one, tsdocker enumerates full IDs and removes only same-project containers with proven-dead owners when automatic cleanup is enabled. Any unresolved current-invocation container blocks before owner status is considered; malformed, live-conflicting, and unverifiable identities also block the run. Creation or inspection ambiguity never triggers name-based deletion; cleanup begins only after the returned full ID and labels have been bound exactly.

For each image, discovery prefers test_<version>.sh and falls back to test_latest.sh. Set requireTestFile: true in release/CI configuration to turn a missing script into an error instead of a warning.

To test an already accepted candidate without rebuilding, reuse its session and bind the canonical local-registry source digest:

export TSDOCKER_SESSION_ID=release-candidate-123
EXPECTED_DIGEST="$(tsdocker digest Dockerfile_##version## --json | jq -r '.digests[0].digest')"
tsdocker test --no-build Dockerfile_##version## --expected-digest="$EXPECTED_DIGEST"

The no-build path preflights every selected tag, then creates the test container from the verified digest-pinned repo@sha256:... reference.

🔧 Build Args from Environment

Pass non-sensitive build configuration as Docker build arguments:

{
  "@git.zone/tsdocker": {
    "buildArgEnvMap": {
      "NODE_VERSION": "NODE_VERSION"
    }
  }
}
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}

Build arguments are visible in build commands and can persist in image history, so never use them for credentials.

🔐 Build Secrets from Environment

Map each BuildKit secret ID to a required environment variable:

{
  "@git.zone/tsdocker": {
    "buildSecretEnvMap": {
      "npm_auth": "SZCI_TOKEN_NPM_1"
    }
  }
}
RUN --mount=type=secret,id=npm_auth,env=SZCI_TOKEN_NPM_1 szci npm prepare \
  && pnpm install \
  && rm -f /root/.npmrc

tsdocker fails before starting the build if a configured secret environment variable is absent or empty. Secret values are inherited by Docker from the process environment and never enter tsdocker's command string. BuildKit does not persist the mounted secret itself; if a build tool writes credentials to another file, remove that file in the same RUN instruction as shown above.

🗺️ Registry Repo Mapping

Use different repository names for different registries:

{
  "@git.zone/tsdocker": {
    "registries": ["registry.gitlab.com", "docker.io"],
    "registryRepoMap": {
      "registry.gitlab.com": "mygroup/myproject",
      "docker.io": "myuser/myproject"
    }
  }
}

When pushing, tsdocker maps the local repo name to the registry-specific path. For example, a locally built myproject:latest becomes registry.gitlab.com/mygroup/myproject:latest and docker.io/myuser/myproject:latest.

📋 Listing Dockerfiles

Inspect your project's Dockerfiles and their relationships:

tsdocker list

Output:

Discovered Dockerfiles:
========================

1. /path/to/Dockerfile_base
   Tag: myproject:base
   Base Image: node:20-alpine
   Version: base

2. /path/to/Dockerfile_app
   Tag: myproject:app
   Base Image: myproject:base
   Version: app
   Depends on: myproject:base

Examples

Minimal Build & Push

{
  "@git.zone/tsdocker": {
    "registries": ["docker.io"],
    "platforms": ["linux/amd64"]
  }
}
tsdocker push

Full Production Setup

{
  "@git.zone/tsdocker": {
    "registries": ["registry.gitlab.com", "ghcr.io", "docker.io"],
    "registryRepoMap": {
      "registry.gitlab.com": "myorg/myapp",
      "ghcr.io": "myorg/myapp",
      "docker.io": "myuser/myapp"
    },
    "buildArgEnvMap": {
      "NODE_VERSION": "NODE_VERSION"
    },
    "buildSecretEnvMap": {
      "npm_auth": "SZCI_TOKEN_NPM_1"
    },
    "platforms": ["linux/amd64", "linux/arm64"],
    "testDir": "./docker-tests"
  }
}

CI/CD Integration

GitLab CI:

build-and-push:
  stage: build
  script:
    - pnpm add --global @git.zone/tsdocker
    - tsdocker push
  variables:
    DOCKER_REGISTRY_1: 'registry.gitlab.com|$CI_REGISTRY_USER|$CI_REGISTRY_PASSWORD'

GitHub Actions:

- name: Build and Push
  run: |
    pnpm add --global @git.zone/tsdocker
    tsdocker login
    tsdocker push
  env:
    DOCKER_REGISTRY_1: 'ghcr.io|${{ github.actor }}|${{ secrets.GITHUB_TOKEN }}'

Gitea Actions:

- name: Build and Push
  run: |
    pnpm add --global @git.zone/tsdocker
    tsdocker push
  env:
    DOCKER_REGISTRY_1: 'gitea.example.com|${{ secrets.REGISTRY_USER }}|${{ secrets.REGISTRY_PASSWORD }}'

tsdocker auto-detects all three CI systems and enables session isolation automatically — no extra configuration needed.

For a release pipeline split across separate commands, keep the candidate identity stable and persist its local registry directory:

export TSDOCKER_SESSION_ID="$RELEASE_CANDIDATE_ID"
tsdocker build Dockerfile_##version##
tsdocker digest Dockerfile_##version## --json > candidate-digests.json

# Later, sequentially, in the same persisted workspace and with the same session ID:
EXPECTED_DIGEST="$(jq -r '.digests[0].digest' candidate-digests.json)"
tsdocker test --no-build Dockerfile_##version## --expected-digest="$EXPECTED_DIGEST"
tsdocker push --no-build Dockerfile_##version## --expected-digest="$EXPECTED_DIGEST"

TypeScript API

tsdocker can also be used programmatically:

import { TsDockerManager } from '@git.zone/tsdocker/dist_ts/classes.tsdockermanager.js';
import type { ITsDockerConfig } from '@git.zone/tsdocker/dist_ts/interfaces/index.js';

const config: ITsDockerConfig = {
  registries: ['docker.io'],
  platforms: ['linux/amd64', 'linux/arm64'],
};

const manager = new TsDockerManager(config);
await manager.prepare();
try {
  await manager.build({ parallel: true });
  await manager.push();
} finally {
  await manager.cleanup();
}

Environment Variables

CI & Session Control

Variable Description
TSDOCKER_SESSION_ID Override the auto-generated session ID (default: random 8-char hex)
TSDOCKER_REGISTRY_PORT Override the dynamically allocated local registry port
TSDOCKER_EXPECTED_DIGEST Fallback expected digest or comma/newline-separated clean-tag mappings
TSDOCKER_AUTO_CLEANUP Set false to skip optional local-only maintenance and abandoned-resource recovery; current registry, SSH, and remote Buildx cleanup plus all safety gates remain mandatory
TSDOCKER_CLEANUP_ON_START Set false to skip selected-builder startup pruning
TSDOCKER_REMOVE_CI_BUILDERS Set false to retain local-only CI builders; they are pruned only while autoCleanup remains enabled; remote topologies are always retired
TSDOCKER_DOCKER_TIMEOUT Per-command timeout for Docker work outside builds, Buildx setup, and cleanup (default: 600)
TSDOCKER_BUILD_TIMEOUT Per-image build timeout in seconds (default: 3600; --timeout takes precedence)
TSDOCKER_BUILDX_SETUP_TIMEOUT Per-command Buildx inspect, create, append, and bootstrap timeout in seconds (default: 600)
TSDOCKER_SSH_TIMEOUT SSH opening-verification timeout in seconds (default: 30); cleanup/recovery controls use the cleanup deadline
TSDOCKER_CLEANUP_TIMEOUT Deadline in seconds for each cleanup or abandoned-recovery scope (default: 120)
TSDOCKER_BUILDX_PRUNE_UNTIL Override the selected-builder cache age rule
TSDOCKER_BUILDX_PRUNE_MAX_USED_SPACE Override the selected-builder cache size rule
CI Generic CI detection (also GITHUB_ACTIONS, GITLAB_CI, GITEA_ACTIONS)

For Buildx cleanup, one TSDOCKER_CLEANUP_TIMEOUT value becomes an absolute, non-resetting deadline shared by setup-termination waiting, prune commands, removal retries, and every invocation-owned builder attempted in one end-of-run sweep.

Registry Credentials

Variable Description
DOCKER_REGISTRY_1 through DOCKER_REGISTRY_10 Pipe-delimited: registry|username|password
DOCKER_REGISTRY_URL Registry URL for single-registry setup
DOCKER_REGISTRY_USER Username for single-registry setup
DOCKER_REGISTRY_PASSWORD Password for single-registry setup

Requirements

  • Docker — Docker Engine 20+ or Docker Desktop
  • Node.js — Version 20.19+ on Node 20, version 22.12+ on Node 22, or version 23+
  • Docker Buildx — Required for multi-architecture builds (included in Docker Desktop)
  • Linux orchestration host — Required for registry-backed build, push, test, and digest workflows, and for native remote builders, so process ownership can be proven exactly

Troubleshooting

"docker not found"

Ensure Docker is installed and in your PATH:

docker --version

Multi-arch build fails

Make sure Docker Buildx is available. tsdocker will set up the builder automatically, but you can verify:

docker buildx version

Registry authentication fails

Check your environment variables are set correctly:

echo $DOCKER_REGISTRY_1
tsdocker login

tsdocker also falls back to ~/.docker/config.json — ensure you've run docker login for your target registries.

Push fails with "fetch failed"

General registry requests retry network and 5xx failures up to six times with exponential backoff; upload substeps use lower limits. If pushes still fail:

  • Check network connectivity to the target registry
  • Verify your credentials haven't expired
  • Look for retry log messages (fetch failed (attempt X/6)) to diagnose the pattern
  • Large layers may need longer timeouts — general blob requests use a five-minute bound

Circular dependency detected

Review your Dockerfiles' FROM statements — you have images depending on each other in a loop.

Build context too large

Use a .dockerignore file to exclude node_modules, .git, .nogit, and other large directories:

node_modules
.git
.nogit
dist_ts

Remote builder setup is blocked by an ownership record

Read the complete error before changing anything. For a Buildx creating tombstone, verify every exact local and remote container and volume printed by tsdocker is absent, then delete only the named record below .nogit/tsdocker-buildx-ownership/. For an SSH opening tombstone, verify that no SSH child or recorded control socket can still own a tunnel before deleting only that record below .nogit/tsdocker-ssh-ownership/. Never clear an ownership directory wholesale or delete a foreign, live, malformed, or otherwise unverifiable record to bypass the safety gate.

Registry or test-container cleanup is indeterminate

Do not remove a container by name. If the error includes a full Docker ID, inspect that exact ID and its complete git.zone.* label set. If create returned no trustworthy full ID, no deletion is authorized; enumerate full IDs by the canonical git.zone.* labels and verify ownership manually. Automatic recovery proceeds only when canonical same-project ownership and a dead process identity are proven. Persistent registry data below .nogit/docker-registry/ is not deleted by automatic container cleanup.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme
1.9 MiB
Languages
TypeScript 100%