ttl.sh: an ephemeral Docker registry for CI, configured with Ansible
An anonymous & ephemeral Docker image registry
At a glance
- What is it?
- ttl.sh is an anonymous container registry for CI workflows, built on the zot registry, with Nginx terminating TLS and a sidecar that deletes manifests once they expire. The repository holds the host configuration, the marketing site and the Compose stack, not the infrastructure itself.
- Who is it for?
- Adopt ttl.sh if your CI pipeline needs a throwaway place to push and pull images and you want no accounts, no image retention and no cleanup job, and you are willing to accept an amd64-only, single-host deployment. Do not adopt it as a production registry: the service is anonymous, blobs live in an S3-compatible bucket that the README does not describe in detail, and the tag expiry path is explicitly not finished in this repository.
- 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 8 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A registry whose images are meant to disappear
ttl.sh is described as an anonymous, expiring container registry built on zot, and the audience is CI workflows rather than production deployments. The premise is that a build pipeline needs somewhere to push an image between one job and the next, and that keeping that image afterwards is pure cost. So the service offers a registry where images carry an expiry and are removed once they pass it, with no account to create and no push credentials to manage.
The repository is not the service. It holds the host configuration in Ansible, a Next.js marketing site under web/, and the Docker Compose stack that runs on the server. As the README puts it, the project does not provision infrastructure, it configures a host that already exists. That distinction matters for anyone expecting a one-command deployment: what you get here is the recipe, and the server is assumed to exist.
The remaining layout is small and readable: an ansible/ directory holding the Nginx and TLS setup, the Docker installation and the Compose deployment, a web/ directory for the site served at https://ttl.sh, a static/ directory described as legacy static assets, and the docker-compose.yaml at the root.
Nginx splits the marketing site from the registry API
The traffic split happens in Nginx, which terminates TLS and proxies two paths. The root path goes to the web container on port 3000. The /v2 path goes to the zot container on port 5000, and within that, /v2/_catalog is blocked.
Blocking the catalog endpoint is the significant detail. A Docker Registry v2 API normally exposes a catalog listing of the repositories it holds, and disabling it means the registry cannot be enumerated by anyone who finds the address. For an anonymous service on the public internet, that is the difference between a scratch space and a directory of everything other people are building.
The consequence for a client is that push and pull by explicit name still work, since those are individual object endpoints, while browsing what exists does not. A CI job that pushes a named image and later pulls that same name is inside the intended use; a job that tries to discover available images is not.
zot runs unmodified with its blobs in an S3 bucket
The registry itself is not modified in any way. The README states that zot runs an off-the-shelf image, ghcr.io/project-zot/zot-linux-amd64, with no local customization. The Compose file pins that image to version v2.1.20. There is no forked zot in this repository, and no patched behaviour, which means the security and behaviour characteristics of the registry are those of upstream zot at that tag.
There is exactly one configuration file, zot-config.json, and Ansible renders it onto the host from the template at ansible/templates/zot-config.json.j2. That template points zot at an S3-compatible bucket for blob storage. The consequence is architectural rather than cosmetic: image layers live in object storage rather than on the host disk, so the registry is not tied to one machine's disk, and the Compose file only mounts ./zot-data to /var/lib/zot for the registry's own local state while the config file is mounted read-only from ./zot-config.json to /etc/zot/config.json.
It also means the bucket is part of your data model. The repository does not document the bucket configuration itself, so whoever operates the service has to supply it and know what happens to blobs when a manifest is deleted.
The TTL sidecar, Redis, and a documentation gap
Expiry is handled by a third service, zot-ephemeral-ttl, built from the sidecar/ directory and running the ghcr.io/replicatedhq/ttlsh-ephemeral-ttl image. The comment in the Compose file describes the mechanism: zot posts image.updated CloudEvents to the sidecar, and the sidecar DELETEs manifests once they are expired. The sidecar keeps its expiry index in Redis, which runs as redis:8-alpine with appendonly enabled and a healthcheck that pings the server, and the sidecar declares a dependency on that healthcheck rather than starting blindly:
redis:
image: redis:8-alpine
container_name: ttlsh-redis
restart: always
command: ["redis-server", "--appendonly", "yes"]Here the repository contradicts itself in a way an adopter needs to notice. The README says tag expiry is handled separately and is not yet wired up in this repo, while the Compose file in the same repository ships a sidecar whose entire job is deleting expired manifests. Both statements are in the repository, and the README appears to predate the sidecar or to describe a deployment path that does not enable it.
The practical takeaway is that you should confirm in your own deployment which of the two is true, because a registry that accepts pushes but never deletes is a storage bill with no benefit, and a registry that deletes too eagerly breaks a pipeline.
One host, three containers, one bridge network
The Compose file describes the whole runtime, and it is worth reading line by line before you copy it. This is the registry service as it stands in docker-compose.yaml:
zot:
image: ghcr.io/project-zot/zot-linux-amd64:v2.1.20
container_name: ttlsh-zot
restart: always
pull_policy: always
ports:
- "127.0.0.1:5000:5000"The web service builds from the web/ context with its own Dockerfile, targeting the linux/amd64 platform only, and is published on 127.0.0.1:3000 so that only the host can reach it. The zot service is published the same way on 127.0.0.1:5000. Both use restart: always and pull_policy: always, and both sit on a bridge network called ttlsh-network.
Two choices stand out. Binding to 127.0.0.1 rather than 0.0.0.0 keeps the containers off the public network and lets Nginx be the only path in, which is the right posture for a service that is reachable without authentication. And pull_policy: always means the host fetches a fresh image on every start, so a rebuilt web image reaches the running service without anyone running a compose pull by hand.
The amd64-only platform list is the limitation to note. The Compose file declares linux/amd64 for both locally built images, and the zot image itself is the amd64 build, so an arm64 host is not what this stack was written for.
Deployment through a deploy branch and the Ansible playbook
Deployment is documented in DEPLOYING.md rather than in the README, and the pipeline is visible in the repository. Pushes to the deploy branch also run .github/workflows/deploy.yml, which builds and pushes the web image and then runs the Ansible playbook. The repository root also carries build-and-push.sh for the image build step outside the workflow.
The two-part arrangement is the mechanism to understand. The workflow handles the artifact, producing the web image and pushing it to the registry, and the playbook handles the host, applying the Nginx and TLS configuration, the Docker setup and the Compose deployment that runs on the server. Ansible templates such as ansible/templates/zot-config.json.j2 are how host-specific values reach the containers, so the registry configuration is rendered on the machine rather than baked into an image.
For an operator, this means the repository is a template for a deployment rather than a finished product you install. You supply the host, the domain and its certificate, the S3-compatible bucket, and the credentials that let Ansible reach the machine, and the repository tells you how those pieces are assembled.
Where ttl.sh is the wrong tool
There are clear cases where a shared ephemeral registry is the wrong answer. If your images must be kept, or must be pulled by a customer or by a customer of your customer, expiry is a defect rather than a feature. If you need access control, an anonymous registry has none, so anyone who knows an image name can pull it. If you need to know what is in the registry, the catalog path is blocked by design. If your pipeline builds on arm64 runners, the amd64-only configuration does not cover it. And if you need to inspect an image after a build failed, the window between push and pull is short by design.
The alternative approach is to run your own registry on infrastructure you control, with retention policies and access rules that you set, and to accept the operational work of running it, upgrading it and storing its blobs. ttl.sh takes the opposite trade: it hands you a shared, anonymous, disposable endpoint and asks for nothing in return, which is the right answer for a CI pipeline whose images live for minutes and whose credentials would otherwise have to be distributed to every runner.
Between those two, the deciding factor is who needs the image after the build. If only your own jobs do, the anonymous ephemeral registry removes real work. If anything else does, the missing access control and the expiry become the two reasons your pipeline breaks.
Editorial conclusion
Adopt ttl.sh if your CI pipeline needs a throwaway place to push and pull images and you want no accounts, no image retention and no cleanup job, and you are willing to accept an amd64-only, single-host deployment. Do not adopt it as a production registry: the service is anonymous, blobs live in an S3-compatible bucket that the README does not describe in detail, and the tag expiry path is explicitly not finished in this repository. Verify three things first: that the expiry sidecar is actually enabled for your deployment, since the README states tag expiry is handled separately and not yet wired up here; that the image reference prefix your CI uses resolves through the /v2 proxy with the catalog path blocked; and that your pipeline tolerates images vanishing between the push and the pull. The licence is Apache-2.0, there are no GitHub releases, and the last push was on 2026-10-14.
Frequently asked questions
What is ttl.sh and who is it for?
It is an anonymous, expiring container registry for CI workflows, built on zot. The intended use is pushing an image between CI jobs without keeping it afterwards and without managing push credentials.
How does ttl.sh delete expired images?
The Compose stack includes a zot-ephemeral-ttl sidecar that receives image.updated CloudEvents from zot and deletes manifests once they are expired, with Redis as the backing store for its expiry index. The README separately states that tag expiry is handled separately and is not yet wired up in the repository, so confirm which behaviour your deployment has.
What is stored where in the ttl.sh deployment?
Image blobs go to an S3-compatible bucket configured in zot-config.json, which Ansible renders onto the host from ansible/templates/zot-config.json.j2. The host itself keeps the registry state in ./zot-data mounted at /var/lib/zot.
How is the ttl.sh service deployed?
DEPLOYING.md covers it. Pushes to the deploy branch run .github/workflows/deploy.yml, which builds and pushes the web image and then runs the Ansible playbook that configures the host.
Can anonymous users browse the images on ttl.sh?
No. Nginx proxies /v2 to the zot container and blocks /v2/_catalog, so the catalog listing is not exposed. Pushing and pulling named images still work.
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/replicatedhq-ttl-sh)Was this useful?