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:8888 for the web UI (or http://localhost:8080 if you map frontend to 8080). 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:

You can either use the docker-compose.yml in the repository or create your own stack using the published images on Docker Hub.

Note: Docker Compose networking uses the service name as the hostname (not container_name). If you rename the backend service, update BACKEND_UPSTREAM on the frontend service. You can set it as backend:5000 or http://backend:5000.

Sample docker-compose.yml using troke12/refity images (starter):

version: "3.9"

services:
  refity-backend:
    image: troke12/refity-backend:latest
    container_name: refity-backend
    restart: unless-stopped
    environment:
      # SFTP connection
      - FTP_HOST=yourftpserver.com
      - FTP_PORT=23
      - FTP_USERNAME=user12345
      - FTP_PASSWORD=random-your-password

      # Backend config
      - PORT=5000
      - JWT_SECRET=random-32-jwt-secret
      - SFTP_SYNC_UPLOAD=true

    volumes:
      - ./refity-data:/app/data
    ports:
      - "5000:5000"

  refity-frontend:
    image: troke12/refity-frontend:latest
    container_name: refity-frontend
    restart: unless-stopped
    depends_on:
      - refity-backend
    environment:
      # Frontend talks to the backend inside the compose network
      - BACKEND_UPSTREAM=refity-backend:5000
      - VITE_REGISTRY_URL=registry.example.com
    ports:
      - "8888:80"

With this starter, the web UI is at http://localhost:8888 and the registry API at http://localhost:5000.

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.

Backend environment variables

Variable Required Description
FTP_HOSTYesSFTP server hostname (e.g. u123456.your-storagebox.de for Hetzner)
FTP_PORTYesSFTP port (usually 22, or 23 for Hetzner Storage Box)
FTP_USERNAMEYesSFTP username
FTP_PASSWORDYesSFTP password
FTP_KNOWN_HOSTSNoPath to SSH known_hosts file for host key verification (recommended in production)
SFTP_SYNC_UPLOADNotrue = upload to SFTP before responding. false = async upload. Default: false
PORTNoBackend HTTP port. Default: 5000
JWT_SECRETProductionSecret for signing JWT tokens. Use a long random string (32+ chars).
CORS_ORIGINSNoComma-separated allowed origins for the API. Default: http://localhost:8080, http://127.0.0.1:8080
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
HETZNER_BOX_IDIf FTP_USAGE_ENABLEDHetzner Storage Box numeric ID

Frontend environment variables

The production frontend uses same-origin /api/* and nginx proxies to the backend.

Variable Required Description
BACKEND_UPSTREAMNoFrontend container only. Where nginx proxies /api and /v2. Default: backend:5000. You may set backend:5000 or http://backend:5000.
VITE_REGISTRY_URLNoRegistry host shown in “docker pull” commands in the UI (e.g. registry.example.com or localhost: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.

System overview

flowchart TB
  Docker["Docker CLI (push/pull)"] -->|"Registry API v2"| Backend["Backend (Go) :5000"]
  Browser["Browser (Web UI)"] --> Frontend["Frontend (React) :8080"]
  Frontend -->|"REST + JWT"| Backend
  Backend -->|"SSH/SFTP"| SFTP["SFTP storage (blobs + manifests)"]
  Backend --> DB["SQLite (refity.db)"]

Request flow

sequenceDiagram
  participant User
  participant Docker
  participant Frontend
  participant Backend
  participant SFTP
  participant SQLite

  Note over User,SQLite: Push image (Docker)
  User->>Docker: docker push
  Docker->>Backend: /v2/* blobs + manifests
  Backend->>SFTP: store blobs/manifests (sync or async)
  Backend->>SQLite: write metadata

  Note over User,Backend: Web UI
  User->>Frontend: open UI
  Frontend->>Backend: /api/* (JWT)
  Backend->>SQLite: read metadata
  Backend-->>Frontend: JSON

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.