JFrog Artifactory
JFrog Artifactory is the enterprise answer to the artifact-repository idea build & artifact management introduced conceptually: instead of running one registry per package format — a Docker registry here, a private npm registry there, a PyPI server somewhere else — Artifactory is a single "universal" repository manager that speaks roughly thirty different package protocols (Docker, npm, Maven, PyPI, Helm, NuGet, Go, generic files, and more) behind one URL, one permission model, and one audit trail. It also proxies and caches upstream public registries so a flaky registry.npmjs.org outage doesn't take your build down with it, and it tracks the exact set of artifacts and dependencies each CI build produced as a single build-info record you promote as one atomic unit from dev through staging to production — the concrete, at-scale version of "build once, promote everywhere." This page assumes that page's vocabulary and goes straight to Artifactory's own architecture: repository types, the config you actually write, build-info promotion, the CLI, and the gotchas that bite.
Picture one giant warehouse that can shelve any kind of package — books, electronics, frozen food — each in its own correctly labeled aisle, instead of a separate specialty warehouse for every product type across town. Delivery trucks (your CI pipeline) drop packages off at one loading dock. When someone asks for something the warehouse doesn't stock yet — a public library nobody's fetched before — the warehouse quietly orders it from the manufacturer once, keeps a copy on its own shelf, and hands that same copy to everyone who asks next time, so the manufacturer's factory only gets bothered once. And when a shipment is ready to move from the loading dock to the store shelf to the delivery truck, the warehouse doesn't repack it at each stop — it just relabels which aisle the same sealed box is sitting in.
What Artifactory is, and the problem of one registry per format
☺ Like you're 10: Instead of a different shed for every kind of package, Artifactory is the one warehouse that can hold all of them, so you only have to guard, back up, and search one place.
JFrog's Artifactory began life around 2006 as an open-source Maven repository manager, and the company built around it grew the product into what it now calls a "universal" artifact repository — one that natively understands the resolution and publish protocols of Maven, Gradle, npm, PyPI, Docker (as an OCI registry), Helm, NuGet, Go, Conan, Debian, RPM, Terraform modules, generic binaries, and more, all inside a single server. A repository inside Artifactory is still typed to exactly one package format — an npm repo speaks npm's protocol, a docker repo speaks the OCI/Docker v2 API — but nothing stops one Artifactory instance from hosting dozens of repos across a dozen formats at once, sharing one authentication system, one permission model, and one audit trail instead of a different login and a different backup story per language ecosystem.
It ships in several editions worth knowing apart: a free JFrog Container Registry tier limited mostly to Docker, Helm, and generic repos; Pro, which unlocks every package format; and Enterprise/Enterprise+, which add high availability, multi-site replication, and deeper integration with JFrog Xray — a separate but tightly coupled product that scans artifacts for known vulnerabilities and license issues and can gate a promotion on the result. All of it is available self-hosted or as JFrog's own SaaS ("JFrog Platform" / *.jfrog.io). Edition boundaries and pricing move fairly often — verify the current feature matrix on JFrog's own site before committing to a tier.
Repository architecture: local, remote, and virtual
☺ Like you're 10: Local is your own shelf, remote is a shelf that automatically restocks from someone else's warehouse, and virtual is one door that opens onto both shelves at once.
Nearly every Artifactory concept reduces to three repository types, and getting them straight is most of learning the tool.
A local repository physically stores artifacts you deploy into it — your CI pipeline's own npm packages, your own Docker images, your own Maven releases. A remote repository is a caching proxy in front of an external registry: it forwards a request to the configured upstream (say, https://registry.npmjs.org) the first time an artifact is asked for, stores a copy locally, and serves every later request for that same artifact straight from cache — cutting external bandwidth, surviving an upstream outage, and giving you one internal audit point for every third-party package your organization pulls. A virtual repository aggregates one or more local and remote repositories behind a single URL, so a client only ever configures one endpoint (.../api/npm/npm/) and Artifactory resolves the request against the underlying repos in a configured order, checking your own published packages before falling through to the cached upstream copy.
Config you actually write: repos and client endpoints
☺ Like you're 10: You describe each shelf in a small JSON block, then point every tool — npm, pip, Maven, Docker — at the warehouse's one front door instead of the internet.
Repositories are created and edited through Artifactory's REST API (or the UI, which is a thin wrapper over the same calls), with a small JSON body per repository declaring its type, its package format, and — for a remote — the upstream URL it fronts.
# local repo — your own published npm packages land here
$ curl -X PUT -u ci-bot:$TOKEN -H "Content-Type: application/json" \
https://mycompany.jfrog.io/artifactory/api/repositories/npm-local \
-d '{"rclass":"local","packageType":"npm"}'
# remote repo — proxies and caches registry.npmjs.org
$ curl -X PUT -u ci-bot:$TOKEN -H "Content-Type: application/json" \
https://mycompany.jfrog.io/artifactory/api/repositories/npm-remote \
-d '{"rclass":"remote","packageType":"npm","url":"https://registry.npmjs.org","storeArtifactsLocally":true}'
# virtual repo — one endpoint clients actually configure
$ curl -X PUT -u ci-bot:$TOKEN -H "Content-Type: application/json" \
https://mycompany.jfrog.io/artifactory/api/repositories/npm \
-d '{"rclass":"virtual","packageType":"npm","repositories":["npm-local","npm-remote"]}'Most platform teams manage these declaratively instead of via one-off curl calls, using the community-maintained jfrog/artifactory Terraform provider — the same discipline Terraform applies to cloud resources, applied to the registry itself. Check the provider's current version and resource names on the Terraform Registry before pinning; provider schemas change across major versions.
resource "artifactory_local_npm_repository" "npm_local" {
key = "npm-local"
}
resource "artifactory_remote_npm_repository" "npm_remote" {
key = "npm-remote"
url = "https://registry.npmjs.org"
}
resource "artifactory_virtual_npm_repository" "npm" {
key = "npm"
repositories = [
artifactory_local_npm_repository.npm_local.key,
artifactory_remote_npm_repository.npm_remote.key,
]
}Every package manager then points at Artifactory the same way it would point at the public registry it's replacing — the only thing that changes is the URL and a credential.
# .npmrc
registry=https://mycompany.jfrog.io/artifactory/api/npm/npm/
//mycompany.jfrog.io/artifactory/api/npm/npm/:_authToken=${ARTIFACTORY_TOKEN}
# pip.conf
[global]
index-url = https://ci-bot:${ARTIFACTORY_TOKEN}@mycompany.jfrog.io/artifactory/api/pypi/pypi/simple
# Maven settings.xml — mirror everything through the virtual repo, credentials on the matching server id
<mirrors><mirror><id>central</id><mirrorOf>*</mirrorOf>
<url>https://mycompany.jfrog.io/artifactory/maven</url></mirror></mirrors>
<servers><server><id>central</id><username>ci-bot</username>
<password>${env.ARTIFACTORY_TOKEN}</password></server></servers>
# Docker — login once, then tag/push into a local repo like any other registry
$ docker login mycompany.jfrog.io
$ docker tag checkout:1.4.3 mycompany.jfrog.io/docker-local/checkout:1.4.3
$ docker push mycompany.jfrog.io/docker-local/checkout:1.4.3Build-info and promotion: enterprise-scale build-once-promote-everywhere
☺ Like you're 10: Artifactory doesn't remember one jar at a time — it remembers the whole build that produced a dozen files together, and moves all of them forward as one package.
A plain artifact upload only records a file. Build-info is Artifactory's answer to something bigger: a JSON record, published alongside the artifacts themselves, capturing every dependency a specific CI build resolved and every artifact it produced, tied to a build name and number. The JFrog CLI (or the native Jenkins/Maven/Gradle Artifactory plugins) assembles this automatically as your build runs, then publishes it as one call.
$ jf c add my-server --url=https://mycompany.jfrog.io --user=ci-bot --password=$TOKEN
$ jf rt upload "target/checkout-1.4.3.jar" libs-snapshot-local/com/acme/checkout/1.4.3/
$ jf rt build-collect-env checkout-ci 42 # capture env vars into the build-info
$ jf rt build-add-git checkout-ci 42 # attach the git commit and branch
$ jf rt build-publish checkout-ci 42 # publish the whole build-info record
# promote the ENTIRE build's artifacts as one atomic unit — not one file at a time
$ jf rt build-promote checkout-ci 42 libs-staging-local --status=staged
$ jf rt build-promote checkout-ci 42 libs-release-local --status=released --copy=truePromotion is fast regardless of artifact size because of how Artifactory stores content: every file is stored once, addressed by its SHA-256 checksum, in a filestore separate from the metadata database. A repository "path" is just a pointer at that checksum. Promoting a build from libs-staging-local to libs-release-local doesn't re-upload anything — it adds (or moves) a path pointing at content that's already sitting in the filestore, which is also why identical binary content referenced from ten different repo paths only ever occupies storage once.
The promotion API's copy field defaults to false — meaning a promotion moves the build's artifacts to the target repository by default, it does not leave a copy behind in the source repo. Pass --copy=true (CLI) or "copy": true (REST) explicitly if you want the artifacts to remain visible in libs-staging-local after they've also landed in libs-release-local. Confirm this against JFrog's current docs before relying on it in a pipeline — API defaults are exactly the kind of behavior platforms occasionally revise between major versions.
This is the enterprise-scale version of the rule build & artifact management introduced: the twelve files a build produced move together, tied to one build-info record naming the exact commit, dependencies, and environment that produced them — so "what actually shipped in release 1.4.3" is a single build-info lookup, not a guess reconstructed from a dozen separate upload timestamps.
Day-to-day commands: the JFrog CLI and AQL
☺ Like you're 10: One command-line tool covers upload, download, publish, promote, and search — and a small query language finds artifacts by their labels instead of their file paths.
$ jf rt download "libs-release-local/com/acme/checkout/1.4.3/*" ./out/
$ jf rt search "libs-release-local/com/acme/checkout/**"
$ jf rt docker-pull mycompany.jfrog.io/docker-local/checkout:1.4.3 docker-local
$ jf rt build-scan checkout-ci 42 # trigger an Xray scan against this build's artifacts
$ jf rt del "libs-snapshot-local/com/acme/checkout/*" --dry-run # always dry-run a delete firstFor anything the CLI's flags don't cover, Artifactory Query Language (AQL) is the tool's own search language — a JSON-shaped query against artifact metadata and properties, useful for cleanup jobs and audits that a simple path glob can't express.
items.find({
"repo": {"$eq": "libs-snapshot-local"},
"created": {"$before": "60d"}
})On a free JFrog SaaS trial instance (or a local Docker run of the Community Edition image): create an npm-local, an npm-remote pointing at registry.npmjs.org, and an npm virtual repo aggregating both, exactly as shown above. Point a throwaway project's .npmrc at the virtual repo and run npm install lodash — watch it resolve through the cache on the Artifactory UI's "Artifacts" tree. Then npm publish a trivial package of your own into npm-local and confirm it resolves from the same virtual URL, right alongside the cached public package.
Gotchas and failure modes
☺ Like you're 10: Most surprises come from forgetting that "delete" and "cache" don't mean what they'd mean on your own laptop.
Docker repo resolution needs a routing decision up front
Unlike npm or PyPI, the Docker v2 API doesn't have a repository name in its URL scheme the way a package manager does — it expects one registry per hostname. Artifactory resolves this one of two ways: the repository-path method, where the repo key is embedded in the URL path behind Artifactory's own API route, or a reverse-proxy / subdomain method, where each Docker repo gets routed by hostname or port through an in-front reverse proxy (JFrog's own SaaS handles this transparently). Pick the wrong one, or skip configuring the reverse proxy, and docker push fails with an opaque "repository name not known to registry" error that has nothing to do with permissions. Confirm the current recommended method against JFrog's docs for your deployment (self-hosted vs. SaaS), since it's one of the areas configuration guidance shifts as Artifactory ships updates.
Deleting a path doesn't immediately free storage
Because storage is checksum-addressed, removing an artifact from a repo only removes that repo's pointer to the underlying content — the actual bytes in the filestore aren't reclaimed until Artifactory's background garbage collection runs and confirms no repository anywhere still references that checksum. Space you expect to see freed right after a bulk delete may not show up until the next GC cycle.
Remote caches grow without limit unless you set retention
A remote repository's cache has no default expiry on the artifacts it has already fetched — every distinct version ever requested stays cached indefinitely unless a cleanup policy or retention period is configured. Left unmanaged, this is a slow, quiet storage leak, especially for high-churn ecosystems like npm. The opposite misconfiguration also bites: an overly aggressive eviction period can quietly drop a cached artifact that has since been pulled from the real upstream (an npm unpublish, a yanked crate), breaking a build that assumed the cache would always have it.
Older Artifactory installs shipped with anonymous read access enabled by default, and plenty of upgraded instances still carry that setting forward unnoticed. If the server is reachable at all from outside your network, anonymous access means anyone can browse the repository tree and enumerate your internal package names, versions, and dependency graph — reconnaissance a supply-chain attacker would otherwise have to work for. Check Security → Settings → Allow Anonymous Access before anything else on a new instance.
Artifactory vs. its alternatives
☺ Like you're 10: Other tools also hold packages — some cover every format like Artifactory does, some are free but narrower, some only handle one cloud's containers.
| Option | Model | Best when | Costs you |
|---|---|---|---|
| JFrog Artifactory | Universal repo manager, ~30 formats, build-info promotion, Xray integration | A multi-language org needs one system of record and one promotion workflow across every team | License cost at scale; self-hosted means you operate the DB, filestore, and HA yourself |
| Sonatype Nexus Repository | Same universal multi-format model, strong free/OSS tier | You want broad format support without a paid license to start | Enterprise security scanning (IQ/Firewall) is a separate paid product; smaller native CI plugin ecosystem |
| Cloud-native registries (ECR, Artifact Registry, ACR) | Managed by the cloud provider, historically Docker/OCI-first | You're single-cloud and want zero infrastructure to operate | Siloed per format and per cloud; no single cross-cloud promotion workflow |
| GitHub Packages / GHCR | Multi-format, tied to a GitHub org or repo | A GitHub-centered team wants low operational overhead | Coupled to GitHub's own permission model and rate limits; not built for enterprise cross-org promotion |
| Plain object storage (S3 + a custom index) | DIY — you build addressing, indexing, and auth yourself | One artifact type, one small team, willing to build tooling | No promotion workflow, no package-manager protocol support — you rebuild what Artifactory already solved |
The practical rule mirrors the one Helm settles on for charts: reach for a universal manager like Artifactory or Nexus when the artifact must travel across teams and formats and you want promotion and audit built in; reach for a single cloud-native registry when you're genuinely single-format, single-cloud, and want one less system to operate. Most large platforms land on Artifactory (or Nexus) as the system of record for everything, with cloud-native registries occasionally mirrored underneath for pull-through latency in a specific region. See the DevOps toolchain for where this sits among the platform's other named tools, and supply-chain security & SBOM for what happens once Xray or a similar scanner is wired into the promotion gate.
Ellie: Build 42 is done — twelve artifacts, one build-info record. I'm not letting anyone touch the jars one at a time.
Benny: Already ran jf rt build-publish checkout-ci 42 — every dependency it resolved and every file it produced is on record now.
Foxy: So why not just grab the jar from staging and drop it into the release repo ourselves? It's the same jar.
Ellie: Because then the release repo has a jar with no build behind it. build-promote moves the whole build as one atomic unit, keyed to the build-info I already recorded — and it's instant, since I'm only relabeling a checksum I already have.
Timmy: And I'm not approving the promotion until Xray's scan on build 42 comes back clean. A promoted build with an unscanned critical CVE isn't a promotion, it's a liability with a new label.
Gizmo: Or you could just docker pull the image off the CI runner and retag it into the release repo yourself. Five seconds, no waiting on Xray. 🤑
Ellie: And no build-info behind it either. Wait for the scan, Gizmo.
1. Distinguish a local, a remote, and a virtual repository in one sentence each. 2. Why is promoting a build in Artifactory fast regardless of artifact size, and what does that tell you about how the filestore is organized? 3. Does the promotion API copy or move a build's artifacts by default, and what flag changes that? 4. Name two distinct storage or access gotchas covered on this page, and what each one actually breaks if ignored. 5. When would you reach for Artifactory over a single cloud-native container registry, and when would the reverse be true?
Check your answers
- A local repo physically stores artifacts you deploy into it. A remote repo proxies and caches an external upstream registry. A virtual repo aggregates one or more local and remote repos behind a single URL so clients only configure one endpoint.
- Storage is checksum-addressed — every file is stored once in a filestore, keyed by its SHA-256 hash, and a repository path is just a pointer at that checksum. Promoting a build only adds or moves a pointer to content that already exists; it never re-uploads the bytes, which is why promotion time doesn't scale with artifact size.
- It defaults to move (
copy: false) — the artifacts land in the target repo and are removed from the source repo unless you explicitly pass--copy=true(CLI) or"copy": true(REST). - Any two of: deleting a path doesn't free filestore space until garbage collection runs and confirms no repo still references that checksum, so storage seems to lag behind a bulk delete; unmanaged remote-repo caches grow indefinitely without a retention policy, quietly consuming storage, while an overly aggressive eviction period can drop a cached artifact the real upstream has since removed and break a build that assumed it; the Docker repo-path vs. reverse-proxy resolution method has to be configured correctly or pushes fail with an opaque error; anonymous read access, if left enabled, lets anyone reachable enumerate your internal package names and dependency graph.
- Reach for Artifactory (or a similar universal manager) when multiple teams need many package formats behind one system of record with build-info promotion and audit built in. A single cloud-native registry is the better fit when you're genuinely single-format and single-cloud and want one less system to operate — at the cost of no shared cross-format, cross-cloud promotion workflow.