k8tz injects a timezone into every pod it admits
Kubernetes admission controller and a CLI tool to inject timezones into Pods and CronJobs
At a glance
- What is it?
- k8tz is a Kubernetes admission controller and a CLI that mount a TZif file into /etc/localtime so that pods, cronjobs and namespaces agree on the clock. It fixes a small but persistent class of date bugs, and it can also leave pods stuck in PodInitializing if you pick the wrong injection strategy.
- Who is it for?
- k8tz suits platform teams whose workloads keep tripping over UTC assumptions, and it does not suit a cluster where you cannot add a mutating webhook, or where the host nodes lack /usr/share/zoneinfo if you intend to use the hostPath strategy.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 97 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Containers do not inherit the node clock, and the default is not guaranteed
The problem k8tz addresses is narrow and very real. A container does not inherit the timezone of the host machine and has access only to the clock from the kernel. The default timezone for most images is UTC, but the documentation is careful to add that it is not guaranteed and may differ from container to container. Once a timestamp crosses a service boundary, that small difference becomes a bug that only shows up in reports, schedules and logs.
k8tz is described as a Kubernetes admission controller and a CLI tool to inject timezones into Pods and CronJobs, and the aim is to standardize a selected timezone across pods and namespaces automatically with minimal effort. The audience is a platform team that has been standardising timestamps by hand, or a set of cronjobs that were written against a developer's local machine and quietly shifted when deployed.
The feature list in the README also covers a second, related problem: standardizing the tzdata version across all pods in a cluster, so that a rule about what a timezone offset means on a given date is the same everywhere. Neither tzdata in the images nor tzdata on the nodes is required for the controller to do its job, which is the part that removes the usual excuse for not standardising anything.
The whole mechanism is a TZif file mounted at /etc/localtime
The mechanism is worth understanding because it explains every design choice in the project. Timezone information is defined using Time Zone Information Format files, `TZif`, as specified in RFC 8536. The Timezone Database contains these files, one per location, describing local time for that location.
To set a container's timezone, `/etc/localtime` inside the container has to point at a valid `TZif` file for the requested zone. In most images those files do not exist by default, so the project has to make them available from inside the container, mounted at `/etc/localtime`. That is the whole job. There is no agent inside your pod watching the clock, and no environment variable doing the work, which is also why the solution has to be delivered at admission time rather than at run time.
The practical consequence is that the timezone becomes a property of the pod specification, and therefore of the controller's mutation. Once the webhook is in the path, the injected volume and init container are part of the pod as it is admitted, and the container's clock follows from the mount rather than from anything the application does.
Three strategies, and the one that can leave a pod stuck
There are three ways to deliver the `TZif` file, and the differences between them are not cosmetic.
With the `hostPath` strategy, the files under `/usr/share/zoneinfo` are expected to exist on every node in the cluster, and it is the user's responsibility to ensure that. The `hostPath` volume then supplies the required file into the pod. The failure mode is explicit in the documentation: if the required timezone is missing on the host machine, the pod will be stuck in `PodInitializing` status and will not be started. That is a cluster wide footgun, because a single node with an incomplete zoneinfo tree can take a workload down without touching the controller at all.
The default method of k8tz is a bootstrap `initContainer`. The init container is injected into the pod and supplies the required `TZif` file through a shared `emptyDir` volume, so the zone data comes from the k8tz image rather than from the node.
The third option, `imageVolume`, is available on Kubernetes 1.33 and later and mounts `/usr/share/zoneinfo` directly from the k8tz image without the shared `emptyDir`. The documentation still recommends the initContainer strategy, because `imageVolume` currently cannot mount `/etc/localtime` from the image. The reason for the preference is right there in the sentence.
Annotations are resolved one key at a time, closest object first
Configuration happens through annotations on `Pod` and `Namespace` objects, and the resolution rule is precise enough to matter. k8tz resolves every annotation key independently, so the closest object to the `Pod` that defines a specific annotation wins for that annotation. A pod that sets the timezone but not the strategy inherits the strategy from its namespace, and a namespace that sets neither falls through to the webhook defaults.
There are three annotations. `k8tz.io/inject` decides whether k8tz should inject a timezone at all and defaults to `true`. `k8tz.io/timezone` sets the zone, with values such as `Africa/Addis_Ababa`, and defaults to `UTC`. `k8tz.io/strategy` chooses the injection strategy, one of `hostPath`, `initContainer` or `imageVolume`, and defaults to `initContainer`.
The default admission order is `Pod`, then `Namespace`, then the webhook defaults. A beta feature changes that chain. Enabling the webhook `--podOwnerLookup` flag, or the Helm value `podOwnerLookup=true`, inserts the controller owners between the pod and the namespace, giving the order `Pod`, controller owners, `Namespace`, webhook defaults. The practical effect is that an annotation on a Deployment or ReplicaSet can now set the timezone for the pods it produces, which is how most people actually want to configure it. The README lists the supported controller owner chains starting with `ReplicaSet`, though the list is cut off in the version described here.
Install paths: Helm for the controller, go install for the CLI
The controller is installed with a Helm chart, and the short version of the instructions is two commands:
helm repo add k8tz https://k8tz.github.io/k8tz/
helm install k8tz k8tz/k8tz --set timezone=Europe/LondonThe chart has its own README with the full set of values, and the timezone you set at install time becomes the webhook default that applies when no annotation overrides it. The chart is released separately from the binary, as a `helm-chart-v0.20.0-k8tz-v0.20.0` release alongside the `v0.20.0` binary release, both dated 2026-07-18.
The CLI has four documented install routes. A precompiled binary is downloadable from the GitHub release page and unpacked with wget and tar. Go users can install it directly:
go install github.com/k8tz/k8tz@latestDocker is the third option, with the image published at quay.io/k8tz/k8tz, and building from source is the fourth, with `make compile` producing a binary at `build/k8tz` and `make TARGET=/usr/local/bin install` copying it into place. The Makefile's install target checks whether the target directory is writable and only then falls back to sudo, so a user writable prefix does not need root.
The CLI is idempotent, so both routes can run side by side
The CLI exists for people who would rather not put a mutating webhook in the request path. The README says k8tz can be used to inject a timezone into YAML files, or integrated inside another deployment script that does not want the admission controller automation.
A pod manifest can be processed from file to file, or piped straight to kubectl:
k8tz inject --strategy=hostPath test-pod.yaml > injected-test-pod.yaml
k8tz inject --timezone=Europe/London test-pod.yaml | kubectl apply -f -Existing deployments in the current namespace can be piped through the same filter, and the Docker image can be used in the same pipeline without installing anything locally.
The property that makes this safe to combine with the controller is idempotence. The documentation states that the injection process is idempotent, that you can run it multiple times, and that you can use the CLI injection alongside the admission controller, because subsequent injections have no effect. For a team migrating, that means you can start with the CLI on existing deployments and switch the webhook on for new ones without ending up with double injected volumes. The absence of a stated failure mode for malformed input is worth testing on your own manifests before piping a whole namespace through it.
Version pins decide what a k8tz upgrade touches
The Dockerfile shows how the timezone data is packaged. The build starts from a separate data image, `quay.io/k8tz/tzdata:2026c`, copies the k8tz binary into it, runs as user 1000 and sets the entrypoint to the binary. The `2026c` suffix is the tzdata release the image was cut against, and the Makefile keeps the same value in `TZDATA_VERSION` while the controller version lives in `VERSION`, currently 0.20.0. Those two move on the same release cycle here, with the v0.20.0 release tagged as 2026c and the previous v0.19.0 tagged as 2026a.
The Go side pins the Kubernetes libraries at v0.32.13 for api, apimachinery, client-go and component-base, uses cobra v1.8.1 for the command line, and `sigs.k8s.io/yaml` for manifest handling. The module targets Go 1.25.12, which is the compiler version a build image needs.
The project is Apache-2.0 licensed, and the repository root carries a `CODE_OF_CONDUCT.md` and a `CHANGELOG.md`. The last push to the repository was on 2026-07-18 and the project is not archived. For a cluster operator the practical upgrade cost is not the controller but the timezone data, since a data image refresh is what changes how a future daylight saving transition is interpreted inside your pods, and the data version moves with the controller release rather than on its own.
Editorial conclusion
k8tz suits platform teams whose workloads keep tripping over UTC assumptions, and it does not suit a cluster where you cannot add a mutating webhook, or where the host nodes lack /usr/share/zoneinfo if you intend to use the hostPath strategy. Before enabling it, confirm the timezone you want on a test namespace with `k8tz.io/timezone` on the Namespace object, and check the tzdata version your image will mount, because the bundled data comes from the quay.io/k8tz/tzdata image rather than from your nodes.
Frequently asked questions
What problem does k8tz solve in Kubernetes?
Containers do not inherit the timezone of the host, and the default is usually UTC but not guaranteed to be the same in every image. k8tz injects a timezone into Pods and CronJobs so a cluster, a namespace or a single pod can be standardised on one zone, and it also standardises the tzdata version across pods.
How do I install the k8tz admission controller?
Add the chart repository with `helm repo add k8tz https://k8tz.github.io/k8tz/` and install with `helm install k8tz k8tz/k8tz --set timezone=Europe/London`. The chart's own README documents the remaining values, and the binary can also be installed with `go install github.com/k8tz/k8tz@latest` or run from the quay.io/k8tz/k8tz image.
What is the difference between the hostPath, initContainer and imageVolume strategies in k8tz?
The hostPath strategy expects zoneinfo files on every node and leaves the pod stuck in `PodInitializing` if the zone is missing there. The initContainer strategy, the default, supplies the file through a shared emptyDir from the k8tz image. The imageVolume strategy needs Kubernetes 1.33 or later and still cannot mount /etc/localtime, so initContainer is the recommended one.
How do k8tz annotations inherit between a pod and its namespace?
Each annotation key is resolved independently, and the closest object to the pod wins for that key. The default order is `Pod`, then `Namespace`, then the webhook defaults, and enabling the `--podOwnerLookup` webhook flag or the `podOwnerLookup=true` Helm value inserts controller owners between the pod and the namespace.
Can I use the k8tz CLI together with the admission controller?
Yes. The documentation states that the injection is idempotent, that you can run it multiple times, and that CLI injection can be used alongside the admission controller because subsequent injections have no effect.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/k8tz-k8tz)