---
title: "Deploy Docker Registry"
description: "A private server for storing and sharing container images"
category: "Storage"
url: https://railway.com/deploy/private-docker-registry
---

# Deploy Docker Registry

A private server for storing and sharing container images

**[Deploy Docker Registry on Railway](https://railway.com/template/private-docker-registry)**

Machine-readable deploy manifest (JSON, validated by TemplateCI): https://railway.com/deploy/private-docker-registry/manifest.json

- **Creator:** A3A
- **Category:** Storage
- **Total deploys:** 1

## Template content

### registry https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/distribution.svg

- **Source:** https://github.com/gridalpha/docker-registry-railway
- **Public domain:** Yes

### Redis https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/redis.svg

- **Image:** redis:8.2
- **Start command:** `/bin/sh -c "rm -rf $RAILWAY_VOLUME_MOUNT_PATH/lost+found/ && exec docker-entrypoint.sh redis-server --requirepass $REDIS_PASSWORD --save 60 1 --dir $RAILWAY_VOLUME_MOUNT_PATH"`

### registry-ui https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/docker-moby.svg

- **Image:** joxit/docker-registry-ui:latest
- **Start command:** `/bin/sh -c 'sed -i "s/^worker_processes.*/worker_processes 2;/" /etc/nginx/nginx.conf; set -- $(grep -m1 ^nameserver /etc/resolv.conf); if [ -z "$NGINX_RESOLVER" ] && [ -n "$2" ]; then case "$2" in *:*) NGINX_RESOLVER="[$2] valid=10s";; *) NGINX_RESOLVER="$2 valid=10s";; esac; fi; export NGINX_RESOLVER; case "$NGINX_PROXY_PASS_URL" in ""|http://:*) NGINX_PROXY_PASS_URL="http://registry.railway.internal:5000";; esac; export NGINX_PROXY_PASS_URL; echo "registry-ui: resolver=$NGINX_RESOLVER upstream=$NGINX_PROXY_PASS_URL"; exec /docker-entrypoint.sh nginx -g "daemon off;"'`
- **Health check:** /
- **Public domain:** Yes

## Buckets

- **registry-storage**

## Documentation

# Deploy and Host Docker Registry on Railway

Docker Registry is the reference implementation of the OCI Distribution Specification, the server behind `docker push` and `docker pull`. Maintained by the CNCF as [distribution/distribution](https://github.com/distribution/distribution), it gives a team its own image store rather than an account on a shared one, so proprietary images stay private, CI stops hitting Docker Hub rate limits, and build artifacts live where you choose.

Self-host Docker Registry on Railway and the template wires up three services. **registry** runs `registry:3` and serves the `/v2/` API on a public HTTPS domain — the endpoint you point `docker login` at. Railway terminates TLS for you, so no `insecure-registries` entry is needed on any client. **registry-ui** runs [joxit/docker-registry-ui](https://github.com/Joxit/docker-registry-ui), a browsable catalog of everything you have pushed; it proxies the registry over the private network, so one login covers both. **Redis** holds distribution's shared blob-descriptor cache, which lets the registry run more than one replica coherently. Every layer, manifest and tag goes to a Railway object storage bucket rather than a disk, so nothing is pinned to a single container.

![Diagram of the registry, registry-ui and Redis services on Railway](https://res.cloudinary.com/rroe4rtk/image/upload/v1787354934/docker-registry-architecture.png)

## Getting Started with Docker Registry on Railway

Set `REGISTRY_PASSWORD` at deploy time — that, with the optional `REGISTRY_USERNAME` (default `admin`), is the one account the registry accepts. There are no default credentials and anonymous access is rejected, so a fresh deployment is closed the moment it is live. Open the **registry-ui** URL first: the browser shows a basic-auth prompt from the registry itself, and signing in gets you an empty catalog. Now log in from your terminal against the **registry** URL, tag a local image with that hostname and push it:

```
docker login registry-production.up.railway.app -u admin
docker tag alpine:3.23 registry-production.up.railway.app/base/alpine:3.23
docker push registry-production.up.railway.app/base/alpine:3.23
```

Refresh the UI and the repository appears with its tag count. Click into it for creation date, size, content digest and architecture, and the history icon for layer commands, entrypoint and environment. To confirm reads, `docker pull` the same tag elsewhere.

![Docker Registry UI listing four pushed image repositories](https://res.cloudinary.com/rroe4rtk/image/upload/v1787354937/docker-registry-catalog.png)

![Tag table for web/nginx showing size, digest and architecture](https://res.cloudinary.com/rroe4rtk/image/upload/v1787354939/docker-registry-taglist.png)

![Image history panel with the nginx layer commands and environment](https://res.cloudinary.com/rroe4rtk/image/upload/v1787354943/docker-registry-image-history.png)

## About Hosting Docker Registry

A container registry is a content-addressable store: clients upload layers by digest, then a manifest naming them, then a tag pointing at the manifest. Docker Registry implements exactly that and nothing more, which is why it is small and fast. Teams self-host when images hold source, models or customer data that should not sit in a third-party account, or when CI pulls enough that public rate limits bite.

- Full OCI Distribution Specification v1.1 support: Docker, Podman, BuildKit, containerd, Skopeo, ORAS and Helm all work unchanged
- Username and password auth on every request, catalog reads included
- Object-storage backend, so image data is not tied to one container's disk
- Tag deletion through the API and the web UI, plus Prometheus metrics privately
- Stores any OCI artifact, not only images — Helm charts, WASM modules, SBOMs

The split is clean: the registry owns the API and all storage access, the UI is a static app plus a small reverse proxy holding no state, and Redis caches blob descriptors so repeated lookups skip object storage.

## Why Deploy Docker Registry on Railway

Railway removes the parts of running a registry that are not about images:

- HTTPS is provisioned automatically, which Docker clients require
- Object storage is attached in the same project, credentials as references
- Redis is one managed service, not a second thing to operate
- The private network keeps the UI-to-registry hop off the internet
- More replicas is a slider, because no service owns local state

## Common Use Cases

- **Private images for a team** — build in CI, push here, pull from staging and production, no per-seat cost
- **Your organisation's golden base images** — so builds stop depending on public registry availability
- **Kubernetes or Nomad clusters** — pulls stay inside your own infrastructure
- **OCI artifact storage** — Helm charts, WASM modules and SBOMs, addressed by digest

## Dependencies for Docker Registry

- **registry** — [gridalpha/docker-registry-railway](https://github.com/gridalpha/docker-registry-railway), a thin layer on the official `registry:3` image that generates the bcrypt password file and storage configuration at startup
- **registry-ui** — `joxit/docker-registry-ui:latest`, web catalog and reverse proxy
- **Redis** — managed Redis, distribution's blob-descriptor cache
- **Object storage** — a bucket holding every layer, manifest and tag

### Environment Variables Reference

| Variable | Service | Purpose |
|---|---|---|
| `REGISTRY_PASSWORD` | registry | Password `docker login` accepts. Required |
| `REGISTRY_USERNAME` | registry | Account name, defaults to `admin` |
| `REGISTRY_HTTP_SECRET` | registry | Signs upload state; identical on every replica |
| `PORT` | registry | `5001`, the private port the health check probes; the API stays on 5000 |
| `S3_BUCKET`, `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION` | registry | Bucket credentials, as references |
| `REDIS_HOST`, `REDIS_PORT`, `REDIS_USER`, `REDIS_PASSWORD` | registry | Blob-descriptor cache; unset them for an in-process one |
| `NGINX_PROXY_PASS_URL` | registry-ui | Private address of the registry |
| `DELETE_IMAGES` | registry-ui | Whether the UI offers tag deletion |

Anything not listed can be set with distribution's own `REGISTRY_` overrides, layered on top of the generated configuration file.

### Deployment Dependencies

- Source repository: [distribution/distribution](https://github.com/distribution/distribution)
- Official image: [registry on Docker Hub](https://hub.docker.com/_/registry)
- Configuration: [distribution docs](https://distribution.github.io/distribution/about/configuration/)
- Web UI: [Joxit/docker-registry-ui](https://github.com/Joxit/docker-registry-ui)

## Hardware Requirements for Self-Hosting Docker Registry

The registry is a single Go binary streaming bytes, so it is bound by bandwidth, not CPU.

| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 0.5 vCPU | 1–2 vCPU |
| RAM | 256 MB | 512 MB–1 GB |
| Storage | Object storage, grows with images | Object storage; no volume needed |
| Runtime | Go 1.25 binary in an Alpine image | Same, plus Redis for multi-replica |

## Self-Hosting Docker Registry with Docker

The smallest useful local instance keeps images on a mounted volume and has no password:

```
docker run -d -p 5000:5000 --name registry \
  -v registry-data:/var/lib/registry \
  registry:3
```

Anything reachable from another machine needs TLS and a password. Generate a bcrypt entry — the only format distribution accepts — and point the registry at it:

```
htpasswd -Bbn admin 'your-password' &gt; auth/htpasswd
docker run -d -p 5000:5000 --name registry \
  -v "$PWD/auth:/auth" -v registry-data:/var/lib/registry \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Docker Registry" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  registry:3
```

Swapping the filesystem backend for S3-compatible object storage means replacing the `storage.filesystem` block in `config.yml` with a `storage.s3` block; the two cannot both be present. On Railway that file is generated at boot.

## Is Docker Registry Free?

Docker Registry is open source under the Apache 2.0 licence, free for any use including commercial, with no seat count, image limit or paid tier. On Railway you pay only for what the deployment consumes: two small containers, managed Redis, and object storage billed by the gigabyte.

## FAQ

**What is Docker Registry?**

The open-source server that stores and distributes container images, implementing the OCI Distribution Specification. Every `docker push` and `docker pull` speaks its API, whether the far end is Docker Hub or your own instance.

**What does this Railway template deploy?**

Three services: the registry on a public HTTPS domain, a web UI for browsing repositories and tags, and managed Redis for the blob-descriptor cache. Image data lives in a Railway object storage bucket.

**Why does the template include Redis and object storage?**

Object storage keeps image data off any container's disk, so the registry stays stateless and survives redeploys. Redis holds the blob-descriptor cache all replicas share, which is what makes running more than one correct.

**Can I create more than one user in self-hosted Docker Registry?**

The password-file backend has no per-user permissions — anyone who can pull can also push and delete. For separate read-only and read-write identities, distribution supports a token authentication server via its `auth.token` settings.

**How do I reclaim space after deleting images from Docker Registry?**

Deleting a tag removes the reference immediately, but the layers are freed by a separate `registry garbage-collect` pass, which upstream requires you to run against a read-only registry.


## Similar templates

- [Garage S3 Storage](https://railway.com/deploy/garage-s3-storage) — Ultra-light S3 server: fast, open-source, plug-and-play.
- [Redis](https://railway.com/deploy/redis-1) — Self Host Latest Redis with Railway
- [EasyImg](https://railway.com/deploy/easyimg) — Simple self-hostable Nuxt.js personal image hosting system.

Open this page in a browser: https://railway.com/deploy/private-docker-registry
