Preparing to Replace Nexus

rubyrailstoolingbuilding-in-public

We’ve been running Sonatype Nexus as our artifact server for years. It hosts our internal gems, proxies rubygems.org, and serves as our Docker registry. It works. Really well, most of the time. But sometimes it doesn’t.

The failure modes are always the same. Storage fills up with no warning because Nexus doesn’t have meaningful quotas or alerts, so the first sign of trouble is a failed gem push or a Docker build that can’t pull a base image. Cleaning up old artifacts and Docker image layers is a manual process that involves navigating a clunky admin UI and hoping the compaction task actually reclaims disk space. And our personal favorite: the upstream proxy randomly stops caching. Bundler requests hit rubygems.org directly instead of the local cache, builds slow down, and nobody notices until someone checks the logs. Oh, and did I mention that the rubygems push support is now over a year outdated, and largely undocumented?

It was time to do something about Nexus…

Meet Flux

Flux is what we’re building to replace Nexus. Not a general-purpose artifact server that tries to be everything, but a registry that fits into the tool chain we already have.

Wave CI builds our projects and pushes artifacts. Meera tracks the work. Sirius2 provides the components. The artifact registry is the connective tissue. Gems flow from CI into products, Docker images flow from builds into deployments. With Nexus, that connection was whatever its API happened to support. With Flux, we can make the registry a proper part of our infrastructure, aware of our access control, our scanning policies, our deployment patterns.

The security angle is what pushed this from “nice to have” to “we need to do this.” Supply-chain attacks are getting worse: dependency confusion, typosquatting, compromised packages. Nexus offers security scanning as a paid add-on, bolted onto a system that wasn’t designed around it. We want scanning in the core. Every artifact scanned by Trivy on ingestion. Vulnerable versions flagged automatically. The ability to block insecure downloads per-repository, or lock a specific artifact with a reason when a CVE drops. And namespace confusion protection built into the virtual repository layer, where if a gem name exists in any private repo, remote versions of that name are excluded entirely. An attacker can’t publish a higher version number on rubygems.org and have it win the resolution.

On the operations side: configurable blob stores with quotas we can actually set and enforce. Content-addressed deduplication so the same Docker layer shared by ten images is stored once. TTL-based caching with explicit expiry instead of Nexus’s “maybe it caches, maybe it doesn’t” behavior. And a cleanup job that finds orphaned blobs and removes them, so disk usage is predictable instead of a ticking bomb.

What Flux looks like

Three types of repositories:

Private hosts your own artifacts. Upload a gem, push a Docker image. The standard case.

Upstream proxies an external source like rubygems.org. Fetches on demand, caches locally with configurable TTLs. Caches NOT_FOUND responses too, so a gem that doesn’t exist upstream doesn’t trigger a request every time Bundler asks.

Virtual aggregates private and upstream repos behind a single URL. When Bundler resolves dependencies, the virtual registry checks private repos first, then upstream. The namespace confusion protection lives here: if my-internal-gem exists in any private repo, no upstream version of my-internal-gem can sneak in through the virtual endpoint.

The data model goes: Repository → Component (a named package) → Artifact (a version, possibly platform-specific) → Asset (a file) → Blob (content-addressed binary). Blobs are deduplicated by SHA256 and stored in a sharded directory structure. Multiple repositories can share a blob store.

First gem pushed

As of this week, the RubyGems API is fully implemented: compact index (names, versions, info/<name>) and the modern index (specs.4.8.gz). Upload a gem, and Flux parses it, extracts metadata, creates the component/artifact/blob records, and updates the index. The index update appends rather than regenerating from scratch, which matters when you have thousands of versions.

bundle install pointed at Flux works. gem push works. Our Gemfile source is still nexus.kuy.io for now. The migration hasn’t happened yet. But the replacement is ready.

What’s next

Docker registry support. The OCI Distribution Spec for layer uploads, manifests, and content-addressable storage. And then the migration itself, pointing our builds at Flux instead of Nexus and watching to see what breaks. Given our history with Nexus, the bar for “better” isn’t very high.