The first CI/CD Pipeline for Cosmos SDK.
Building and maintaining Docker images for blockchain nodes is repetitive
work: tracking upstream releases, writing multi-stage Dockerfiles, managing
cross-compilation for amd64 and arm64, pushing to a registry, and
keeping everything consistent across a fleet of chains. Teams usually end up
with a collection of bespoke shell scripts that diverge over time.
Dockermint replaces that with a single, uniform pipeline. Define a chain once in a TOML recipe file and Dockermint handles the rest — Dockerfile generation, cross-compilation via BuildKit, release polling, registry push, persistence, and notifications. Adding a new chain requires no code changes: only a new recipe file.
Who is Dockermint for? Infrastructure engineers and DevOps teams running Cosmos SDK validator nodes or RPC infrastructure who need reproducible, multi-architecture Docker images without maintaining a separate build system per chain.
Status: Phase 1 complete.
dockermint-cliis fully implemented and runnable. Daemon mode (Phase 2) and gRPC/RPC mode (Phase 3) are not yet implemented.
| Resource | URL |
|---|---|
| Main site | https://dockermint.io |
| Documentation | https://docs.dockermint.io/ |
| GitHub | https://github.com/Dockermint/dockermint |
| Related project | Pebblify |
Dockermint runs in three modes depending on your use case:
- CLI — one-shot build, locally or via a remote BuildKit endpoint. Errors cause an immediate dump, log, and exit. Implemented in Phase 1.
- Daemon — continuous polling for new GitHub releases. On error: log, notify, persist the failure, and continue polling. Phase 2 (not yet implemented).
- RPC — daemon with an optional gRPC server, accepting remote build requests from a CLI client. On error: log and return idle. Phase 3 (not yet implemented).
- Docker with the BuildKit plugin (
docker buildx version) - QEMU user-space emulation for multi-arch builds:
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - Rust toolchain (if building from source)
# Build both binaries with the default feature set (OpenSSL vendored)
cargo build --release
# Switch to rustls instead of OpenSSL
cargo build --release --no-default-features --features ssl-rustls
# Binaries produced:
# target/release/dockermint-cli (fully implemented)
# target/release/dockermint-daemon (stub — not yet implemented)dockermint-cli provides four subcommands.
Execute a build for one or more recipes.
dockermint-cli build --recipe <name|all> [OPTIONS]
| Flag | Type | Default | Description |
|---|---|---|---|
--recipe <name|all> |
string | required | Recipe name (e.g. cosmos-gaiad) or all |
--version <tag> |
string | latest release | Specific version tag to build |
--flavor KEY=VALUE |
repeatable | recipe default | Override a flavor dimension |
--push |
flag | off | Push to the registry after a successful build |
--force |
flag | off | Rebuild even if the tag already exists in the registry |
--platform <linux/amd64|linux/arm64> |
repeatable | all configured | Target platform(s) |
--dry-run |
flag | off | Resolve and print the Dockerfile; do not build |
--keep-builders |
flag | off | Keep BuildKit builder instances after the build |
--config <path> |
path | ./config.toml |
Path to config.toml |
--recipes-dir <path> |
path | from config | Path to the recipes directory |
--log-level <level> |
string | from config | Log level: trace, debug, info, warn, error |
When --recipe all is used, every discovered recipe is built in sequence.
Failures are recorded per-recipe and the process continues. The exit code is
the highest severity code among all builds.
When --version is omitted, the latest GitHub release for the recipe's
repository is fetched automatically.
--dry-run is offline: it resolves and prints the Dockerfile to stdout and
the resolved flavor summary to stderr, without contacting a Docker daemon or
the VCS. When --version is also omitted, a <latest> placeholder stands in
for the version tag.
When --flavor is repeated with the same key, the last value wins.
# Build the latest gaiad release for all configured platforms
dockermint-cli build --recipe cosmos-gaiad
# Build a specific version with PebbleDB
dockermint-cli build --recipe cosmos-gaiad --version v19.0.0 --flavor db_backend=pebbledb
# Build all recipes and push to the registry
dockermint-cli build --recipe all --push
# Dry-run: print the generated Dockerfile without building
dockermint-cli build --recipe cosmos-gaiad --dry-run
# Build for a single platform only
dockermint-cli build --recipe cosmos-gaiad --platform linux/amd64
# Use a custom config and recipes directory
dockermint-cli build --recipe cosmos-gaiad \
--config /etc/dockermint/config.toml \
--recipes-dir /etc/dockermint/recipesList all discovered recipes.
dockermint-cli list-recipes [--recipes-dir <path>] [--config <path>] [--format table|json]
Default output format is an aligned table. Use --format json for
machine-readable output.
List available flavor dimensions for a recipe.
dockermint-cli list-flavors --recipe <name> [--recipes-dir <path>] [--config <path>] [--format table|json]
Shows every declared flavor dimension with its available values and the currently resolved default.
Print version and build information.
dockermint-cli version
Outputs the binary name, crate version, git commit hash, target triple, and compiled feature flags.
Dockermint is configured via config.toml. All sections use strict parsing:
unknown keys are rejected at startup to catch typos. CLI arguments take
precedence over config.toml values.
Secrets must never appear in config.toml. Use .env for all sensitive
values (see Secrets below).
# ============================================================================
# [meta] — required; schema versioning
# ============================================================================
[meta]
config_version = 1 # Schema version of this file (required)
# ============================================================================
# [general] — cross-cutting operational settings
# ============================================================================
[general]
mode = "cli" # "cli" | "daemon"
log_level = "info" # "trace" | "debug" | "info" | "warn" | "error"
log_dir = "/var/log/dockermint" # Directory for rotated log files
recipes_dir = "./recipes" # Path to the recipes directory
# ============================================================================
# [daemon] — polling settings (daemon mode only; Phase 2)
# ============================================================================
[daemon]
poll_interval_secs = 300 # Global default: seconds between VCS polls
# Per-chain poll-interval override:
# [daemon.chains."cosmos-gaiad"]
# poll_interval_secs = 600
# ============================================================================
# [grpc] — gRPC server settings (RPC mode only; Phase 3)
# ============================================================================
[grpc]
enabled = false
listen_address = "0.0.0.0:50051"
auth_mode = "token" # "token" | "mtls" | "both"
# tls_cert_path = "/path/to/server.crt"
# tls_key_path = "/path/to/server.key"
# tls_ca_path = "/path/to/ca.crt"
# Token secret: GRPC_AUTH_TOKEN in .env
# ============================================================================
# [builder] — image build settings
# ============================================================================
[builder]
platforms = ["linux/amd64", "linux/arm64"] # Target build platforms
# docker_host = "unix:///var/run/docker.sock" # Empty = system default
# cleanup_builders = false # Destroy builders after build
# ============================================================================
# [registry] — OCI registry settings
# Credentials: REGISTRY_USER, REGISTRY_PASSWORD in .env
# ============================================================================
[registry]
url = "" # e.g. "ghcr.io/dockermint". Empty = Docker Hub.
# ============================================================================
# [notifier] — notification settings (daemon mode only; Phase 2)
# Secrets: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID in .env
# ============================================================================
[notifier]
enabled = false # Enable/disable build-status notifications
# ============================================================================
# [metrics] — Prometheus scrape endpoint
# ============================================================================
[metrics]
enabled = true
listen_address = "0.0.0.0:9100" # Prometheus scrape endpoint bind address
# ============================================================================
# [flavours] — flavor overrides
# Global overrides apply to every recipe.
# Per-recipe overrides live under [flavours.recipes."<recipe-stem>"].
# ============================================================================
[flavours]
# db_backend = "pebbledb" # global override example
# [flavours.recipes."cosmos-gaiad"]
# db_backend = "pebbledb"All sensitive values are loaded from .env (via dotenvy). A missing .env
file is not an error; only a malformed one is. Variables are optional at load
time; required-ness is enforced at startup based on which features are in use.
| Variable | Used by | Required when |
|---|---|---|
GH_PAT |
VCS scrapper | Authenticated GitHub API requests (raises rate limit from 60 to 5000 req/hr) |
GH_USER |
VCS scrapper | Paired with GH_PAT for authenticated requests |
REGISTRY_USER |
Registry push | --push flag is used |
REGISTRY_PASSWORD |
Registry push | --push flag is used |
TELEGRAM_BOT_TOKEN |
Notifier | [notifier] enabled = true (Phase 2) |
TELEGRAM_CHAT_ID |
Notifier | [notifier] enabled = true (Phase 2) |
GRPC_AUTH_TOKEN |
gRPC server | [grpc] auth_mode = "token" or "both" (Phase 3) |
Example .env file:
GH_PAT=ghp_xxxxxxxxxxxxxxxxxxxx
GH_USER=your-github-username
REGISTRY_USER=your-registry-user
REGISTRY_PASSWORD=your-registry-password| Code | Name | Condition |
|---|---|---|
| 0 | EXIT_SUCCESS |
Operation completed successfully |
| 1 | EXIT_GENERAL |
Unclassified error |
| 2 | EXIT_CONFIG |
Configuration file missing, invalid, or version mismatch |
| 3 | EXIT_RECIPE |
Recipe file missing, invalid, or incompatible flavors |
| 4 | EXIT_SYSTEM |
System prerequisites not met (Docker, BuildKit, disk, etc.) |
| 5 | EXIT_VCS |
VCS API error (auth, rate limit, network, version not found) |
| 6 | EXIT_BUILD |
Docker/BuildKit build failed |
| 7 | EXIT_PUSH |
Registry push failed (auth, network, manifest) |
| 8 | EXIT_STORE |
Database error (save, read, schema mismatch) |
| 9 | EXIT_NOTIFY |
Notification send failed |
| 10 | EXIT_INTERNAL |
Internal bug (unreachable code, invariant violation) |
For --recipe all, the exit code is the highest severity (numerically
greatest) code among all per-recipe builds. EXIT_SUCCESS (0) is returned
only when every build succeeded.
The default feature set activates all production modules with vendored OpenSSL for TLS. Modules are replaceable at compile time via Cargo feature flags.
ssl-openssl, vcs-github, builder-buildkit, registry-oci,
db-redb, notifier-telegram, metrics-prometheus
| Feature | Description |
|---|---|
ssl-openssl |
Vendored OpenSSL via reqwest/native-tls-vendored. Default. Builds OpenSSL from source; no system OpenSSL dependency. |
ssl-rustls |
rustls via reqwest/rustls. No OpenSSL; uses aws-lc-rs as crypto provider. |
To switch: cargo build --no-default-features --features ssl-rustls,...
| Feature | Module | Description |
|---|---|---|
vcs-github |
Scrapper | GitHub REST API client for tag/release discovery |
builder-buildkit |
Builder | BuildKit via docker buildx; manages per-platform builder instances |
registry-oci |
Push | OCI Distribution Spec HTTP client for authentication and image push |
db-redb |
Saver | Embedded redb key-value store for build state persistence |
notifier-telegram |
Notifier | Telegram Bot API notifications for build events |
metrics-prometheus |
Metrics | Prometheus text-exposition scrape endpoint |
| Feature | Description |
|---|---|
docker-integration-tests |
Enables integration tests that require a live Docker daemon. Not part of the default set. |
At least one backend in each category must be compiled in. The build will fail
with a clear compile_error! if a category has no active backend.
| Chain | Binary | Sidecars |
|---|---|---|
| Cosmos Hub | gaiad |
— |
| Axelar | axelard |
Tofnd, Vald |
| Fetch | fetchd |
— |
| Injective | injectived |
Peggo |
| Osmosis | osmosisd |
— |
| Chain | Binary | Recipe file |
|---|---|---|
| Cosmos Hub | gaiad |
recipes/cosmos-gaiad.toml |
| Kyve | kyved |
recipes/kyve-kyved.toml |
Recipes declare available and default flavors per dimension:
[flavours.available]
db_backend = ["goleveldb", "pebbledb"]
binary_type = ["dynamic", "static"]
running_env = ["alpine3.23", "resolute", "distroless"]
running_user = ["root", "custom", "dockermint"]
build_tags = ["netgo", "ledger", "muslc"]
[flavours.default]
db_backend = "goleveldb"
binary_type = "static"
running_env = "alpine3.23"
running_user = "root"
build_tags = ["netgo", "muslc"]The active flavor for any dimension is resolved in priority order:
CLI --flavor args > config.toml per-recipe > config.toml global > recipe defaults
Incompatible flavor combinations produce an error before the build starts.
dockermint-cli dockermint-daemon
(one-shot build) (polling + optional gRPC server)
| |
+----------+------------+
|
SHARED CORE PIPELINE
|
config -> checker -> recipe -> scrapper
|
push <- builder <- builder <- builder
(buildx) (Dockerfile) (template engine)
|
saver / notifier / metrics
CROSS-CUTTING: logger, commands
| Module | Responsibility |
|---|---|
config |
Load and merge config.toml, .env, and CLI args |
checker |
Verify Docker, BuildKit, disk, and network prerequisites |
recipe |
Parse TOML recipes, resolve flavors, validate compatibility |
scrapper |
GitHub API client: fetch tags and releases |
builder |
Template engine, BuildKit manager, Go recipe builder |
push |
OCI registry authentication and image push |
saver |
Build state persistence (RedB, embedded) |
notifier |
Build status notifications (default: Telegram) |
metrics |
Prometheus metrics server |
cli |
Clap-based CLI with subcommands and exit code mapping |
logger |
Structured logging with log rotation |
commands |
Shell command execution shared by all modules |
Dockerfile content is produced by a template engine that resolves two classes of variables from the recipe:
{{UPPERCASE}}— host variables injected by Dockermint (e.g.{{HOST_ARCH}},{{SEMVER_TAG}},{{CREATION_TIMESTAMP}},{{BUILD_TAGS_COMMA_SEP}}){{lowercase}}— build variables resolved at build time from recipe[variables], flavors, and profiles (e.g.{{golang_version}},{{db_backend}})
| Phase | Target | Scope | Status |
|---|---|---|---|
| 0 | N/A | Architecture specs (all modules) | Complete |
| 1 | v0.1.0 | CLI mode, 5 chains, BuildKit, OCI push | Complete |
| 2 | v0.2.0 | Daemon mode, persistence, metrics, notifier | Planned |
| 3 | v0.3.0 | gRPC server and authenticated CLI client | Planned |
| 4 | v1.0.0 | Chain expansion, C-FFI library, security audit | Planned |
See docs/ROADMAP.md for the full phase breakdown.
Dockermint follows a design-first engineering workflow. Every feature begins with an architecture spec that is reviewed and confirmed before any code is written. A GitHub issue is opened to track the work, code is implemented against the spec, and the change must pass the full test suite — including mutation testing — before a pull request is opened. Code review is required before merge. No step may be skipped.
See docs.dockermint.io/contributing
or docs/ in this repository for the full contribution guide.
Dockermint compiles and runs on all five toolchains:
| Target |
|---|
x86_64-unknown-linux-gnu |
x86_64-unknown-linux-musl |
aarch64-unknown-linux-gnu |
aarch64-unknown-linux-musl |
aarch64-apple-darwin |
Apache License, Version 2.0. See LICENSE.