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
- Group — A top-level namespace (e.g.
myteam). You create groups in the web UI. No group is auto-created on push; if you push to a non-existent group, the push will fail. This keeps structure and access under your control. - Repository — A named image repository under a group, e.g.
myteam/myapp. Created when you first push an image to that name (or you can create an empty repo from the UI if the project supports it). - Tag — A named reference to a manifest (e.g.
latest,v1.0). One repository can have many tags. Deleting a tag removes the tag reference and optionally the manifest if unreferenced. - Digest — Content-addressable hash of a blob or manifest (e.g.
sha256:abc123...). Layers and manifests are stored on SFTP by digest. Pull by digest is supported. - Manifest list — Multi-architecture image: a manifest that references several platform-specific manifests. Refity supports push and pull of manifest lists; the UI shows the total size from the sum of layer sizes of the referenced manifests.
Requirements
- Docker and Docker Compose
- An SFTP server (e.g. Hetzner Storage Box, any VPS with SSH/SFTP, or SFTP-compatible storage)
Quick start
- Clone the repository and copy the example env file:
git clone https://github.com/troke12/refity.git
cd refity
cp .env.example .env
- Edit
.envand set at least:FTP_HOST,FTP_PORT,FTP_USERNAME,FTP_PASSWORD. For production, setJWT_SECRET. - 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:
- Backend — Exposes port
5000(configurable viaPORT). Needs access to your SFTP server (outbound). Persists SQLite in a volume (e.g../dataor a named volume) so metadata survives restarts. - Frontend — Built with Vite; typically served by nginx in the container, port
8080. Calls the backend at the URL you set (e.g.VITE_API_URLwhen building).
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_HOST | Yes | SFTP server hostname (e.g. u123456.your-storagebox.de for Hetzner) |
FTP_PORT | Yes | SFTP port (usually 22 or 23 for Hetzner) |
FTP_USERNAME | Yes | SFTP username |
FTP_PASSWORD | Yes | SFTP password |
JWT_SECRET | Production | Secret for signing JWT tokens. Use a long random string (32+ chars). |
CORS_ORIGINS | No | Comma-separated allowed origins for the API (e.g. your frontend URL). Default: http://localhost:8080, http://127.0.0.1:8080 |
FTP_KNOWN_HOSTS | No | Path to SSH known_hosts file for host key verification. Recommended in production. |
SFTP_SYNC_UPLOAD | No | true = upload to SFTP before responding (slower, file on SFTP when push completes). false = async upload. Default: false |
FTP_USAGE_ENABLED | No | true only if using Hetzner Storage Box and want the usage card on the dashboard. Default: false |
HCLOUD_TOKEN | If FTP_USAGE_ENABLED | Hetzner Cloud API token (same as HCLOUD token) |
HETZNER_BOX_ID | If FTP_USAGE_ENABLED | Hetzner Storage Box numeric ID |
PORT | No | Backend HTTP port. Default: 5000 |
Architecture
Refity consists of two services:
- Backend (Go) — Port 5000. Serves the Docker Registry API (
/v2/*) and a REST API for the web UI (/api/*). Uses a local driver for buffering during push, then uploads blobs and manifests to SFTP. Metadata (repos, tags, image sizes) is stored in SQLite. - Frontend (React + Vite) — Port 8080. Web UI for login, dashboard, groups, repositories, and tag listing. Talks to the backend REST API with JWT.
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
- Create a group in the web UI (e.g.
myteam). Groups are not auto-created on push. - Tag your image:
docker tag myimage localhost:5000/myteam/myimage:latest - 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
- TLS: Run a reverse proxy (nginx, Caddy, Traefik) in front of the backend and frontend. Terminate HTTPS there and proxy to
localhost:5000(backend) andlocalhost:8080(frontend), or serve the frontend as static files from the same domain. - Env: Set
JWT_SECRETto a long random value. SetCORS_ORIGINSto your frontend origin(s). Do not commit.env; use secrets or env injection. - SFTP: Use
FTP_KNOWN_HOSTSso the backend verifies the SFTP host key. Prefer a dedicated SFTP user with access only to the path used by Refity. - Persistence: Mount the directory that contains
refity.db(and any temp storage) so data survives container restarts.
Backup & restore
- SFTP: Back up the entire Refity tree on SFTP (e.g.
registry/and any other top-level path you use). Use your provider’s snapshot orrsync/rcloneover SFTP. - SQLite: Back up
refity.dbfrom the backend container or host volume. A simple copy is enough; for minimal downtime usesqlite3 .backupor run the copy during low traffic. - Restore: Restore SFTP content and
refity.dbto the same paths, then restart Refity. Ensure SFTP credentials and paths match the original setup.
Security
- JWT: Set
JWT_SECRETin production. Do not use the default. - CORS: Set
CORS_ORIGINSto the exact origin(s) of your frontend. - SFTP host key: Use
FTP_KNOWN_HOSTSwith a path to aknown_hostsfile to verify the SFTP server and avoid MITM. - Credentials: Do not commit
.env. Use secrets management or env injection in your deployment. - TLS: In production, put the registry behind a reverse proxy (e.g. nginx, Caddy) with HTTPS. Configure Docker clients to trust the registry (insecure registry or valid TLS).
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).
POST /api/auth/login— Body:{"username":"admin","password":"..."}. Returns{"token":"..."}.GET /api/auth/me— Current user (requires JWT).PUT /api/auth/password— Body:{"current_password":"...","new_password":"..."}(requires JWT).GET /api/dashboard— Dashboard data (groups, total images, total size; optional FTP usage if enabled).GET /api/groups— List groups.POST /api/groups— Body:{"name":"myteam"}. Create group.GET /api/groups/:group/repositories— List repositories in a group (with tags).GET /api/groups/:group/repositories/:repo/tags— List tags for a repository.DELETE /api/repositories/:name— Delete a repository (name isgroup/repo).DELETE /api/repositories/:name/tags/:tag— Delete a tag.GET /api/ftp/usage— Hetzner Storage Box usage (only whenFTP_USAGE_ENABLED=true).
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
- Push fails with “repository not found” or 404: Ensure the group exists. Create the group in the web UI first, then push to
group/repo:tag. - Cannot connect to SFTP: Check
FTP_HOST,FTP_PORT, firewall, and credentials. For Hetzner Storage Box use port23if required. If usingFTP_KNOWN_HOSTS, ensure the file is correct and mounted. - Dashboard shows wrong total images or size: Dashboard is cached. Push invalidates the cache; if you changed data outside Refity, restart the backend or wait for cache expiry.
- Tag size shows as very small (e.g. few KB): For manifest lists, size is computed from sub-manifests’ layers. If a sub-manifest is missing on SFTP or fetch fails, that part is skipped; check backend logs and SFTP layout.
- CORS errors in browser: Set
CORS_ORIGINSto the exact origin of the frontend (e.g.https://refity.example.com). No trailing slash.
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.