Configuring Images
Configurable images let you build one container image and customize it per host at deploy time. Instead of baking configuration into each image, you include template files that are rendered with host-specific values when you create a release.
Concepts
A configurable image has three parts:
- Template files (
.khtmpl) — files with Go template syntax that are rendered per host - Schema (
schema.json, optional) — a JSON Schema that validates configuration values - Starter (
starter.json, optional) — an example configuration to help users get started
When you create a release for a host, you provide a JSON config file. The templates are rendered with your config values and the result is encrypted with the host's public key. The host pulls the release and decrypts the config locally.
Creating a configurable image
1. Add template files
Any file with a .khtmpl extension is treated as a template. Templates use Go's text/template syntax. When rendered, the .khtmpl suffix is stripped from the filename.
For example, /etc/caddy/Caddyfile.khtmpl in the image becomes /etc/caddy/Caddyfile on the host after rendering.
Caddyfile.khtmpl:
:{{ .Port }} {
respond "Hello from {{ .Hostname }}"
}
Templates have access to all keys in your config JSON. If your config file is:
{
"Port": 8080,
"Hostname": "web-01"
}
The rendered file will be:
:8080 {
respond "Hello from web-01"
}
2. Add a schema (optional)
Place a JSON Schema file at /etc/kheeper/schema.json in the image. The schema validates configuration values when creating a release, catching errors early.
schema.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"Hostname": {
"type": "string"
}
},
"required": ["Port", "Hostname"],
"additionalProperties": false
}
3. Add a starter (optional)
Place an example config at /etc/kheeper/starter.json. This serves as documentation for users creating releases from your image.
starter.json:
{
"Port": 80,
"Hostname": "my-host"
}
4. Build and push
Mark the image as configurable with the kheeper.configurable=1 annotation:
podman build --annotation kheeper.configurable=1 -t kheeper.com/myorg/webapp:v1 .
kheeper push myorg/webapp:v1
Verify the image was recognized as configurable:
kheeper images get myorg/webapp:v1
The output should include a ConfigImage field.
Example Containerfile
FROM quay.io/fedora/fedora-bootc:44
RUN dnf -y install caddy
COPY Caddyfile /etc/caddy/Caddyfile.khtmpl
COPY schema.json /etc/kheeper/schema.json
RUN bootc container lint
Creating releases
Once your configurable image is pushed, create releases for individual hosts:
# Write the config
cat > config.json << 'EOF'
{
"Port": 80,
"Hostname": "web-01"
}
EOF
# Create the release
kheeper releases create myorg/web-01:v1 \
--image kheeper.com/myorg/webapp:v1 \
--config-file config.json
If the image has a schema, the config is validated before the release is created.
Activating releases
After creating a release, activate it to tell the host which version to run:
kheeper hosts activate myorg/web-01:v1
The host polls for changes and applies the new release automatically.
Versioning and rollback
Each release is tagged (e.g. v1, v2). To roll back, activate a previous tag:
kheeper hosts activate myorg/web-01:v1
You can create multiple releases from different images or different configs:
# New config
kheeper releases create myorg/web-01:v2 \
--image kheeper.com/myorg/webapp:v1 \
--config-file updated-config.json
# Or new image version
kheeper releases create myorg/web-01:v3 \
--image kheeper.com/myorg/webapp:v2 \
--config-file config.json
List all releases for a host:
kheeper releases list myorg/web-01
Template syntax reference
Templates use Go's text/template package. Common patterns:
# Simple value substitution
{{ .Key }}
# Default values
{{ if .Key }}{{ .Key }}{{ else }}default{{ end }}
# Conditionals
{{ if eq .Env "production" }}
workers = 16
{{ else }}
workers = 2
{{ end }}
# Iterating over lists
{{ range .Servers }}
upstream {{ . }};
{{ end }}
See the Go text/template documentation for the full syntax.
Security
Configuration is encrypted with the host's ECDSA P-256 public key before being uploaded to the registry. Only the host can decrypt its own configuration using its private key. The Kheeper registry never has access to decrypted configuration values.