Releases: troke12/refity
v1.2.3
Release notes – v1.2.3
Backend
Fix: SFTP connection pool dies permanently after idle timeout
- Symptom: After ~15 minutes of inactivity, all
docker pushoperations fail withconnection loston every blob upload. The registry returns HTTP 500 on PUT and keeps retrying indefinitely. The only recovery was a full container restart. - Cause: Hetzner Storage Box drops idle SSH connections after ~15 minutes. When
getClient()detected a stale connection and reconnection failed, it returned the already-closed client back to the pool viaputClient(). SinceputClient()never health-checked, the dead client cycled through the pool permanently — poisoning all 4 slots. - Fix: Complete rewrite of the SFTP connection pool with 5 resilience mechanisms:
1. Keepalive goroutine
A background goroutine pings all pooled connections every 5 minutes using Getwd(). Dead connections are closed and replaced before they can be checked out by a caller. This prevents Hetzner's idle timeout from ever killing connections silently.
2. Safe checkout with retry
getClient() now retries reconnection up to 3 times with exponential backoff (2s, 4s, 6s). If all retries fail, it returns nil instead of a dead client — callers receive a clear SFTP unavailable error rather than a cryptic connection lost deep in the write path.
3. Health-checked return
putClient() now verifies the connection is alive before returning it to the pool. Dead connections are discarded and a background goroutine (fillPool) attempts to replace them, restoring pool capacity without blocking the current request.
4. Partial pool initialization
NewDriverPool() no longer fails if one of the initial connections can't be established. It starts with whatever connections succeed and fills the rest in background. This prevents a transient SFTP blip during startup from taking down the entire registry.
5. Safe Reader/Writer lifecycle
Reader() and Writer() on the pooled driver previously returned the SFTP client to the pool via defer putClient() while the file handle was still in use by the caller — a use-after-return race. New poolReadCloser and poolFileWriter wrappers hold the client until the caller explicitly closes the file.
Summary
| Area | Change | Type |
|---|---|---|
| SFTP Pool | Keepalive goroutine (5 min ping interval) | Fix |
| SFTP Pool | Retry with backoff on stale connection checkout | Fix |
| SFTP Pool | Health-check on client return, auto-replace dead connections | Fix |
| SFTP Pool | Partial init + background pool fill | Fix |
| SFTP Pool | Reader/Writer hold client until file closed | Fix |
| SFTP Pool | Pool.Alive() exposes healthy connection count |
Enhancement |
| SFTP Pool | Pool.Close() for graceful shutdown |
Enhancement |
Upgrade notes
- Pull the latest backend image after CI publishes the tag:
docker pull troke12/refity-backend:v1.2.3(or:latestif you track that). - Restart the backend container. No database migration is required.
- Behavior change: None. This is a purely internal fix — the registry API is unchanged. The pool now self-heals instead of requiring manual container restarts after idle periods.
v1.2.2
Release notes – v1.2.2
Backend
Fix: Require group/image for registry push (no root-level repos)
- Symptom: Images could be pushed as
registry/myapp:tagwith no group segment, so everything landed at the registry root and did not match the UI grouping model (see issue #2). - Cause: Repository name validation allowed a single path segment; the group-path check had been skipped earlier to avoid upload errors.
- Fix: All push entry points (blob upload
POST/PATCH/PUTwith digest, and manifestPUT) now require at least one/in the repository name (group/image). Root-level names returnNAME_INVALIDwith a distribution-style JSON error.
Enhancement: Auto-create group on push
- Symptom: Operators expected to define the group implicitly on first
docker push group/imagewithout creating the group in the UI first. - Fix: After validating
group/image, the backend ensures the group exists:EnsureGroupinserts the group row idempotently in SQLite, andCreateGroupFolderensures the SFTPregistry/<group>path so the first push defines the group automatically.
Fix: SFTP PutContent file descriptor leak
- Symptom: Long-running backends could exhaust file descriptors on the SFTP side; uploads could eventually fail with
SSH_FX_FAILUREor similar. - Cause: File handles opened during
PutContentwere not always closed after writes. - Fix: Writes now close the underlying SFTP file handle when finished.
Summary
| Area | Change | Type |
|---|---|---|
| Registry | Require group/image; reject root-level push |
Fix |
| Registry + DB + SFTP | Auto-create missing group on push | Enhancement |
| SFTP | Close file handles after PutContent |
Fix |
Upgrade notes
- Pull the latest backend image after CI publishes the tag:
docker pull troke12/refity-backend:v1.2.2(or:latestif you track that). - Restart the backend container. No database migration is required.
- Behavior change: New pushes must use
group/image. Existing root-level repositories (if any) can still be pulled; migrate or re-push under a group if you want everything aligned with the UI.
v1.2.1
Release notes – v1.2.1
Backend
Fix: Blob upload 404 on cached Docker push
- Symptom:
docker pushfails withblob unknown/ repeated retries when pushing images built
with cache (layers reused from previous builds). AllPUT /v2/.../blobs/uploads/...requests return
404. - Cause: The SFTP driver's
ensureDir,PutContent, andMovefunctions checked if the group
folder existed on SFTP before creating directories. AnyStaterror — including stale SSH connections
— was treated as "repository not found", returning404 NAME_INVALIDeven for repos that already
exist with multiple tags. - Fix: Removed the strict group folder existence checks. The recursive directory creation that
already existed inensureDirnow handles all cases correctly — creating directories as needed
instead of failing.
Fix: SFTP connection pool stale connections
- Symptom: After the SFTP server restarts or SSH connections time out, all registry operations
fail until the backend is restarted. - Cause: The SFTP connection pool had no health check or reconnection logic. Broken clients were
returned from the pool indefinitely. - Fix:
getClient()now performs a health check (Getwd()) before returning a client. If the
connection is dead, it automatically reconnects and logs[SFTP] Pool: stale connection detected, reconnecting....
Fix: PATCH handler race condition on blob chunk upload
- Symptom: Intermittent digest mismatch or incomplete blob data when Docker pushes layers in
parallel across multiple connections. - Cause: The PATCH handler (
uploadBlobData) sent the202 Acceptedresponse before closing
the file writer. If Docker sent the finalizing PUT on a different connection, the file could still be
unflushed. - Fix: File writer is now closed before sending the 202 response, ensuring data is fully
persisted to disk.
Summary
| Area | Change | Type |
|---|---|---|
| Registry | Fix blob upload 404 on cached docker push |
Fix |
| SFTP | Connection pool health check + auto-reconnect | Fix |
| Registry | Close chunk file before responding to PATCH | Fix |
Upgrade notes
- Pull the latest backend image:
docker pull troke12/refity-backend:latest - Restart the backend container. No database migration or config changes needed.
v1.2.0
Release notes – v1.2.0
Breaking changes
Registry authentication required
- All
/v2/endpoints (Docker Registry HTTP API) now require Basic Auth. Previously the registry was open (designed to be protected by a reverse proxy). docker login <registry>is now mandatory beforedocker pushordocker pull.- Credentials are validated against the same user database as the web UI.
- Unauthenticated requests receive
401 UnauthorizedwithWww-Authenticate: Basic realm="Refity Registry"header, following the Docker Distribution spec.
Default admin password is now random
- On first run (empty database), the admin account is created with a randomly generated password printed to the server log. The old default
admin:adminis removed. - If you are upgrading and already have users in the database, nothing changes — existing accounts are untouched.
JWT secret no longer has a hardcoded fallback
- If
JWT_SECRETis not set, a random secret is generated per session. Tokens will be invalidated on restart. SetJWT_SECRETin production to persist sessions across restarts.
Backend
Docker pull fix: Content-Length header
- Fix:
docker pullfrom the registry failed withmissing or empty Content-Length header. - Cause: The blob download and manifest GET handlers did not set the
Content-Lengthresponse header, which the Docker daemon requires. - Change: Both
handleBlobDownloadandhandleManifest(GET) now setContent-LengthandDocker-Content-Digestheaders before writing the response body.
Multiple tags per repository
- Fix: The web UI only showed 1 tag per repository, even after pushing multiple tags (e.g.
:31,:32,:latest). - Cause: The
imagestable hadUNIQUEon both(name, tag)anddigest. When two different tags shared the same digest (or whenINSERT OR REPLACEresolved a conflict ondigest), the old row was silently deleted. - Changes:
- Removed
UNIQUEconstraint from thedigestcolumn. Multiple tags can now point to the same digest. - Replaced
INSERT OR REPLACEwithINSERT ... ON CONFLICT(name, tag) DO UPDATE— only updates when the same tag is re-pushed, without affecting other tags. - Auto-migration: On startup, if the old schema is detected (
digest TEXT NOT NULL UNIQUE), the table is automatically recreated without the constraint. Existing data is preserved.
- Removed
Security hardening
Critical
- Random JWT secret — If
JWT_SECRETis not set, a cryptographically random 64-character hex secret is generated instead of using a hardcoded default. A warning is logged. - Random admin password — First-run admin account uses a random 24-character hex password, logged to console. No more
admin:admin. - Upload state HMAC — The
packUploadState/unpackUploadStatefunctions now return an error if the secret is empty, instead of falling back to a hardcoded key.
High
- Role-based access control (RBAC) — Destructive API operations (create/delete repository, create group, delete tag) now require the
adminrole. Read-only endpoints (dashboard, list groups/repos/tags) remain accessible to all authenticated users. - SFTP host key warning — If
FTP_KNOWN_HOSTSis not configured, a warning is logged at startup:SSH host key verification is DISABLED. - Server timeouts —
ReadHeaderTimeoutreduced from 60s to 10s (Slowloris protection). AddedIdleTimeout: 120sto reclaim idle keep-alive connections. - Blob digest validation — Blob HEAD/GET endpoints now validate the digest format with
^sha256:[a-f0-9]{64}$regex, preventing path traversal via crafted digest values. - Hetzner API log sanitization — API response bodies and error details are no longer logged or returned to the client. Only the HTTP status code is logged.
Medium
- Rate limiting — Login endpoint: max 10 failed attempts per IP per 5 minutes. Registry Basic Auth: max 20 failed attempts per IP per 5 minutes. Exceeding the limit returns
429 Too Many Requests. - Tamper-proof user context — User claims (ID, username, role) are now stored in Go's
context.Contextinstead of HTTP headers, preventing spoofing via reverse proxy header injection. - Manifest ref validation — Manifest references are limited to 128 characters and reject null bytes.
- Stricter repo name validation — Repository names must start with an alphanumeric character (
^[a-zA-Z0-9]...).
Frontend
Nginx security headers
Added the following headers to nginx.conf.template:
X-Frame-Options: DENY— clickjacking protectionX-Content-Type-Options: nosniff— MIME sniffing protectionX-XSS-Protection: 1; mode=block— reflected XSS protectionReferrer-Policy: strict-origin-when-cross-origin— referrer leakage protectionPermissions-Policy: camera=(), microphone=(), geolocation=()— disable unnecessary browser APIsserver_tokens off— hide nginx version
Summary
| Area | Change | Type |
|---|---|---|
| Registry | Basic Auth required for all /v2/ endpoints |
Breaking |
| Registry | Content-Length header on blob download and manifest GET |
Fix |
| Database | Allow multiple tags per repo (remove UNIQUE on digest) |
Fix |
| Database | Auto-migration for existing databases | Enhancement |
| Security | Random JWT secret / admin password / upload state HMAC | Critical |
| Security | RBAC on destructive API operations | High |
| Security | Rate limiting on login + registry auth | Medium |
| Security | Blob digest validation, manifest ref limits, repo name strictness | Medium |
| Security | User context in context.Context (not headers) |
Medium |
| Frontend | Nginx security headers + hide server version | Enhancement |
| Server | IdleTimeout, reduced ReadHeaderTimeout |
Enhancement |
| Server | SFTP host key warning, Hetzner log sanitization | Enhancement |
Upgrade notes
- Set
JWT_SECRETin your environment if you haven't already. Without it, sessions are lost on restart. - First-time users: Check server logs for the generated admin password.
- Existing users: Your accounts and passwords are unchanged. The RBAC changes only affect non-admin users (if you have any).
- Docker clients: Run
docker login <your-registry>before push/pull. Existing~/.docker/config.jsoncredentials will continue to work. - Database migration is automatic. No manual SQL needed. A log line confirms:
Migration complete: images table updated.
v1.1.2
Release notes – v1.1.2
Frontend
Docker pull copy per tag
- On the repository page (tag list), each tag row shows the full
docker pull <registry>/<group>/<repo>:<tag>command with a Copy button. - Registry URL is taken from
VITE_REGISTRY_URLwhen set; otherwise the current host in production orlocalhost:5000in development. - Optional env: see
.env.exampleforVITE_REGISTRY_URL.
Nginx template: registry proxy
/v2location proxies to the backend with:proxy_request_buffering off– streams large PATCH bodies (layer uploads) instead of buffering, reducing server-to-server push retries.client_max_body_size 0– no upload size limit.- Long timeouts (900s) for connect, send, and read.
- Use this when exposing the registry via the frontend (e.g. NPM → refity-frontend → backend).
Backend
Docker push from same host (connection reuse)
- Fix: When the Docker daemon and Refity registry run on the same host,
docker pushcould retry on the first layer; the POST (initiate blob
upload) succeeded but the following PATCH (upload blob) did not reach the handler. - Cause: Docker reuses the same TCP connection for POST then PATCH. The handler did not read the POST body, which can break connection reuse
for the next request. - Change: The initiate-blob-upload handler now drains the request body (
io.Copy(io.Discard, r.Body)andr.Body.Close()) before handling,
so the connection is ready for the PATCH. - No API or config change; upgrade and redeploy to benefit.
Docker push behind reverse proxy ("short copy" fix)
- Fix:
docker pushthrough a reverse proxy (e.g. Cloudflare → Nginx Proxy Manager → Refity) failed withshort copy: wrote 1 of 2294while
pushing via direct IP worked fine. - Cause: Three issues combined:
- Absolute Location URLs – Upload handlers returned
Location: https://host/v2/...with scheme and host. If the proxy didn't set
X-Forwarded-Protocorrectly or the scheme mismatched, the Docker client followed the Location to the wrong endpoint and the connection was reset
after 1 byte. - Monolithic uploads ignored in async mode – Docker sends small blobs (config, ~2 KB) via
POST /v2/.../uploads/?digest=sha256:...
(monolithic). The handler only accepted this whenSFTP_SYNC_UPLOAD=true; in the default async mode it discarded the body and returned 202,
forcing an unnecessary PATCH round-trip that could fail through proxies. - Manual 100-Continue – The PATCH handler called
w.WriteHeader(100)manually, which interfered with Nginx's ownExpect: 100-continue
handling and could produce duplicate or garbled interim responses.
- Absolute Location URLs – Upload handlers returned
- Changes:
- All
Locationheaders now use relative paths (/v2/...) instead of absolute URLs — matches the official Docker Distribution (registry)
behavior and works behind any proxy without configuration. - Monolithic blob uploads (
POSTwith?digest=) now work in both sync and async modes. In async mode the blob is written to local staging
first, then uploaded to SFTP in the background. - Removed manual
w.WriteHeader(http.StatusContinue)— Go'snet/httpsends 100 Continue automatically whenr.Bodyis read.
- All
- No API or config change; upgrade and redeploy to benefit.
Summary
| Area | Change |
|---|---|
| Frontend | Per-tag docker pull copy on repository page |
| Frontend | Nginx /v2 proxy: stream body, long timeouts |
| Backend | Drain POST body in initiateBlobUpload for same-host push |
| Backend | Relative Location URLs, monolithic async uploads, remove manual 100-Continue for reverse proxy compatibility |
| Config | Optional VITE_REGISTRY_URL in .env.example |
v1.1.1
v1.1.1
Fixed
- Frontend Docker startup
- Fixed
exec /docker-entrypoint.sh: no such file or directorycaused by CRLF line endings
in the entrypoint/template files. - Frontend image build now normalizes line endings to LF during build.
- Added
.gitattributesto keep*.sh,*.template,*.confcommitted with LF.
- Fixed
- Frontend nginx upstream (
BACKEND_UPSTREAM)- Fixed nginx crash:
invalid port in upstream "http://refity-backend:5000"when
BACKEND_UPSTREAMwas provided as a full URL. BACKEND_UPSTREAMnow accepts either:backend:5000(host:port), orhttp://backend:5000(full URL)
- Entrypoint normalizes
BACKEND_UPSTREAMand renders nginx config at container startup.
- Fixed nginx crash:
Docs / Website
- Installation section includes a complete Docker Hub–based
docker-compose.ymlexample (env
inline, noenv_file). - Added syntax highlighting for YAML/JSON/Bash code blocks.
- Fixed docs anchor navigation so headings (e.g.
#production) don’t get hidden under the
sticky header. - Configuration reference split into two tables: Backend env and Frontend env.
- Architecture section shows rendered Mermaid diagrams (system overview + request flow).
- Clarified
BACKEND_UPSTREAMformat in docs (host:port or URL).
Repo / README
- README simplified and points to web documentation.
- Clickable links + centered badges (Docker pulls backend+frontend, GitHub stars, website/docs).
Notes
- Minor release focused on Docker frontend reliability + docs/UX.
- If you use Docker Hub images, pull the latest frontend image for this tag.
v1.1.0
Release notes – v1.1.0
Highlights
- FTP Usage optional – Dashboard can run without Hetzner; when disabled, only Total Images, Total Groups, and Total Size are shown.
- Dashboard & tag size fixes – Total image count and tag size (including multi-arch) now stay correct after push.
- Responsive UI – Improved layout and navigation on mobile and tablet.
- Config & docs – Reorganized
.env.exampleand updated README.
New features
FTP Usage (Hetzner) – optional
- FTP Usage card on the dashboard is off by default so the app works for users who don’t use Hetzner Storage Box.
- Set
FTP_USAGE_ENABLED=truein.envand configureHCLOUD_TOKENandHETZNER_BOX_IDto show the FTP Usage card and fetch usage from the Hetzner API. - When disabled, the dashboard only shows Total Images, Total Groups, and Total Size (no API calls to Hetzner).
Bug fixes
- Dashboard cache – After pushing an image, the dashboard now refreshes correctly (total images, repo count, and stats).
- Tag size (multi-arch) – Tag size is now computed from the sum of layer sizes of each platform manifest (e.g. ~135 MB) instead of the manifest list JSON size (e.g. 3.99 KB).
- Mobile menu – Separator lines in the burger menu now span the full width on expand.
Improvements
Environment & configuration
.env.example– Reorganized into sections:- Storage (SFTP)
- Server
- Security & Auth
- Dashboard – FTP Usage (Hetzner)
- Frontend (Vite)
- Each variable has a short comment (required/optional, default, usage).
- README – Documented FTP Usage default and when to enable it for Hetzner.
UI / layout
- Dashboard – When FTP Usage is hidden, the three stat cards (Total Images, Total Groups, Total Size) use equal width and fill the row.
- Responsive layout (mobile & tablet):
- Navbar – Hamburger menu from 991px down; 48px touch targets; consistent padding.
- Repository (tag list) – On tablet/mobile, tags are shown as cards instead of a table; Docker pull and actions are easier to use on small screens.
- Dashboard – Stats and group cards use responsive columns (e.g. 1 column on small mobile, 2 on larger mobile, 4 on desktop).
- Group page – Repo grid: 1 / 2 / 3 columns by breakpoint; delete button has a minimum tap size.
- Login, Footer, StatCard, modals – Adjusted spacing and typography for small screens.
- Breakpoints used: 991px (tablet), 576px (mobile).
Upgrade from v1.0.0
- No breaking changes.
- Default change: FTP Usage is now disabled by default. If you use Hetzner Storage Box and want the usage card, set
FTP_USAGE_ENABLED=truein your.envand keepHCLOUD_TOKENandHETZNER_BOX_IDset. - Optional: Refresh your
.envlayout using the new.env.examplesections (your existing variable names and values stay valid).
v1.0.0
Refity v1.0.0
First stable release. Refity is a self-hosted Docker private registry with SFTP backend storage and a modern React web UI.
Highlights
- Docker Registry API v2 — Push and pull images with the standard Docker CLI
- SFTP storage — All image data stored on your SFTP server; no cloud lock-in
- React web UI — Dashboard, group-based repos, tag management, profile & change password
- JWT auth — Secure login; production options for JWT secret, CORS, and SSH host verification
- Docker images —
troke12/refity-backendandtroke12/refity-frontendon Docker Hub
Features
Registry & storage
- Docker Registry v2 API compatible
- SFTP backend for blobs, manifests, and metadata
- Async SFTP upload with progress and retry
- Strict group/folder control (no auto-create; push fails if group missing)
- Multi-architecture support (manifest list validation)
- Digest validation and
Docker-Content-Digestheaders - Tag listing and per-tag Docker pull copy in the UI
Web UI
- Dashboard with stats and group cards
- Group-based navigation: create groups, then repositories under each group
- Create/delete repositories and tags
- Profile page with change-password form
- Login, logout, JWT-protected API calls
- Responsive layout (desktop and mobile)
Security (production)
- JWT_SECRET — Configurable via env (required in production)
- CORS_ORIGINS — Configurable allowed origins
- FTP_KNOWN_HOSTS — Optional SSH host key verification
- Path traversal fixes (repo name validation, local driver path checks)
- SECURITY.md with production checklist
Docker images
| Image | Description |
|---|---|
| troke12/refity-backend | Go server: Registry API + REST API, SFTP, SQLite |
| troke12/refity-frontend | React app served by nginx |
Tags: latest and v1.0.0 (and future version tags on release).
Quick start
git clone https://github.com/troke12/refity.git
cd refity
cp .env.example .env
# Edit .env with FTP_* and JWT_SECRET
docker-compose up -d- Frontend: http://localhost:8080
- Backend API: http://localhost:5000
- Default login:
admin/admin— change after first login.
Documentation
- README — Quick start, env, API, architecture
- SECURITY.md — Production security checklist
Full changelog
- Registry: v2 API, SFTP driver, async upload, retry, digest validation, tag list, multi-arch manifest validation, strict group/folder
- Backend: Go server, SQLite, JWT auth, REST API (auth, dashboard, groups, repos, tags, profile/change password), config (JWT, CORS, FTP known hosts)
- Frontend: React dashboard, groups, repositories, tags, profile, login/logout, Docker pull copy
- CI: GitHub Actions workflow to build and push images to Docker Hub on version tags
- Docs: README with Mermaid architecture, SECURITY.md, MIT license, updated screenshot