A single-binary, server-rendered web UI for browsing OCI and Docker registries.
MazeRegistryUI exists because the common alternative, joxit/docker-registry-ui,
is a JavaScript single-page app that ships a large bundle to the browser and
talks to the registry from there. This one renders HTML on the server and uses
HTMX for the few interactive parts, so the page is useful before any script
runs and works on a phone. The browser never contacts a registry: every call is
proxied by the backend, which keeps credentials server-side and removes the
need for CORS configuration on the registry. One binary, one config file, and a
header picker for as many registries as you list.
It speaks the OCI Distribution Specification v1.1, as served by Distribution 3.x, Harbor, GHCR, Gitea and others. Legacy Docker manifest schema 1 is rejected on purpose.
![]() |
![]() |
| Repository — tags load as they scroll into view | Image — layer composition, config and raw manifest |
![]() |
![]() |
| Mobile layout | Light theme |
Every configured registry in one place, with a live reachability check
- Multiple registries in one instance, switched from a picker in the header, each with its own credentials, timeouts and permissions.
- Catalog with instant in-memory search; the full repository list is indexed server-side, so filtering does not re-walk the registry.
- Tag rows that resolve lazily as they scroll into view, bounded by a per-registry concurrency limit.
- Image detail: proportional layer composition bar with the three largest layers highlighted, the build command behind each layer, labels, environment, config, annotations and the raw manifest.
- Multi-architecture indexes: every child manifest is listed with its platform, and shared layers are counted once in the total size.
- Copyable
docker pullcommand, with an overridable pull hostname. - Manifest deletion, opt-in per registry.
- Auth: anonymous, HTTP basic, a static bearer token, and the automatic bearer token-service handshake (Harbor, GHCR, Gitea, htpasswd-protected Distribution).
- Dark and light themes, remembered per visitor.
/healthzendpoint, structured logs (text or JSON), graceful shutdown.- No CDN, no Node build step: templates, CSS, IBM Plex Mono and HTMX are embedded in the binary, so it runs air-gapped.
git clone https://github.com/amaze-labs/MazeRegistryUI.git
cd MazeRegistryUI
$EDITOR deploy/config.yaml # point it at your registries
docker compose up -dOpen http://localhost:8080. The bundled stack starts a throwaway
Distribution 3 registry alongside the UI, so there is something to look at
before you change anything. docker compose down -v removes both.
Write a config.yaml:
server:
addr: ":8080"
ui:
title: Registry
registries:
- id: local
name: Local registry
url: http://registry.internal:5000
auth:
type: noneThen:
docker run -d --name mazeregistryui \
-p 8080:8080 \
-v "$PWD/config.yaml:/etc/mazeregistryui/config.yaml:ro" \
--read-only --tmpfs /tmp:size=16m \
--cap-drop ALL --security-opt no-new-privileges:true \
ghcr.io/amaze-labs/mazeregistryui:latestThe image runs as uid 65532 and reads /etc/mazeregistryui/config.yaml by
default. See docs/DEPLOYMENT.md for Kubernetes, reverse
proxies and TLS.
make build # -> bin/mazeregistryui
./bin/mazeregistryui -config config.yamlRequires Go 1.25 or newer and nothing else. Useful flags:
./bin/mazeregistryui -config config.yaml -check # validate the config and exit
./bin/mazeregistryui -versionConfiguration is file-only: registries cannot be added from the UI, so the file is the complete list of what is browsable.
| Key | Default | What it does |
|---|---|---|
server.addr |
:8080 |
Listen address. |
server.base_path |
(none) | Serve under a sub-path, e.g. /registry. |
server.access_log |
false |
One structured log line per request. |
ui.title |
MazeRegistryUI |
Name in the header and page titles. |
ui.theme |
auto |
auto, dark or light; visitors can override. |
ui.show_pull_command |
false |
Render a copyable docker pull command. |
registries[].id |
derived from name |
Identifier used in URLs. |
registries[].url |
(required) | Registry root — without the /v2 suffix. |
registries[].auth.type |
none |
none, basic or bearer. |
registries[].delete_enabled |
false |
Expose manifest deletion for this registry. |
registries[].cache_ttl |
60s |
How long tag-addressed data is reused. |
Secrets stay out of the file with ${VAR} / ${VAR:-fallback} expansion or
the password_env / token_env fields.
Full reference, worked examples and troubleshooting: docs/CONFIGURATION.md.
- To run the container: any OCI runtime. Images are published for
linux/amd64andlinux/arm64. - To build from source: Go 1.25 or newer. No CGO, no Node, no other toolchain.
- Registries: anything implementing OCI Distribution Spec v1.1 — Distribution 3.x, Harbor, GHCR, Gitea, Zot, Artifactory. Registries that only serve Docker manifest schema 1 are not supported.
- Deletion additionally needs the registry itself started with storage deletion
enabled (
REGISTRY_STORAGE_DELETE_ENABLED=truefor Distribution).
| Document | Contents |
|---|---|
| docs/CONFIGURATION.md | Every configuration field, worked examples, troubleshooting. |
| docs/DEPLOYMENT.md | Docker, Compose, Kubernetes, reverse proxies, releases. |
| docs/ARCHITECTURE.md | How it is put together, for people changing the code. |
| CONTRIBUTING.md | Development loop and commit conventions. |
GNU General Public License v3.0 or later — see LICENSE.
Deploying this as a network service does not by itself oblige you to publish changes; the GPL is triggered by distribution, not by use. If you distribute a modified build, the modified source must go with it.
Bundled third-party components — htmx (0BSD), IBM Plex Mono (OFL-1.1) and gopkg.in/yaml.v3 (MIT and Apache-2.0) — keep their own licences: see THIRD-PARTY-NOTICES.md.




