Documentation

Refity is a self-hosted Docker private registry that stores all image blobs and manifests on an SFTP server. It implements the Docker Registry HTTP API v2 and adds a REST API and web UI for managing groups and repositories.

Concepts

Requirements

Quick start

  1. Clone the repository and copy the example env file:
git clone https://github.com/troke12/refity.git
cd refity
cp .env.example .env
  1. Edit .env and set at least: FTP_HOST, FTP_PORT, FTP_USERNAME, FTP_PASSWORD. For production, set JWT_SECRET.
  2. Start the stack:
docker-compose up -d

Open http://localhost:8080 for the web UI. Default login: admin / admin — change the password after first login.

The registry API is on port 5000. Configure Docker to use it as an insecure registry, or put a reverse proxy with TLS in front.

Installation details

Refity runs with Docker Compose. Two services are started:

On first run, the backend creates the SQLite database and a default user admin / admin. Change the password via the web UI (Profile) before exposing the service.

Ensure the host that runs Docker can reach your SFTP server (port 22 or 23). If you use FTP_KNOWN_HOSTS, the file must be available inside the backend container (mount it or bake into image).

Configuration reference

All configuration is via environment variables. See .env.example in the repo for a categorized list. Summary:

Variable Required Description
FTP_HOSTYesSFTP server hostname (e.g. u123456.your-storagebox.de for Hetzner)
FTP_PORTYesSFTP port (usually 22 or 23 for Hetzner)
FTP_USERNAMEYesSFTP username
FTP_PASSWORDYesSFTP password
JWT_SECRETProductionSecret for signing JWT tokens. Use a long random string (32+ chars).
CORS_ORIGINSNoComma-separated allowed origins for the API (e.g. your frontend URL). Default: http://localhost:8080, http://127.0.0.1:8080
FTP_KNOWN_HOSTSNoPath to SSH known_hosts file for host key verification. Recommended in production.
SFTP_SYNC_UPLOADNotrue = upload to SFTP before responding (slower, file on SFTP when push completes). false = async upload. Default: false
FTP_USAGE_ENABLEDNotrue only if using Hetzner Storage Box and want the usage card on the dashboard. Default: false
HCLOUD_TOKENIf FTP_USAGE_ENABLEDHetzner Cloud API token (same as HCLOUD token)
HETZNER_BOX_IDIf FTP_USAGE_ENABLEDHetzner Storage Box numeric ID
PORTNoBackend HTTP port. Default: 5000

Architecture

Refity consists of two services:

Push flow: Docker client pushes layers and manifest to the backend. The backend writes layers to local temp storage (or streams in sync mode), then uploads to SFTP. Manifest is stored on SFTP by digest and by tag. Metadata is written to SQLite. Dashboard cache is invalidated so the UI reflects the new image.

Pull flow: Docker client requests manifest and blobs. The backend reads from SFTP (and serves from local cache when available) and returns them. No SQLite involved for pull.

Using the registry

Push an image

  1. Create a group in the web UI (e.g. myteam). Groups are not auto-created on push.
  2. Tag your image: docker tag myimage localhost:5000/myteam/myimage:latest
  3. Push: docker push localhost:5000/myteam/myimage:latest

If your registry is behind a domain, replace localhost:5000 with your registry host and configure Docker (insecure registry or HTTPS).

Pull an image

docker pull your-registry/myteam/myimage:latest

You can copy the exact docker pull command from the web UI for each tag.

Delete tag or repository

From the web UI you can delete a single tag or the entire repository. Deleting a repository removes its data from SQLite and deletes the repository folder on SFTP.

Docker daemon: insecure registry

If your registry is HTTP (no TLS), configure the Docker daemon to allow it. Edit /etc/docker/daemon.json (Linux) or Docker Desktop settings:

{
  "insecure-registries": ["localhost:5000"]
}

Replace localhost:5000 with your registry host:port. Restart Docker after changing.

Production deployment

Backup & restore

Security

API reference

Docker Registry API v2

Refity implements the standard Docker Registry HTTP API v2 under /v2/. All docker push and docker pull operations use this API. Authentication for the registry is not required by default; the web UI uses the separate REST API with JWT.

REST API (web UI)

All REST endpoints are under /api/ and require a valid JWT in the Authorization: Bearer <token> header (except login).

Example: login and get dashboard:

# Login
TOKEN=$(curl -s -X POST http://localhost:5000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin"}' | jq -r '.token')

# Get dashboard
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/dashboard

Troubleshooting

FAQ

Why SFTP and not S3?

Refity is the only Docker registry that uses SFTP as the primary storage backend. That lets you use Hetzner Storage Box, existing SFTP servers, or NAS without adding S3/GCS. See Why SFTP? for a comparison with Harbor and Nexus.

Does Refity support multi-architecture images?

Yes. Manifest lists (multi-arch) are supported. Tag size in the UI is computed from the sum of layer sizes of the referenced manifests (compressed size).

Can I use Refity without Hetzner?

Yes. Any SFTP server works. Set FTP_USAGE_ENABLED=false (default) so the dashboard does not call the Hetzner API. The UI then shows only total images, total groups, and total size.

Where are images stored on SFTP?

Under registry/<group>/<repo>/: blobs/ for layer blobs (by digest), and manifests/ for manifest blobs (by digest and by tag).

How do I backup my registry?

Back up the SFTP storage (all blobs and manifests) and the SQLite database file (refity.db) if you want to preserve metadata. The database can be recreated from SFTP in principle, but backing both is simpler. See Backup & restore above.

Can I change the backend port?

Yes. Set PORT (e.g. 5001). Update Docker Compose port mapping and any reverse proxy or insecure-registries configuration.

Why does the UI show “compressed” size for tags?

Image manifests store layer sizes as compressed (blob) sizes. The UI shows that sum. Docker Desktop often shows “virtual size” (uncompressed on disk). Both are correct; they measure different things.

More details?

See the README on GitHub for architecture diagrams, development setup, and contribution guidelines.