# registry-ui reads a Docker or OCI registry directly and refuses to handle your TLS

> registry-ui is a Go web interface for browsing and managing Docker Registry and OCI-compatible registries, with a tag retention CLI, an event listener and a SQLite or MySQL backend. It suits a platform team that wants a read-mostly view of its image estate, and it deliberately ships no authentication of its own.

**Quiq/registry-ui** — Web UI for Docker Registry

- Repository: https://github.com/Quiq/registry-ui
- Stars: 531 · Forks: 104
- Language: Go
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/quiq-registry-ui

## It talks to the registry API, so it needs no access to the storage

The architecture is a single Go binary that acts as a client to a registry, and the dependency list makes that clear. The module is `github.com/quiq/registry-ui` on Go 1.26.0, and the direct dependencies include `github.com/google/go-containerregistry` at 0.22.1, which is the library that does the talking to registries, plus `github.com/labstack/echo/v4` for the HTTP layer, `github.com/CloudyKit/jet/v6` for the templates, `github.com/spf13/viper` for configuration, `github.com/sirupsen/logrus` for logging, `github.com/mattn/go-sqlite3` and `github.com/go-sql-driver/mysql` for the event database, and `github.com/tidwall/gjson` for JSON handling. The repository layout follows the same split, with a `registry/` package, an `events/` package, a `data/` directory, `templates/` and `static/` for the front end, and `web.go`, `middleware.go` and `template.go` at the root. Nothing in the design implies the UI reads the registry's storage directly, which is why it works against a remote registry as easily as against a local one.

## Four commands get you from no registry to a browser view

The quick start is four docker commands, and each one does something specific. First you start a registry on your host if you do not already have one, with the network set to host so the UI can reach it:

```bash
    docker run -d --network host --name registry registry
```

Then you push an image to it, tagging and pushing to a path under `127.0.0.1:5000`:

```bash
    docker tag alpine:edge 127.0.0.1:5000/owner/name
    docker push 127.0.0.1:5000/owner/name
```

Finally you run the UI, pointing it at the registry and telling it the connection is insecure, and then open `http://127.0.0.1:8000`:

```bash
    docker run --rm --network host \
        -e REGISTRY_HOSTNAME=127.0.0.1:5000 \
        -e REGISTRY_INSECURE=true \
        --name registry-ui quiq/registry-ui
```

The published image is `quiq/registry-ui`. The two environment variables are the whole configuration surface for the quick start, and the second one is a decision rather than a default, since it tells the UI that the registry speaks plain HTTP.

## config.yml options map to environment variables by section and key

Configuration lives in `config.yml` with self-descriptive options, and every option can be overridden through environment variables using `SECTION_KEY_NAME` syntax. The documented examples are `LISTEN_ADDR`, `PERFORMANCE_TAGS_ASYNC_REFRESH_INTERVAL` and `REGISTRY_HOSTNAME`, and the naming convention tells you how the file is organised: the file has sections, and the key inside the section becomes the second part of the variable name. The background refresh interval is worth noticing, because it connects to one of the stated features, that the repository list and tags are cached and refreshed in the background, so the UI does not query the registry on every page load. The Go dependency on viper is what implements this file-plus-environment merge. A full config file can be passed in as a volume, and the documented form is:

```bash
    docker run -d -p 8000:8000 -v /local/config.yml:/opt/config.yml:ro quiq/registry-ui
```

The image also accepts a custom root CA certificate mounted over `/etc/ssl/certs/ca-certificates.crt`, and a timezone through a `TZ` environment variable such as `America/Los_Angeles`.

## The event listener is where the registry tells the UI what changed

The event listener is the feature that turns a static viewer into something that knows what happened. It is described as an event listener to capture registry notifications, stored in SQLite or MySQL. The registry side is configured with a notifications endpoint, and the block the documentation gives looks like this:

```yaml
    notifications:
      endpoints:
        - name: registry-ui
          url: http://registry-ui.local:8000/event-receiver
          headers:
            Authorization: [Bearer abcdefghijklmnopqrstuvwxyz1234567890]
          timeout: 1s
          threshold: 5
          backoff: 10s
          ignoredmediatypes:
            - application/octet-stream
```

You adjust the URL and the token as appropriate, and if the UI runs under a non-default base path such as `/ui` then the endpoint path becomes `/ui/event-receiver`. The threshold and backoff values are the retry behaviour of the registry's own notification sender, and the ignored media type keeps the layer blobs out of the event stream, which is what keeps the event table small. The persistence side has its own rules: to keep the SQLite database for event data you mount the data folder, and the documentation asks you to ensure it is owned by `nobody`, which is uid 65534 on Alpine.

## The data folder and read-only mode interact in one specific way

There is a small but real operational conflict in the container configuration, and the documentation resolves it explicitly. The container supports `--read-only` mode, which is the right default for a UI that mostly reads. But if you use the event listener, you need to mount the data folder in read-write mode as shown in the example, so that the SQLite database remains writable. The form is:

```bash
    -v /local/data:/opt/data
```

The Dockerfile is consistent with that. The final image is `alpine:3.24`, it installs ca-certificates and tzdata, creates `/opt/data` and chowns it to nobody, adds the templates, the static files and `config.yml`, copies the compiled binary from the builder stage, and switches to `USER nobody` with the entrypoint at `/opt/registry-ui`. So the writable path is prepared and owned by the same user the process runs as. The builder stage is `golang:1.27.2-alpine3.24`, and it runs the package tests before building:

```dockerfile
RUN go test -v ./registry && \
    go build -o /opt/registry-ui *.go
```

## Tag purging is a CLI in the same binary, and it does not free disk

The retention feature is a built-in CLI for tag retention, described as purging tags older than X days while keeping at least Y tags. It runs inside the same binary, which is why the cron entry invokes the container's own executable. The first requirement is on the registry side, because the registry has to permit deletion:

```yaml
    storage:
      delete:
        enabled: true
```

The scheduled job is a plain crontab line:

```bash
    10 3 * * * root docker exec -t registry-ui /opt/registry-ui -purge-tags
```

And there is a dry-run mode for previewing what would be purged, which is the version to run first:

```bash
    docker exec -t registry-ui /opt/registry-ui -purge-tags -dry-run
```

The caveat at the end of that section is the important part. Purging tags only removes tag references, and to reclaim disk space you have to run garbage collection on your registry afterwards. So the CLI is half a solution by design, and an operator who expects the disk to shrink after a purge will be disappointed.

## MySQL is an option, and the table can be created by hand

The event store defaults to SQLite, and MySQL is a configuration change rather than a different binary. To use MySQL you update `event_database_driver` and `event_database_location` in the config file, and you create the database referenced in the DSN beforehand. The required privileges are `SELECT`, `INSERT` and `DELETE`, and the reason those three are named is that the table can be created by you, which avoids granting `CREATE`. The manual schema is:

```sql
	CREATE TABLE events (
		id INTEGER PRIMARY KEY AUTO_INCREMENT,
		action CHAR(4) NULL,
		repository VARCHAR(100) NULL,
		tag VARCHAR(100) NULL,
		ip VARCHAR(45) NULL,
		user VARCHAR(50) NULL,
		created DATETIME NULL
	);
```

The columns are what the event stream carries. The action is a four-character code, the repository and tag identify what was touched, the IP and user say who did it and the created timestamp says when, and the nullability on every column except the primary key reflects that a notification does not always contain all of them.

## Where a registry UI stops being useful

The security boundary is the one the project is clearest about, and it is a real constraint rather than a caveat. The documentation states that the UI does not handle TLS or authentication, and instructs you to place it behind a reverse proxy such as nginx, oauth2_proxy or similar. So the UI is safe to expose only if you get that proxy right, and the token in the event listener's Authorization header is the only credential in the loop. The functional limit is the write side. The features list covers browsing repositories, tags and nested repository trees at any depth, viewing full image details with layers, config and command history, supporting Docker and OCI image formats including multi-platform image indexes, the event listener, the retention CLI, auto-discovery of authentication methods such as basic auth, token service and keychain, and background refresh of the cached repository list and tags. Purging is a bulk operation. Anything finer-grained, such as editing a manifest or promoting a tag between repositories, is not in that list. The alternative to weigh against is a registry that ships its own browser interface, which is what the Docker distribution does, and the difference in approach is that the registry's own UI is guaranteed to match the API version, while registry-ui is a separate client that can fall behind a registry that changes its endpoints. The reverse proxy requirement, the coarse write operations and the fact that a UI with no auth of its own is one misconfiguration away from being public are the reasons to treat it as an internal tool.

## Conclusion

registry-ui is worth deploying when a platform team needs to see what is in a registry, browse tags and image histories, and apply a tag retention policy, because it speaks to the registry API directly, ships the retention CLI in the same binary and supports both Docker and OCI formats including multi-platform image indexes. It is a poor fit for anything exposed to a network you do not control, because the documentation states plainly that the UI does not handle TLS or authentication and tells you to place it behind a reverse proxy such as nginx or oauth2_proxy. It is also a poor fit if you want to delete images and reclaim disk, because purging tags only removes tag references and you still have to run garbage collection on the registry afterwards. Before deploying it, decide whether SQLite or MySQL holds the event data, set the ownership of the data directory to `nobody` as the documentation asks, and note that the last push to the repository was on 2026-10-12 with release 0.12.1 published the same day.

## FAQ

### How do I run registry-ui against a Docker registry?

Start a registry with docker run -d --network host --name registry registry, push an image to 127.0.0.1:5000, then run the UI with docker run --rm --network host -e REGISTRY_HOSTNAME=127.0.0.1:5000 -e REGISTRY_INSECURE=true --name registry-ui quiq/registry-ui and open http://127.0.0.1:8000.

### Does registry-ui handle authentication and TLS?

No. The documentation states that the UI does not handle TLS or authentication and that you should place it behind a reverse proxy such as nginx, oauth2_proxy or similar. The container can still be given a custom root CA certificate by mounting it over /etc/ssl/certs/ca-certificates.crt.

### How do I purge old tags in registry-ui?

Enable tag deletion in the registry config with storage.delete.enabled set to true, then schedule a cron job that runs docker exec -t registry-ui /opt/registry-ui -purge-tags. Use the -dry-run flag first to preview, and note that purging only removes tag references, so garbage collection has to be run on the registry afterwards to reclaim disk space.

### Can registry-ui use MySQL instead of SQLite for event data?

Yes, by updating event_database_driver and event_database_location in the config file and creating the database referenced in the DSN beforehand. The required privileges are SELECT, INSERT and DELETE, and the table can be created manually to avoid granting CREATE.

## Sources

- [Issues](https://github.com/Quiq/registry-ui/issues)
- [License: Apache-2.0](https://github.com/Quiq/registry-ui/blob/master/LICENSE)
- [Quiq/registry-ui on GitHub](https://github.com/Quiq/registry-ui)
- [README](https://github.com/Quiq/registry-ui/blob/master/README.md)
- [Releases](https://github.com/Quiq/registry-ui/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/quiq-registry-ui
