Initium is a single binary that replaces ad-hoc bash scripts in Kubernetes initContainers. Use it when your pod needs to do any of these before the main container starts:
- Wait for a database or API to become reachable
- Run database migrations or seed data
- Render config files from templates
- Fetch secrets or config from an HTTP endpoint
- Run a setup script with structured logging
Add an Initium initContainer that targets the Postgres TCP port:
initContainers:
- name: wait-for-postgres
image: ghcr.io/kitstream/initium:latest
args: ["wait-for", "--target", "tcp://postgres:5432", "--timeout", "120s"]
securityContext:
runAsNonRoot: true
runAsUser: 65534
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]Initium retries with exponential backoff (default: up to 60 attempts, 1s initial delay, 30s max delay) until the connection succeeds or the timeout expires.
Use an http:// or https:// target instead of tcp://:
args: ["wait-for", "--target", "http://config-service:8080/healthz"]By default Initium expects HTTP 200. To accept a different status code:
args: ["wait-for", "--target", "http://api:8080/ready", "--http-status", "204"]Yes. Pass multiple --target flags. They are checked sequentially — all must succeed:
args:
- wait-for
- --target
- tcp://postgres:5432
- --target
- tcp://redis:6379
- --target
- http://config-service:8080/healthzIf any target fails to become reachable within the timeout, the initContainer exits with a non-zero code and the pod will not start.
Use the seed subcommand. It wraps your existing seed tool and forwards its exit code:
initContainers:
- name: seed-data
image: ghcr.io/kitstream/initium:latest
args: ["seed", "--", "/app/seed", "--file", "/seeds/initial.sql"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: urlEverything after -- is the command Initium will execute. Initium does not interpret those arguments — it passes them directly via execve.
Use the migrate subcommand. It works the same way as seed but is a separate subcommand so you can distinguish migration steps from seed steps in logs:
initContainers:
- name: migrate
image: ghcr.io/kitstream/initium:latest
args: ["migrate", "--", "flyway", "migrate"]
env:
- name: FLYWAY_URL
value: "jdbc:postgresql://postgres:5432/mydb"Use the render subcommand. Mount a ConfigMap containing your template and let Initium expand environment variables into the output:
initContainers:
- name: render-config
image: ghcr.io/kitstream/initium:latest
args:
- render
- --template
- /templates/app.conf.tmpl
- --output
- app.conf
- --workdir
- /work
env:
- name: DB_HOST
value: postgres
- name: DB_PORT
value: "5432"
volumeMounts:
- name: workdir
mountPath: /work
- name: templates
mountPath: /templates
readOnly: trueThe rendered file lands in /work/app.conf, which your main container can read from the shared workdir volume.
Add --json before the subcommand:
args: ["--json", "wait-for", "--target", "tcp://postgres:5432"]Output looks like:
{"time":"2026-01-15T10:30:00Z","level":"INFO","msg":"target is reachable","target":"tcp://postgres:5432","attempts":"1"}This is useful when you're shipping logs to a centralized system like Loki, Datadog, or Elasticsearch.
All retry parameters are flags on the wait-for subcommand:
| Flag | Default | What it does |
|---|---|---|
--max-attempts |
60 |
Total number of attempts before giving up |
--initial-delay |
1s |
Delay after the first failure |
--max-delay |
30s |
Upper bound on delay between retries |
--backoff-factor |
2.0 |
Multiplier applied to the delay after each attempt |
--jitter |
0.1 |
Random fraction (0.0–1.0) added to each delay to prevent thundering herd |
--timeout |
5m |
Hard deadline across all targets |
Example — fast retries with low jitter:
args:
- wait-for
- --target
- tcp://postgres:5432
- --max-attempts
- "10"
- --initial-delay
- "200ms"
- --max-delay
- "2s"
- --backoff-factor
- "1.5"
- --jitter
- "0.05"Initium exits with code 1 after exhausting all retry attempts or hitting the --timeout deadline. The last error is printed to stderr:
2026-01-15T10:32:00Z [ERROR] target not reachable target=tcp://postgres:5432 error=all 60 attempts failed, last error: tcp dial postgres:5432: dial tcp: connect: connection refused
Because the initContainer exits non-zero, Kubernetes will restart it according to the pod's restartPolicy (default: Always for Deployments).
Yes. It is a standalone static binary. Common non-Kubernetes uses:
# Docker Compose — wait for a dependency before starting
docker run --rm --network mynet ghcr.io/kitstream/initium:latest \
wait-for --target tcp://db:5432
# CI pipeline — gate a step on service readiness
./bin/initium wait-for --target http://localhost:8080/healthz --timeout 30sThe -- tells Initium where its own flags end and the wrapped command begins. Without it, Initium might try to interpret your tool's flags as its own:
# Correct — Initium sees "migrate" subcommand, then passes "flyway migrate" to execve
args: ["migrate", "--", "flyway", "migrate"]
# Wrong — Initium tries to parse "flyway" as a flag to the migrate subcommand
args: ["migrate", "flyway", "migrate"]This is the same convention used by kubectl, docker, and many other CLI tools.
No. Commands passed after -- are executed directly via the operating system's execve syscall. There is no /bin/sh -c wrapper. This means:
- Shell features like pipes (
|), redirects (>), globbing (*), and variable expansion ($VAR) will not work - This is intentional — it prevents shell injection attacks
- If you genuinely need shell features, wrap your script in a file and execute it:
args: ["exec", "--", "/bin/sh", "/scripts/setup.sh"]
No. Initium is designed for the most restrictive Kubernetes security posture:
securityContext:
runAsNonRoot: true
runAsUser: 65534 # nobody
runAsGroup: 65534
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
capabilities:
drop: [ALL]This satisfies the Kubernetes restricted Pod Security Standard. No special PSPs, ClusterRoles, or RBAC bindings are required.
No. Initium automatically redacts values for log keys matching common secret patterns: token, password, secret, auth, authorization, api_key, apikey. These appear as REDACTED in both text and JSON log output.
However, Initium cannot redact secrets that appear as part of a URL or arbitrary string. Avoid embedding credentials directly in target URLs — use environment variables and your application's own config parsing instead.
No. All file-writing operations (render, fetch) are constrained to the path specified by --workdir (default: /work). Initium rejects:
- Absolute target paths like
/etc/passwd - Path traversal like
../../etc/passwdorsub/../../etc/shadow
If a path escapes the workdir, Initium exits with an error and writes nothing.
No. Initium has no default outbound connections. Every network target must be explicitly provided via --target or --url flags. There is no telemetry, no update checker, no default phone-home behavior.
To prevent man-in-the-middle attacks. If you connect to https://vault:8200, Initium verifies the server's TLS certificate against the system CA bundle.
If you need to connect to a service with a self-signed certificate (common in dev/staging), explicitly opt in:
args: ["wait-for", "--target", "https://vault:8200/v1/sys/health", "--insecure-tls"]The --insecure-tls flag is intentionally verbose — it should stand out in code review.
A scratch base image contains zero OS packages, zero libraries, zero shells. This means:
- Zero CVEs from base image packages — nothing to scan, nothing to patch
- No shell for attackers to use if the container is compromised
- Tiny image — the final image is ~2MB (just the Rust binary + CA certificates)
The trade-off is that you cannot kubectl exec into the container for debugging. This is acceptable for initContainers, which run once and exit.
Release images include SBOM and provenance attestations. Verify with cosign:
cosign verify-attestation \
--type https://slsa.dev/provenance/v0.2 \
ghcr.io/kitstream/initium:0.1.0Yes. Initium runs with least privilege, makes no cluster API calls, and cannot access other namespaces, nodes, or the Kubernetes API. It only makes outbound TCP/HTTP connections to targets you explicitly configure.
There are three ways:
1. Reference the image directly in your pod spec (simplest):
initContainers:
- name: wait-for-db
image: ghcr.io/kitstream/initium:latest
args: ["wait-for", "--target", "tcp://postgres:5432"]2. Use the Helm chart (for templated deployments):
helm install my-app charts/initium \
--set sampleDeployment.enabled=true \
--set 'initContainers[0].name=wait-db' \
--set 'initContainers[0].command[0]=wait-for' \
--set 'initContainers[0].args[0]=--target' \
--set 'initContainers[0].args[1]=tcp://postgres:5432'3. Build from source:
git clone https://github.com/KitStream/initium.git
cd initium
make build
./bin/initium --helplatest— tracks the most recent release. Convenient but not reproducible.0.1.0(specific version) — pinned and reproducible. Recommended for production.
Always pin a specific version in production workloads to avoid unexpected behavior from image updates.
No. The Helm chart installs nothing by default (sampleDeployment.enabled: false). It provides templates and values for injecting Initium initContainers into your own deployments. It does not create CRDs, webhooks, ClusterRoles, or any cluster-scoped resources.
Use an emptyDir volume mounted at --workdir (default /work):
spec:
initContainers:
- name: render-config
image: ghcr.io/kitstream/initium:latest
args: ["render", "--template", "/templates/app.conf.tmpl", "--output", "app.conf", "--workdir", "/work"]
volumeMounts:
- name: workdir
mountPath: /work
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: workdir
mountPath: /work
readOnly: true
volumes:
- name: workdir
emptyDir: {}The initContainer writes to /work, and the main container reads from it.
Define multiple initContainers in order. Kubernetes runs them sequentially:
initContainers:
- name: wait-for-db
image: ghcr.io/kitstream/initium:latest
args: ["wait-for", "--target", "tcp://postgres:5432"]
securityContext: &initium-security
runAsNonRoot: true
runAsUser: 65534
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
- name: migrate
image: ghcr.io/kitstream/initium:latest
args: ["migrate", "--", "/app/migrate", "up"]
securityContext: *initium-security
- name: seed
image: ghcr.io/kitstream/initium:latest
args: ["seed", "--", "/app/seed", "--file", "/seeds/data.sql"]
securityContext: *initium-securityIf any step fails, the subsequent steps do not run and the pod stays in Init:Error.
Initium has no dependency on the Kubernetes API — it is just a binary that runs inside a container. It works on any Kubernetes version that supports initContainers (1.6+). The Helm chart uses standard apps/v1 APIs and works on Kubernetes 1.16+.
Yes. Pull the public image and push it to your registry:
docker pull ghcr.io/kitstream/initium:0.1.0
docker tag ghcr.io/kitstream/initium:0.1.0 registry.internal/initium:0.1.0
docker push registry.internal/initium:0.1.0Then reference your internal registry in the pod spec. If your registry requires authentication, configure an imagePullSecret as usual.
git clone https://github.com/KitStream/initium.git
cd initium
make build # produces target/release/initium
make test # runs all unit tests
make lint # runs cargo clippy + cargo fmt --check# Build for your local architecture
docker build -t initium:dev .
# Build multi-arch (requires Docker Buildx)
make docker-build VERSION=devCheck the initContainer logs:
kubectl logs <pod-name> -c <initcontainer-name>Common causes:
| Symptom | Likely cause | Fix |
|---|---|---|
target not reachable after all attempts |
Target service isn't running or DNS is wrong | Check the service/endpoint exists and is in the same namespace (or use FQDN) |
unsupported target scheme |
Missing tcp:// or http:// prefix |
Add the scheme: tcp://postgres:5432 not just postgres:5432 |
path traversal detected |
Output path tries to escape workdir | Use a relative path for --output |
context cancelled |
Overall --timeout was too short |
Increase --timeout or check why the target takes so long |
Yes. The container image is built for both linux/amd64 and linux/arm64. Kubernetes pulls the correct architecture automatically via the multi-arch manifest.