Tools Used in DevOps · JFrog Artifactory

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.

☺ Explain it like I'm 10

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.

🐘Your host for this topic: Ellie the Elephant — she never drops a build artifact, and Artifactory is the warehouse she runs: every package that has ever passed through gets a permanent shelf and a paper trail, in whatever format it arrived 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.

Consuming: one URL either way npm-remote caches registry.npmjs.org npm-local your own published packages npm (virtual) the one URL clients use Developer / CI npm install Promoting: build once, relabel forward dev-local build #42 lands here staging-local Xray scan runs here release-local what prod pulls from promote promote Same checksum both times — no re-upload, just a new repository path.

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.3

Build-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=true

Promotion 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.

◆ Key idea

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 first

For 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"}
})
✎ Try it

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.

⚠ Anonymous access is the default worth checking first

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.

OptionModelBest whenCosts you
JFrog ArtifactoryUniversal repo manager, ~30 formats, build-info promotion, Xray integrationA multi-language org needs one system of record and one promotion workflow across every teamLicense cost at scale; self-hosted means you operate the DB, filestore, and HA yourself
Sonatype Nexus RepositorySame universal multi-format model, strong free/OSS tierYou want broad format support without a paid license to startEnterprise 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-firstYou're single-cloud and want zero infrastructure to operateSiloed per format and per cloud; no single cross-cloud promotion workflow
GitHub Packages / GHCRMulti-format, tied to a GitHub org or repoA GitHub-centered team wants low operational overheadCoupled 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 yourselfOne artifact type, one small team, willing to build toolingNo 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.

🎬 At the Ship-It Guild
🐘

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.

✓ Checkpoint

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
  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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.