Docker
This section provides a collection of pre-built Docker images and templates designed for various development and operational tasks.
Prerequisites
- Docker or Podman container runtime
- Access to GitHub Container Registry (ghcr.io) - public images require no authentication
- Kubernetes cluster (optional, for backup images with CronJob examples)
Utils Images
| Image | Description | Status | Dockerfiles | Versions |
|---|---|---|---|---|
ghcr.io/this-is-tobi/tools/act-runner:latest | Act runner for running GitHub Actions workflows locally (ubuntu based) | Active | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/backup:latest | Unified backup utility for MariaDB, MongoDB, PostgreSQL, etcd, Vault, Qdrant and S3 using rclone streaming (alpine based) | Active | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/curl:latest | Lightweight image with curl, wget, jq, yq and openssl (alpine based) | Active | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/debug:latest | Debug container with networking and troubleshooting tools (debian based) | Active | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/dev:latest | Development container with common development tools (debian based) | Active | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/dev-lite:latest | Development container with common development tools (lite version, debian based) | Active | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/gh-runner:latest | Self-hosted GitHub Actions runner with common packages (ubuntu based) | Active | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/gh-runner-gpu:latest | Self-hosted GitHub Actions runner with GPU support (ubuntu based) | Active | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/mc:latest | MinIO Client for S3-compatible storage operations (alpine based) | Source removed | - | Versions |
ghcr.io/this-is-tobi/tools/pg-backup:latest | PostgreSQL backup utility with S3 support (postgres based) | Deprecated (Legacy) | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/s3-backup:latest | S3 bucket sync and backup utility (debian based) | Deprecated (Legacy) | Dockerfile | Versions |
ghcr.io/this-is-tobi/tools/vault-backup:latest | HashiCorp Vault backup utility with S3 support (vault based) | Deprecated (Legacy) | Dockerfile | Versions |
Status Legend:
- Active: Currently maintained and recommended for use
- Legacy: Still functional but superseded by newer images (consider migrating to
backupimage) - Deprecated: Not recommended for new deployments, will be removed in future versions
- Source removed: Image still available in registry but source code removed from repository
TIP
This table intentionally never lists a specific version number - every image reference uses :latest, and the Versions column links straight to each image's live tag list on ghcr.io instead. A hardcoded version column would drift the moment any image released again; this doesn't.
TIP
The backup images are supplied with a sample kubernetes cronjob in their respective folders.
Usage Examples
Development Images
# Debug container
docker run -it ghcr.io/this-is-tobi/tools/debug:latest
# Development environment
docker run -it -v $(pwd):/workspace -w /workspace ghcr.io/this-is-tobi/tools/dev:latestBackup Images
The new unified backup image supports MariaDB, MongoDB, PostgreSQL, etcd, Vault, Qdrant, and S3-to-S3 backups with streaming:
# PostgreSQL backup to S3
docker run --rm \
-e DB_HOST=<host> \
-e DB_PORT=5432 \
-e DB_NAME=<database> \
-e DB_USER=<user> \
-e DB_PASS=<password> \
-e S3_ENDPOINT=<s3-endpoint> \
-e S3_ACCESS_KEY=<access-key> \
-e S3_SECRET_KEY=<secret-key> \
-e S3_BUCKET_NAME=<bucket> \
ghcr.io/this-is-tobi/tools/backup:latest \
/home/alpine/scripts/postgres-backup.sh
# MariaDB backup to S3
docker run --rm \
-e DB_HOST=<host> \
-e DB_PORT=3306 \
-e DB_NAME=<database> \
-e DB_USER=<user> \
-e DB_PASS=<password> \
-e S3_ENDPOINT=<s3-endpoint> \
-e S3_ACCESS_KEY=<access-key> \
-e S3_SECRET_KEY=<secret-key> \
-e S3_BUCKET_NAME=<bucket> \
ghcr.io/this-is-tobi/tools/backup:latest \
/home/alpine/scripts/mariadb-backup.sh
# MongoDB backup to S3
docker run --rm \
-e DB_HOST=<host> \
-e DB_PORT=27017 \
-e DB_NAME=<database> \
-e DB_USER=<user> \
-e DB_PASS=<password> \
-e S3_ENDPOINT=<s3-endpoint> \
-e S3_ACCESS_KEY=<access-key> \
-e S3_SECRET_KEY=<secret-key> \
-e S3_BUCKET_NAME=<bucket> \
ghcr.io/this-is-tobi/tools/backup:latest \
/home/alpine/scripts/mongodb-backup.sh
# etcd backup to S3
docker run --rm \
-e ETCD_ENDPOINTS=https://<host>:2379 \
-e ETCD_CACERT=/certs/ca.crt \
-e ETCD_CERT=/certs/client.crt \
-e ETCD_KEY=/certs/client.key \
-e S3_ENDPOINT=<s3-endpoint> \
-e S3_ACCESS_KEY=<access-key> \
-e S3_SECRET_KEY=<secret-key> \
-e S3_BUCKET_NAME=<bucket> \
-v /path/to/certs:/certs:ro \
ghcr.io/this-is-tobi/tools/backup:latest \
/home/alpine/scripts/etcd-backup.sh
# Vault backup to S3
docker run --rm \
-e VAULT_ADDR=<vault-address> \
-e VAULT_TOKEN=<token> \
-e S3_ENDPOINT=<s3-endpoint> \
-e S3_ACCESS_KEY=<access-key> \
-e S3_SECRET_KEY=<secret-key> \
-e S3_BUCKET_NAME=<bucket> \
ghcr.io/this-is-tobi/tools/backup:latest \
/home/alpine/scripts/vault-backup.sh
# Qdrant backup to S3
docker run --rm \
-e QDRANT_URL=<qdrant-url> \
-e QDRANT_COLLECTION=<collection-name> \
-e QDRANT_API_KEY=<api-key> \
-e S3_ENDPOINT=<s3-endpoint> \
-e S3_ACCESS_KEY=<access-key> \
-e S3_SECRET_KEY=<secret-key> \
-e S3_BUCKET_NAME=<bucket> \
ghcr.io/this-is-tobi/tools/backup:latest \
/home/alpine/scripts/qdrant-backup.sh
# S3-to-S3 sync
docker run --rm \
-e SOURCE_S3_ENDPOINT=<source-endpoint> \
-e SOURCE_S3_ACCESS_KEY=<source-key> \
-e SOURCE_S3_SECRET_KEY=<source-secret> \
-e SOURCE_S3_BUCKET_NAME=<source-bucket> \
-e S3_ENDPOINT=<target-endpoint> \
-e S3_ACCESS_KEY=<target-key> \
-e S3_SECRET_KEY=<target-secret> \
-e S3_BUCKET_NAME=<target-bucket> \
ghcr.io/this-is-tobi/tools/backup:latest \
/home/alpine/scripts/s3-backup.shNOTE
Legacy backup images (pg-backup, vault-backup, s3-backup) are still available but consider migrating to the unified backup image.
Utility Images
# MinIO Client for S3 operations (deprecated - use backup image instead)
docker run -it ghcr.io/this-is-tobi/tools/mc:latest \
mc alias set myminio <endpoint> <access-key> <secret-key>Building Images Locally
# Clone repository
git clone https://github.com/this-is-tobi/tools.git
cd tools/docker/utils/<image-name>
# Build image
docker build -t my-custom-image:latest .
# Multi-architecture build
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 \
-t myregistry/image:latest --push .Release & Build Automation
Active images (the deprecated images below use static, hand-maintained tags and are excluded from all of this) are versioned and rebuilt automatically. Nothing about publishing a new image version normally requires a manual step.
End-to-end flow:
- A base image gets a new version, or a month goes by → a commit lands on
mainscoped to one image's directory. cd.ymlrunsrelease-pleaseon every push tomain. For each image directory with unreleasedfix/feat/breaking commits since its last release, it opens (or updates) an independent release PR bumping that image's version, updating itsCHANGELOG.md, and bumping its entry in.release-please-manifest.json.- Merging a release PR creates a git tag
<image-name>-v<version>(e.g.curl-v2.0.4). cd.yml'strigger-buildjob then explicitly dispatchesbuild-images.ymlviaworkflow_dispatch(with noIMAGESfilter, i.e. "check every active image"). This is deliberate, not incidental:release-please-actioncreates its tags using the defaultGITHUB_TOKEN, and GitHub's anti-recursion guard meanspush:-triggered workflows (likebuild-images.yml's owntags:trigger) never fire for events authored byGITHUB_TOKEN—workflow_dispatchis explicitly exempt from that restriction, which is whycd.ymldispatches it directly instead of relying on the tag push itself. Dispatching unconditionally for every active image is safe and cheap:build-images.yml's own skip-if-already-published guard (docker manifest inspect) makes it a no-op for every image whose manifest-declared version is already published, so in practice only the image(s) that actually just got a new version end up building.build-images.ymllooks up each candidate image's build metadata (context, Dockerfile, target) inci/matrix.json, resolves its version from.release-please-manifest.json, and builds/pushes the ones that aren't already published (multi-arch, plus SBOM + provenance attestations), via the sharedthis-is-tobi/github-workflowsreusable workflows.
NOTE
Discovered this GITHUB_TOKEN gap for real on 2026-07-17: act-runner-v2.0.5's tag was pushed by release-please but build-images.yml never ran (confirmed via gh run list showing zero runs, ever) until cd.yml's trigger-build job was added. If a future change to github-workflows' release-app.yml starts accepting a PAT for the release-please-action token: input instead of GITHUB_TOKEN, the native tag trigger would start working too and this dispatch step would become redundant (but harmless to leave in place, given the no-op guard).
Each image versions and releases independently — bumping curl never touches debug's version or triggers its rebuild.
Base image updates
Renovate (renovate.json) watches the ARG BASE_IMAGE=... default in every active Dockerfile (this is the single source of truth for the base image — ci/matrix.json no longer duplicates it) and opens a PR per outdated base, commit-scoped as fix(...) so release-please treats it as a patch release for that image. Deprecated images are excluded from Renovate entirely.
Scheduled dependency refresh
None of these Dockerfiles pin apt/apk package versions, so a base image bump alone won't catch newer package versions between base image releases. refresh-images.yml runs monthly, touches a .refresh marker file inside each active image's directory, and pushes one fix(docker): scheduled dependency refresh commit. That's a real, path-scoped commit, so release-please cuts a genuine patch release from it — this is the mechanism that keeps floating packages current without hand-tracking every dependency. Trigger it manually via workflow_dispatch with a comma-separated IMAGES input to refresh specific images on demand.
Versioning (release-please)
release-please-config.json + .release-please-manifest.json define one release-please "package" per active image directory (docker/utils/<name>), each with:
component: the image name, used to build the<name>-v<version>tag.initial-version: where that image's version counter starts.bootstrap-sha: the commit this system was introduced at — release-please only considers commits after this SHA for that path. This repo had real unreleasedfeat:/fix:history predating this pipeline; withoutbootstrap-sharelease-please would walk that entire history on its first run and could bump several images unexpectedly. Don't remove this field unless you specifically want release-please to re-scan full history for a package.
ci/matrix.json deliberately has no tag field for actively-released images, and release-please-config.json deliberately has no extra-files pointing back into it. build-images.yml resolves each active image's version straight from .release-please-manifest.json at build time instead (deprecated images, which aren't release-please-managed, keep a static hand-set tag in matrix.json as their only source).
WARNING
Don't add an extra-files entry targeting ci/matrix.json (or any other root-level JSON array file). Release-please's built-in JSON extra-files updater assumes the target file's root is an object — its format-preserving stringifier slices the content before the first { and after the last } (json-stringify.ts). Pointed at an array-rooted file, this corrupts it into a doubly-nested array ([[...]]) on every patch, silently breaking every jq '.[] | ...' consumer. Hit this for real on 2026-07-16 (PR #11, act-runner 2.0.5) — root-caused by reading release-please's source directly, not guessed. If a future release-please version fixes this upstream, it'd be safe to reintroduce extra-files here, but verify against a real generated PR diff first.
Release PRs are not auto-merged (AUTOMERGE_RELEASE: false in cd.yml) — review and merge them like any other PR. This matches the convention used in the github-workflows repo itself.
Adding a new image
- Add its Dockerfile under
docker/utils/<name>/withARG BASE_IMAGE=...+FROM ${BASE_IMAGE}(needed for Renovate to detect it). - Add an entry to
ci/matrix.json(name,description,build.context,build.dockerfile,build.target,build.latest) — nobuild.tagfield, that's resolved at build time from the manifest. - Add a matching package to
release-please-config.json(component,initial-version= its starting version,bootstrap-sha= currentHEAD— noextra-files, see the warning above) and a matching entry to.release-please-manifest.json(same starting version). - Add its row to the table above and to
docs/04-docker.md's docs.
NOTE
If an image shares a directory with another (like dev/dev-lite used to), release-please can't version them independently — a change to either one's Dockerfile bumps both. Give each image its own directory unless you deliberately want lockstep versioning.
Deprecating an image
Set "deprecated": true on its ci/matrix.json entry. This excludes it from Renovate, from the scheduled refresh, and from release-please-config.json/the manifest (remove its package if present) — it keeps whatever tag it has and is no longer auto-released. It remains buildable via build-images.yml's manual workflow_dispatch (explicit IMAGES input bypasses the deprecated filter, so you can still force a rebuild — e.g. to patch a CVE — before removing it entirely).
Manual rebuilds
build-images.yml also accepts workflow_dispatch with an optional comma-separated IMAGES input (leave empty to rebuild every active image). Useful for forcing a rebuild without waiting on a release-please PR.
PR build checks
ci.yml verifies buildability before anything merges, not just commit message format. On every non-draft PR it diffs changed files against ci/matrix.json's build.context fields and builds (AMD64 only, no attestation) just the images actually touched, tagged <image>:pr-<number> and pushed to ghcr.io — a PR touching only docs or a single Dockerfile builds nothing or exactly that one image. docker/templates/** changes get a local docker build for both the dev and prod stages instead (no push — templates were never published by this pipeline, they're copy-paste starting points). PR-tagged images are deleted from ghcr.io once the PR closes (merged or not), via the shared clean-cache.yml workflow.
NOTE
PRs from forks won't be able to push the check image — GITHUB_TOKEN is read-only for pull_request events from forks, which GitHub enforces regardless of the permissions: requested in the workflow. Not an issue for same-repo branches.
Known limitations
- No cosign keyless signing. The old pipeline signed every published tag with
cosign sign; the sharedbuild-docker.ymlworkflow's built-in attestation step only forwards SBOM and SLSA provenance, not signing. SBOM/provenance attestations are still generated. - No automated image testing yet. Builds aren't smoke-tested before being tagged
latest. Options were scoped (post-publish smoke test, a true pre-push gate, or aTEST_COMMANDhook proposed upstream inbuild-docker.yml) but deferred.
Template Images
Pre-configured Docker image templates that can be customized for specific use cases.
| Name | Description |
|---|---|
| nginx | Bun dev/build + rootless nginx SPA prod image with runtime env substitution, hardened for restricted environments (OpenShift-compatible). |
| bun | Bun dev/build/prod multi-stage image for APIs, hardened for restricted environments (OpenShift-compatible). |
nginx (frontend/SPA)
Expects a package.json with a Vite-style dev script and a build script producing dist/, and a committed bun.lock. Adjust the dev/build commands in the Dockerfile if your app's toolchain differs.
Usage:
# Copy the whole template (Dockerfile + conf + entrypoint) next to your app
curl -fsSL "https://raw.githubusercontent.com/this-is-tobi/tools/main/docker/templates/nginx/Dockerfile" -o Dockerfile
curl -fsSL "https://raw.githubusercontent.com/this-is-tobi/tools/main/docker/templates/nginx/default.conf.template" -o default.conf.template
curl -fsSL "https://raw.githubusercontent.com/this-is-tobi/tools/main/docker/templates/nginx/entrypoint.sh" -o entrypoint.sh
# Dev image, meant to be run with your source bind-mounted over /app for hot reload
docker build --target dev -t my-frontend:dev .
docker run -p 5173:5173 -v "$(pwd):/app" my-frontend:dev
# Production image (build + prod stages, served by nginx)
docker build --target prod -t my-frontend:latest .
docker run -p 8080:8080 -e SERVER=my-backend:3000 my-frontend:latestNotes:
- Three stages:
dev(Vite/similar dev server viabun run dev -- --host),build(bun run build->dist),prod(served by rootless nginx). SERVERsets the/apireverse-proxy upstream (host:port). It defaults to a harmless loopback placeholder so the container still starts if you don't use/api.- To inject runtime env vars into built JS files (values not baked in at build time), set
VARIABLES="MY_VAR OTHER_VAR"plus the correspondingMY_VAR=...env vars atdocker runtime — see the comments inentrypoint.sh. - The prod image runs as a non-root user with group
0, and all files it needs to read/write are group-owned and group-writable, so it works unmodified under OpenShift's restricted SCC (arbitrary UID, GID0). - For a
readOnlyRootFilesystem: truesecurity context, mount writableemptyDirvolumes at/tmpand/etc/nginx/conf.d(nginx needs to write its pid/temp files and the templated config at startup).
bun (API)
Expects a package.json with a build script (e.g. bun build ./src/index.ts --outdir dist --target bun) producing dist/index.js, and a committed bun.lock. Adjust the entrypoints at the top of the Dockerfile if your app's layout differs.
Usage:
curl -fsSL "https://raw.githubusercontent.com/this-is-tobi/tools/main/docker/templates/bun/Dockerfile" -o Dockerfile
curl -fsSL "https://raw.githubusercontent.com/this-is-tobi/tools/main/docker/templates/bun/.dockerignore" -o .dockerignore
# Dev image, meant to be run with your source bind-mounted over /app for hot reload
docker build --target dev -t my-api:dev .
docker run -p 3000:3000 -v "$(pwd):/app" my-api:dev
# Production image (build + prod stages)
docker build --target prod -t my-api:latest .
docker run -p 3000:3000 my-api:latestNotes:
- Three stages:
dev(hot reload viabun --watch),build(bundles and prunes to production-only dependencies),prod(minimaldistrolessruntime, no shell/package manager). - The prod image runs as a non-root user with group
0(OpenShift restricted SCC compatible) and needs no writable volumes even underreadOnlyRootFilesystem: true. - The
distrolessprod image has no shell orwget/curl, so there's no DockerHEALTHCHECK; wire an HTTP liveness/readiness probe (e.g./healthz) at the orchestrator level instead. NODE_ENVis set toproductionbefore thebuildstage runs, not just at runtime, since bundlers inlineprocess.env.NODE_ENVat build time.
Troubleshooting
Image Pull Issues
# Login to GHCR if needed
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
# Verify image exists
# https://github.com/this-is-tobi/tools/pkgs/container/toolsRuntime Issues
Permission errors:
- Most images run as non-root user
- Check volume mount permissions
- Use:
docker run --user $(id -u):$(id -g)
Out of memory:
- Increase Docker memory limits
- Check with:
docker stats
Backup Image Issues
Connection failures:
- Verify endpoints are reachable from the container
- Check credentials are correct (watch for obfuscated values in logs)
- Ensure TLS/SSL certificates are valid
Database connection failed:
- Verify host, port, and credentials
- Check firewall rules allow container network access
- Test network connectivity:
docker run --rm ghcr.io/this-is-tobi/tools/debug:latest ping <host>
S3 upload failures:
- Verify S3 endpoint is accessible
- Check bucket permissions and policies
- For large databases (>48GB), add
RCLONE_EXTRA_ARGS="--s3-chunk-size 128Mi" - Check available disk space if not using streaming mode
Local destination (LOCAL_PATH) usage:
- Mount a host directory into the container and set
LOCAL_PATHto its path - S3 env vars (
S3_ENDPOINT,S3_ACCESS_KEY, etc.) are not required whenLOCAL_PATHis set - Useful for testing against port-forwarded Kubernetes services or air-gapped environments:sh
kubectl port-forward svc/postgres 5432:5432 & docker run --rm \ --network host \ -v /local/backups:/backups \ -e DB_HOST=localhost \ -e DB_PORT=5432 \ -e DB_NAME=mydb \ -e DB_USER=user \ -e DB_PASS=password \ -e LOCAL_PATH=/backups \ ghcr.io/this-is-tobi/tools/backup:latest \ /home/alpine/scripts/postgres-backup.sh