Loading repository data…
Loading repository data…
AndriyKalashnykov / repository
Ubuntu dev-container images with a reproducible DevOps toolchain — kubectl, helm, k9s, kind, flux; base plus Java 25 and Go variants, every CLI mise-pinned
A layered set of Ubuntu-based development-environment Docker images. A common base image ships a broad cloud-native / DevOps toolchain (kubectl, helm, kustomize, k9s, kind, flux, conftest, opa, Docker-in-Docker, …); the java and go variants build on it with their respective language stacks. Every tool version is pinned and reproducible via mise — see TOOLING.md.
Images are hadolint-linted, Trivy-scanned (filesystem + image), cosign keyless-signed with SBOM attestation, and published to GHCR through a Renovate-managed GitHub Actions pipeline.
$ make help
Usage: make COMMAND
Commands :
help - List available tasks on this project
check-env - Check that docker is installed
login - Log in to GHCR (uses GITHUB_PAT with write:packages)
lint - Lint Dockerfiles (hadolint) + verify shell scripts are executable
lint-scripts-exec - Fail if any committed shell script lacks the +x bit
ci - Run local CI checks (Dockerfile lint + build the base image)
renovate-validate - Validate renovate.json against the Renovate schema
build-base-inline-cache - Build remote cache for the base image
build-base - Build base image
run-base - Run base image
push-base - Push base image to a registry
delete-base - Delete base image locally
build-java-inline-cache - Build remote cache for the java dev image
build-java - Build java dev image
run-java - Run java dev image
push-java - Push java dev image to a registry
delete-java - Delete java dev image locally
build-go - Build go dev image (no secrets — image is credential-free)
run-go - Run go dev image (DooD socket + runtime-injected SSH/GPG/PAT creds)
push-go - Push go dev image (BLOCKED by default — local-only by policy; set ALLOW_GO_PUSH=1)
delete-go - Delete go dev image locally
build-all - Build base + go + java images
build-push-all - Build and push the publishable images (base + java; go is local-only)
cleanup - Cleanup docker images, containers, volumes, networks, build cache
ubuntu:26.04 (LTS)
└─ docker-ubuntu-base base + DevOps toolchain (mise-managed) + Docker-in-Docker
├─ docker-ubuntu-java + Java 25 LTS / Maven / Gradle
└─ docker-ubuntu-go + Go toolchain & dev CLIs, kubebuilder, goreleaser,
GPG, PostgreSQL
The java and go images are built FROM ghcr.io/andriykalashnykov/docker-ubuntu-base,
so make build-java / make build-go depend on make build-base.
All CLI tools are managed by mise and pinned in
per-image .mise.toml files (base/.mise.toml, java/.mise.toml,
go/.mise.toml); the base OS (tag + digest) and mise itself are pinned via
Dockerfile ARGs. Everything is kept current automatically by Renovate.
The full strategy and the list of what is/ isn't mise-managed is documented in
TOOLING.md.
| Requirement | Notes |
|---|---|
| Docker with BuildKit | DOCKER_BUILDKIT=1 |
| GNU Make | drives all build/run/push targets |
GITHUB_PAT (write:packages) | only for make login / push-* to GHCR |
All three images build with no host secrets — the filesystem is credential-free.
For the go image, the operator's credentials are injected at container start
by make run-go (never baked into the image): an SSH key pair at
~/.ssh/id_rsa{,.pub} or ~/.ssh/id_ed25519{,.pub}, a GPG secret key + ownertrust
at $GPG_SECRET_KEY_FILE / $GPG_OWNERTRUST_FILE, and the GITHUB_PAT /
GPG_PASSPHRASE env vars — each used only if present (otherwise a fresh SSH key is
generated and GPG/PAT setup is skipped). The GPG_* defaults point at the owner's
dotfiles; set them to your own exported files (see the example below).
export GITHUB_PAT=<github-pat-with-write:packages> # for `make login` / push to GHCR
make build-base && make run-base # base image
make build-java && make run-java # java dev image
make build-go && make run-go # go dev image
run-* drops you into an interactive bash shell with the full toolchain on
PATH (and JAVA_HOME / GOROOT set by the entrypoint). run-go additionally
bind-mounts the host Docker socket (Docker-out-of-Docker) and the current directory,
and injects your SSH/GPG keys + PAT at container start (if present on the host).
The go image is credential-free; make run-go injects your credentials at
container start via read-only bind-mounts and env-by-name — no secret value ever
touches docker's argv, and nothing is baked into the image. Provide whichever you
need; each is optional and used only if present:
# SSH key — auto-detected from ~/.ssh/id_rsa{,.pub} or ~/.ssh/id_ed25519{,.pub}
# (read-only bind-mount; if absent, a fresh per-container key is generated)
# GitHub PAT — written to ~/.netrc inside the container for git-over-HTTPS auth.
# Passed by env NAME only (the value never appears in argv).
export GITHUB_PAT=<github-pat-with-write:packages>
# GPG signing key — point these at YOUR exported secret key + ownertrust files
# (read-only bind-mounts); the passphrase is passed by env NAME only.
gpg --export-secret-keys --armor KEYID > ~/gpg-secret.key
gpg --export-ownertrust > ~/gpg-ownertrust.txt
export GPG_SECRET_KEY_FILE=~/gpg-secret.key
export GPG_OWNERTRUST_FILE=~/gpg-ownertrust.txt
export GPG_PASSPHRASE=<your-gpg-key-passphrase>
make build-go && make run-go # build (credential-free), then run with creds injected
Inside the container your SSH key is at ~/.ssh/, your PAT is in ~/.netrc (mode
0600), and your GPG key is imported into ~/.gnupg — ready for git clone over
HTTPS and signed git commit -S.
Never pass these as
docker build --build-arg/ values on the command line — that bakes the secret into the image history and exposes it viaps/docker history. The runtime-injection flow above is the only supported path.
The base and java images are published to GHCR — pull and run directly:
docker run -it --rm ghcr.io/andriykalashnykov/docker-ubuntu-base:26.04 bash
docker run -it --rm ghcr.io/andriykalashnykov/docker-ubuntu-java:26.04 bash
Mount your project and run the toolchain. The entrypoint activates mise, so
java/mvn/gradle/go and JAVA_HOME/GOROOT are ready:
# Java (image ships Java 25 LTS, Maven 3.9, Gradle 9)
docker run --rm -it -v "$PWD":/home/user/app -w /home/user/app \
ghcr.io/andriykalashnykov/docker-ubuntu-java:26.04 mvn -B package
docker run --rm -it -v "$PWD":/home/user/app -w /home/user/app \
ghcr.io/andriykalashnykov/docker-ubuntu-java:26.04 gradle build
# Go (image ships Go 1.26 + dlv, goreleaser, swag, goose, …) — build go locally first
make build-go
docker run --rm -it -v "$PWD":/home/user/app -w /home/user/app \
ghcr.io/andriykalashnykov/docker-ubuntu-go:26.04 go build ./...
docker run --rm -it -v "$PWD":/home/user/app -w /home/user/app \
ghcr.io/andriykalashnykov/docker-ubuntu-go:26.04 go test ./...
# …or open an interactive shell with your project mounted and work normally:
docker run --rm -it -v "$PWD":/home/user/app -w /home/user/app \
ghcr.io/andriykalashnykov/docker-ubuntu-java:26.04 bash
The go image is built locally only (
make build-go) — a personal dev container that is local-only by policy (the image itself is credential-free; SSH/GPG/PAT are injected at run time), so build it before running it.
Run make help for the authoritative list.
| Target | Description |
|---|---|
help | List available targets |
check-env | Check required env vars and installed tools |
login | Log in to GHCR (GITHUB_PAT via --password-stdin) |
lint | Lint all Dockerfiles with hadolint |
ci | Run local CI checks (lint + build-base) |
renovate-validate | Validate renovate.json against the Renovate schema |
build-base / run-base / push-base / delete-base | Base image lifecycle |
build-java / run-java / push-java / delete-java | Java image lifecycle |
build-go / run-go / delete-go | Go image lifecycle (built locally only) |
push-go | Push go image — blocked unless ALLOW_GO_PUSH=1 (local-only by policy; the image is credential-free) |
build-base-inline-cache / build-java-inline-cache | Build & push the registry inline cache |
build-all | Build base + go + java |
build-push-all | Build & push the publishable images (base + java; go excluded) |
cleanup | Remove all images/layers/containers/volumes and prune build cache |
| Variable | Purpose |
|---|---|
GITHUB_PAT | GHCR auth (write:packages) for login/push-*; also injected into the go image .netrc at run time |
GPG_PASSPHRASE | GPG key passphrase for the go image (passed to make run-go at run time) |
GPG_SECRET_KEY_FILE | Path to your exported GPG secret key for the go image (gpg --export-secret-keys); default is the owner's dotfiles file |
GPG_OWNERTRUST_FILE | Path to your exported GPG ownertrust for the go image (gpg --export-ownertrust); default is the owner's dotfiles file |
IMAGE_REGISTRY | Override the publish registry (default ghcr.io) |
CI (main.yml) — builds, scans, signs and
publishes the base + java images to GHCR. Pipeline: changes
(paths-filter) → static-check (hadolint + Trivy filesystem scan) → build
(build → Trivy image scan → tag-gated push (:26.04 + :latest + :sha-…) →
cosign keyless signing by digest → SBOM generation) → ci-pass (single required
status check). PRs build and scan but do not push/sign. The go image is
built locally only (a personal dev container; local-only by policy).cleanup-runs.yml)
— weekly prune of old workflow runs via the gh CLI.Dependency updates are handled by Renovate (renovate.json): the Ubuntu base
tag (+ pinned digest), GitHub Action SHAs, the mise ARG, and every mise-pinned tool.
Images are signed keyless via cosign (Sigstore OIDC). Verify with:
cosign verify ghcr.io/andriykalashnykov/docker-ubuntu-base:26.04 \
--certificate-identity-regexp 'https://github.com/AndriyKalashnykov/docker-ubuntu/.github/workflows/main.yml@refs/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com