# JFrog Artifactory for DevOps — Complete Implementation Guide with CI/CD Integration
Every production pipeline I have worked on eventually hits the same wall: where do the artifacts live, who built them, and can we trust them? If your team is still pulling dependencies directly from public registries during builds, promoting containers by retagging in Docker Hub, or storing Helm charts on a shared NFS mount, you are carrying technical debt that compounds with every deployment. JFrog Artifactory solves this by providing a single, universal artifact repository that speaks every package manager protocol your organization uses.
This guide walks through a full implementation — from repository topology to CI/CD integration to security scanning — based on patterns I have deployed across e-commerce, financial services, and multi-team enterprise environments.
What Is JFrog Artifactory
JFrog Artifactory is a universal binary repository manager. Unlike single-purpose registries (Docker Hub for containers, npmjs.org for Node packages, Maven Central for Java), Artifactory supports over 30 package formats behind a single control plane. It stores, versions, and distributes every artifact your software supply chain produces or consumes.
At its core, Artifactory provides:
- Protocol-native access — Maven clients talk to it as a Maven repo, Docker daemons push/pull using the Docker Registry API v2, Helm CLI treats it as a chart museum, and npm/yarn resolve packages natively.
- Immutable storage — Once an artifact is published at a specific version, it cannot be overwritten (configurable per repository).
- Rich metadata — Build info, properties, checksums (SHA-256, MD5), and custom attributes attached to every artifact.
- Access control — Fine-grained permissions at the repository, path, and artifact level integrated with LDAP, SAML, and OIDC providers.
- High availability — Active/active clustering with shared storage (S3, GCS, Azure Blob, NFS).
Why DevOps Teams Need a Universal Artifact Repository
The fundamental problem Artifactory solves is artifact provenance and trust. In a mature DevOps organization:
Repository Types: Local, Remote, Virtual
Understanding the three repository types is critical to designing your topology.
Local Repositories
Local repositories store artifacts produced by your organization. These are the repositories your CI pipelines push to.
# Example: creating a local Docker repository via REST API
curl -u admin:password -X PUT \
"https://artifactory.company.com/artifactory/api/repositories/docker-local" \
-H "Content-Type: application/json" \
-d '{
"key": "docker-local",
"rclass": "local",
"packageType": "docker",
"dockerApiVersion": "V2",
"description": "Internal Docker images"
}'
Remote Repositories
Remote repositories proxy external registries and cache downloaded artifacts. When a developer requests lodash@4.17.21 from your remote npm repo, Artifactory fetches it from npmjs.org once, caches it, and serves all subsequent requests locally.
# Remote repository proxying Docker Hub
curl -u admin:password -X PUT \
"https://artifactory.company.com/artifactory/api/repositories/docker-hub-remote" \
-H "Content-Type: application/json" \
-d '{
"key": "docker-hub-remote",
"rclass": "remote",
"packageType": "docker",
"url": "https://registry-1.docker.io/",
"externalDependenciesEnabled": true
}'
Virtual Repositories
Virtual repositories aggregate multiple local and remote repositories behind a single URL. Developers configure one registry URL; Artifactory resolves artifacts by searching included repositories in priority order.
# Virtual repo aggregating local + remote Docker repos
curl -u admin:password -X PUT \
"https://artifactory.company.com/artifactory/api/repositories/docker" \
-H "Content-Type: application/json" \
-d '{
"key": "docker",
"rclass": "virtual",
"packageType": "docker",
"repositories": ["docker-local", "docker-hub-remote"],
"defaultDeploymentRepo": "docker-local"
}'
Setting Up Artifactory for Multiple Package Types
Maven/Gradle (Java)
<!-- settings.xml for Maven -->
<settings>
<servers>
<server>
<id>artifactory</id>
<username>${env.ARTIFACTORY_USER}</username>
<password>${env.ARTIFACTORY_TOKEN}</password>
</server>
</servers>
<mirrors>
<mirror>
<id>artifactory</id>
<mirrorOf>*</mirrorOf>
<url>https://artifactory.company.com/artifactory/maven-virtual/</url>
</mirror>
</mirrors>
</settings>
// build.gradle for Gradle
repositories {
maven {
url "https://artifactory.company.com/artifactory/maven-virtual/"
credentials {
username = System.getenv("ARTIFACTORY_USER")
password = System.getenv("ARTIFACTORY_TOKEN")
}
}
}
publishing {
repositories {
maven {
url "https://artifactory.company.com/artifactory/maven-local/"
credentials {
username = System.getenv("ARTIFACTORY_USER")
password = System.getenv("ARTIFACTORY_TOKEN")
}
}
}
}
npm (Node.js)
# .npmrc configuration
registry=https://artifactory.company.com/artifactory/api/npm/npm-virtual/
//artifactory.company.com/artifactory/api/npm/npm-virtual/:_authToken=${ARTIFACTORY_TOKEN}
always-auth=true
PyPI (Python)
# pip.conf
[global]
index-url = https://${ARTIFACTORY_USER}:${ARTIFACTORY_TOKEN}@artifactory.company.com/artifactory/api/pypi/pypi-virtual/simple
trusted-host = artifactory.company.com
# ~/.pypirc for publishing
[distutils]
index-servers = artifactory
[artifactory]
repository = https://artifactory.company.com/artifactory/api/pypi/pypi-local
username = ${ARTIFACTORY_USER}
password = ${ARTIFACTORY_TOKEN}
Docker
# Login to Artifactory Docker registry
docker login artifactory.company.com -u $ARTIFACTORY_USER -p $ARTIFACTORY_TOKEN
# Tag and push
docker tag myapp:latest artifactory.company.com/docker-local/myapp:1.2.3
docker push artifactory.company.com/docker-local/myapp:1.2.3
# Pull through virtual repository
docker pull artifactory.company.com/docker/myapp:1.2.3
Helm Charts
# Add Artifactory as Helm repo
helm repo add company https://artifactory.company.com/artifactory/helm-virtual \
--username $ARTIFACTORY_USER \
--password $ARTIFACTORY_TOKEN
# Push chart using JFrog CLI
jf rt upload "myapp-chart-*.tgz" helm-local/ \
--target-props="chart.name=myapp;chart.version=1.2.3"
CI/CD Integration
GitHub Actions
name: Build and Publish
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup JFrog CLI
uses: jfrog/setup-jfrog-cli@v4
env:
JF_URL: ${{ secrets.JF_URL }}
JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }}
- name: Configure JFrog CLI
run: |
jf rt ping
jf rt bce ${{ github.repository }} ${{ github.run_number }}
- name: Build Docker Image
run: |
jf docker build -t ${{ secrets.JF_URL }}/docker-local/myapp:${{ github.sha }} .
jf docker push ${{ secrets.JF_URL }}/docker-local/myapp:${{ github.sha }}
- name: Publish Build Info
run: |
jf rt build-publish ${{ github.repository }} ${{ github.run_number }}
- name: Scan with Xray
run: |
jf build-scan ${{ github.repository }} ${{ github.run_number }}
Jenkins (Declarative Pipeline)
pipeline {
agent any
environment {
ARTIFACTORY_SERVER = 'artifactory-prod'
IMAGE_NAME = "docker-local/myapp"
IMAGE_TAG = "${env.BUILD_NUMBER}-${env.GIT_COMMIT[0..7]}"
}
stages {
stage('Build') {
steps {
script {
def server = Artifactory.server(ARTIFACTORY_SERVER)
def rtDocker = Artifactory.docker(server: server)
docker.build("${IMAGE_NAME}:${IMAGE_TAG}")
def buildInfo = rtDocker.push(
"${env.ARTIFACTORY_URL}/${IMAGE_NAME}:${IMAGE_TAG}",
'docker-local'
)
server.publishBuildInfo(buildInfo)
}
}
}
stage('Xray Scan') {
steps {
script {
def server = Artifactory.server(ARTIFACTORY_SERVER)
def scanConfig = [
'buildName': env.JOB_NAME,
'buildNumber': env.BUILD_NUMBER,
'failBuild': true
]
def scanResult = server.xrayScan(scanConfig)
echo "Scan result: ${scanResult}"
}
}
}
stage('Promote to Staging') {
when { branch 'main' }
steps {
script {
def server = Artifactory.server(ARTIFACTORY_SERVER)
def promotionConfig = [
'buildName': env.JOB_NAME,
'buildNumber': env.BUILD_NUMBER,
'targetRepo': 'docker-staging',
'status': 'Staged',
'comment': 'Promoted by Jenkins',
'copy': true
]
server.promote(promotionConfig)
}
}
}
}
}
GitLab CI
variables:
ARTIFACTORY_URL: "https://artifactory.company.com/artifactory"
IMAGE_NAME: "docker-local/myapp"
stages:
- build
- scan
- promote
build:
stage: build
image: releases-docker.jfrog.io/jfrog/jfrog-cli-v2-jf
script:
- jf c add --url=$ARTIFACTORY_URL --access-token=$JF_ACCESS_TOKEN
- jf docker build -t $ARTIFACTORY_URL/$IMAGE_NAME:$CI_COMMIT_SHA .
- jf docker push $ARTIFACTORY_URL/$IMAGE_NAME:$CI_COMMIT_SHA
- jf rt bce $CI_PROJECT_NAME $CI_PIPELINE_ID
- jf rt bp $CI_PROJECT_NAME $CI_PIPELINE_ID
xray_scan:
stage: scan
script:
- jf c add --url=$ARTIFACTORY_URL --access-token=$JF_ACCESS_TOKEN
- jf bs $CI_PROJECT_NAME $CI_PIPELINE_ID --fail=true
promote_staging:
stage: promote
when: manual
script:
- jf c add --url=$ARTIFACTORY_URL --access-token=$JF_ACCESS_TOKEN
- jf rt bpr $CI_PROJECT_NAME $CI_PIPELINE_ID docker-staging --copy
only:
- main
JFrog Xray for Security Scanning
JFrog Xray provides deep recursive scanning of all artifacts and their dependencies. Unlike standalone SCA tools, Xray is natively integrated with Artifactory — it scans artifacts as they are published and can block downloads of vulnerable components.
Setting Up Xray Policies and Watches
# Create a security policy via REST API
curl -u admin:password -X POST \
"https://artifactory.company.com/xray/api/v2/policies" \
-H "Content-Type: application/json" \
-d '{
"name": "critical-vuln-block",
"type": "security",
"rules": [{
"name": "block-critical",
"criteria": {
"min_severity": "Critical"
},
"actions": {
"block_download": {
"active": true,
"unscanned": true
},
"fail_build": true,
"notify_deployer": true
}
}]
}'
# Create a watch to apply the policy to repositories
curl -u admin:password -X POST \
"https://artifactory.company.com/xray/api/v2/watches" \
-H "Content-Type: application/json" \
-d '{
"general_data": {
"name": "production-artifacts-watch",
"active": true
},
"project_resources": {
"resources": [{
"type": "repository",
"name": "docker-staging",
"bin_mgr_id": "default",
"filters": [{
"type": "package-type",
"value": "Docker"
}]
}]
},
"assigned_policies": [{
"name": "critical-vuln-block",
"type": "security"
}]
}'
Xray in the Development Workflow
Xray provides three scanning triggers:
License Compliance:
Beyond security vulnerabilities, Xray also enforces license policies. You can create license policies that block artifacts with GPL, AGPL, or other copyleft licenses from entering your commercial codebase:
curl -u admin:password -X POST \
"https://artifactory.company.com/xray/api/v2/policies" \
-H "Content-Type: application/json" \
-d '{
"name": "license-compliance",
"type": "license",
"rules": [{
"name": "block-copyleft",
"criteria": {
"banned_licenses": ["GPL-2.0", "GPL-3.0", "AGPL-3.0"]
},
"actions": {
"block_download": {"active": true},
"fail_build": true
}
}]
}'
JFrog CLI Usage Examples
The JFrog CLI (jf) is the primary interface for automation:
# Configure CLI with access token
jf c add myserver --url=https://artifactory.company.com --access-token=$TOKEN
# Upload with properties
jf rt upload "target/*.jar" maven-local/com/company/myapp/1.0/ \
--build-name=myapp --build-number=42 \
--target-props="release.status=DEV;vcs.revision=${GIT_SHA}"
# Download with pattern matching
jf rt download "docker-staging/myapp/latest/" ./deploy/ \
--props="release.status=STAGED"
# Search artifacts by properties
jf rt search "docker-local/myapp/" --props="release.status=PROD"
# Copy artifacts between repositories (promotion)
jf rt copy "docker-staging/myapp/1.2.3/" "docker-prod/myapp/1.2.3/" \
--flat=false
# Set properties on existing artifacts
jf rt set-props "docker-prod/myapp/1.2.3/" \
"deployed.env=production;deployed.date=$(date -u +%Y%m%dT%H%M%SZ)"
# Collect build environment and publish
jf rt bce myapp 42
jf rt bag myapp 42
jf rt bp myapp 42
# Scan a build
jf bs myapp 42 --fail=true --vuln
Industry Case Studies
Case Study 1: E-Commerce Platform — 50 Microservices Promotion Pipeline
Challenge: A mid-sized e-commerce company with 50 microservices needed consistent artifact promotion across dev, staging, and production environments. Teams were independently pushing Docker images with inconsistent tagging, and there was no way to determine which image version was running in production without SSH-ing into nodes.
Solution Architecture:
Repository Topology:
├── docker-dev (local) — CI pushes here on every commit
├── docker-staging (local) — promoted after integration tests pass
├── docker-prod (local) — promoted after staging validation
├── docker-hub-remote (remote) — cached base images from Docker Hub
├── docker (virtual) — aggregates all above for pulls
├── helm-dev (local) — charts pushed on commit
├── helm-staging (local) — charts promoted with matching images
└── helm-prod (local) — production-ready charts
Promotion Pipeline:
#!/bin/bash
# promote.sh — Promotes a build from dev to staging
BUILD_NAME=$1
BUILD_NUMBER=$2
# Verify Xray scan passed
SCAN_RESULT=$(jf bs $BUILD_NAME $BUILD_NUMBER --fail=true 2>&1)
if [ $? -ne 0 ]; then
echo "Xray scan failed. Cannot promote."
exit 1
fi
# Promote Docker image
jf rt bpr $BUILD_NAME $BUILD_NUMBER docker-staging \
--status="Staged" \
--comment="Promoted after passing Xray scan and integration tests" \
--copy=true \
--props="promoted.by=${USER};promoted.date=$(date -u +%Y%m%dT%H%M%SZ)"
# Promote corresponding Helm chart
jf rt copy "helm-dev/myapp-chart-${BUILD_NUMBER}.tgz" "helm-staging/" \
--flat=true
Results:
- Deployment time reduced from 45 minutes to 8 minutes (no rebuild needed)
- Zero incidents caused by "wrong image in production" — down from 3-4 per quarter
- Full audit trail: can trace any running container back to its source commit in under 30 seconds
Case Study 2: Financial Services — Compliance and Immutable Artifacts
Challenge: A regulated financial institution needed to meet SOX compliance requirements: every artifact deployed to production must be immutable, signed, and traceable to a specific build with full dependency manifests. Auditors needed quarterly reports showing which versions were deployed and when.
Solution Architecture:
# Terraform: Artifactory repository with strict immutability
resource "artifactory_local_docker_v2_repository" "docker_prod" {
key = "docker-prod"
tag_retention = 100
max_unique_tags = 50
block_pushing_schema1 = true
}
Artifact Signing with Cosign:
# Sign artifact after successful build
cosign sign --key cosign.key \
artifactory.company.com/docker-prod/payment-service:${VERSION}
# Verify signature before deployment
cosign verify --key cosign.pub \
artifactory.company.com/docker-prod/payment-service:${VERSION}
Audit Report Generation:
import requests
import os
from datetime import datetime
ARTIFACTORY_URL = "https://artifactory.company.com/artifactory"
TOKEN = os.environ["ARTIFACTORY_TOKEN"]
def get_production_deployments(quarter_start, quarter_end):
"""Query AQL for all artifacts promoted to prod in the quarter."""
aql_query = f'''
items.find({{
"repo": "docker-prod",
"created": {{"$gte": "{quarter_start}"}},
"created": {{"$lte": "{quarter_end}"}}
}}).include("name","created","modified","sha256","property.*")
.sort({{"$asc": ["created"]}})
'''
response = requests.post(
f"{ARTIFACTORY_URL}/api/search/aql",
headers={"Authorization": f"Bearer {TOKEN}",
"Content-Type": "text/plain"},
data=aql_query
)
return response.json()["results"]
def generate_compliance_report(artifacts):
"""Generate SOX-compliant artifact report."""
report = []
for artifact in artifacts:
props = {p["key"]: p["value"] for p in artifact.get("properties", [])}
report.append({
"artifact": artifact["name"],
"sha256": artifact["sha256"],
"promoted_date": artifact["created"],
"promoted_by": props.get("promoted.by", "unknown"),
"build_number": props.get("build.number", "unknown"),
"xray_scan_status": props.get("xray.status", "unknown")
})
return report
Results:
- Passed 4 consecutive SOX audits with zero findings related to artifact management
- Reduced audit preparation time from 2 weeks to 2 hours (automated report generation)
- Complete dependency SBOM for every production artifact via Xray
Case Study 3: Multi-Team Enterprise — Virtual Repository Aggregation
Challenge: A 2000-engineer enterprise with 40 product teams needed isolation between teams while maintaining a unified developer experience. Each team needed their own repositories for access control, but developers should not need to know which team owns a library to use it.
Solution Architecture:
Virtual Repository: npm-company (what developers configure)
├── npm-team-alpha-local (Team Alpha's published packages)
├── npm-team-beta-local (Team Beta's published packages)
├── npm-team-gamma-local (Team Gamma's published packages)
├── npm-platform-local (Shared platform libraries)
├── npm-approved-remote (Curated external dependencies)
└── npm-npmjs-remote (Proxied npmjs.org — read-only fallback)
Access Control with Permission Targets:
# Team Alpha can only deploy to their own repo
curl -u admin:password -X PUT \
"https://artifactory.company.com/artifactory/api/v2/security/permissions/team-alpha-deploy" \
-H "Content-Type: application/json" \
-d '{
"name": "team-alpha-deploy",
"repo": {
"repositories": ["npm-team-alpha-local", "docker-team-alpha-local"],
"actions": {
"groups": {
"team-alpha-developers": ["read", "write", "annotate"],
"team-alpha-leads": ["read", "write", "annotate", "delete", "manage"]
}
},
"include-patterns": ["**"],
"exclude-patterns": [""]
},
"build": {
"actions": {
"groups": {
"team-alpha-developers": ["read"],
"team-alpha-leads": ["read", "write", "delete", "manage"]
}
},
"include-patterns": ["team-alpha/**"]
}
}'
Build Info Aggregation for Release Trains:
# Aggregate multiple team builds into a release build
jf rt bce "release-train" "2024-Q1-R3"
# Add all component builds
jf rt bad "release-train" "2024-Q1-R3" "team-alpha/payment-service" "145"
jf rt bad "release-train" "2024-Q1-R3" "team-beta/user-service" "892"
jf rt bad "release-train" "2024-Q1-R3" "team-gamma/notification-service" "67"
# Publish aggregated build
jf rt bp "release-train" "2024-Q1-R3"
# Scan the entire release as a unit
jf bs "release-train" "2024-Q1-R3" --fail=true
Results:
- Developer onboarding time reduced from 2 days to 2 hours (single registry URL)
- Security team gained visibility across all teams through unified Xray policies
- Inter-team dependency consumption increased 300% (easier discovery through virtual repos)
Best Practices for Retention, Cleanup, and Cost Management
Retention Policies
Storage costs grow linearly with artifacts. Implement retention policies early:
# AQL-based cleanup: delete dev images older than 30 days with no downloads
curl -u admin:password -X POST \
"https://artifactory.company.com/artifactory/api/search/aql" \
-H "Content-Type: text/plain" \
-d 'items.find({
"repo": "docker-dev",
"stat.downloaded": {"$before": "90d"},
"created": {"$before": "30d"}
})' | jq -r '.results[].path' | while read path; do
jf rt delete "docker-dev/${path}" --quiet
done
Cost Management Strategy
| Strategy | Savings | Effort |
|---|---|---|
| Delete undownloaded dev artifacts (>30 days) | 40-60% storage | Low |
| Limit snapshot retention (keep last 5) | 20-30% storage | Low |
| Move cold artifacts to cheaper storage tier | 30-50% cost | Medium |
| Deduplicate with checksum-based storage | 15-25% storage | Built-in |
| Set max unique Docker tags per image | 10-20% storage | Low |
Monitoring Storage Growth
# Get storage summary
curl -u admin:password \
"https://artifactory.company.com/artifactory/api/storageinfo" | \
jq '{
total: .fileStoreSummary.totalSpace,
used: .fileStoreSummary.usedSpace,
free: .fileStoreSummary.freeSpace,
top_repos: [.repositoriesSummaryList[] |
{name: .repoKey, used: .usedSpace, files: .filesCount}
] | sort_by(.used) | reverse | .[0:10]
}'
Quick Reference Table
| Task | Command / Configuration |
|---|---|
| Login to Docker registry | <code class="inline-code">docker login artifactory.company.com</code> |
| Push Docker image | <code class="inline-code">jf docker push artifactory.company.com/docker-local/app:tag</code> |
| Upload generic artifact | <code class="inline-code">jf rt upload "file.zip" generic-local/path/</code> |
| Download artifact | <code class="inline-code">jf rt download "repo/path/file" ./local/</code> |
| Search by properties | <code class="inline-code">jf rt search "repo/" --props="key=value"</code> |
| Promote build | <code class="inline-code">jf rt bpr BUILD_NAME BUILD_NUM target-repo</code> |
| Scan build with Xray | <code class="inline-code">jf bs BUILD_NAME BUILD_NUM --fail=true</code> |
| Publish build info | <code class="inline-code">jf rt bp BUILD_NAME BUILD_NUM</code> |
| Set artifact properties | <code class="inline-code">jf rt set-props "repo/path/" "key=value"</code> |
| Delete old artifacts | <code class="inline-code">jf rt delete "repo/" --props="created<timestamp"</code> |
| Get storage info | <code class="inline-code">curl $URL/api/storageinfo</code> |
| Configure npm | Set <code class="inline-code">registry</code> in <code class="inline-code">.npmrc</code> to virtual repo URL |
| Configure Maven | Set <code class="inline-code"><mirror></code> in <code class="inline-code">settings.xml</code> to virtual repo URL |
| Configure pip | Set <code class="inline-code">index-url</code> in <code class="inline-code">pip.conf</code> to PyPI virtual repo URL |
Summary
JFrog Artifactory is not just a registry — it is the artifact backbone of a mature DevOps organization. The key principles for a successful implementation:
The investment pays off within the first quarter. When an incident occurs and someone asks "what exactly is running in production and where did it come from," you will have the answer in seconds, not hours.
---
Frequently Asked Questions
What is JFrog Artifactory and why do teams use it?
JFrog Artifactory is a universal artifact repository manager that stores build artifacts, Docker images, packages (npm, Maven, PyPI), and Helm charts in one place. It provides a single source of truth for all binary artifacts, enforces security scanning, and speeds up builds with caching proxies for remote repositories. Teams use it to control what goes into production.
What is the difference between Artifactory and a container registry?
Artifactory is a universal repository supporting 30+ package formats including Docker, Maven, npm, PyPI, and Helm. A dedicated container registry (Docker Hub, ECR, GCR) only handles Docker/OCI images. Artifactory consolidates all artifact types with consistent access control, while dedicated registries offer simpler setup for container-only workflows.
How do I configure Artifactory as a Docker registry?
Create a Docker repository in Artifactory (local for pushing, remote for proxying Docker Hub, virtual to combine both). Configure Docker to trust Artifactory's SSL certificate, then login with docker login <artifactory-url>. Tag images with the Artifactory path: docker tag myapp artifactory.example.com/docker-local/myapp:v1 and push.
How do I clean up old artifacts in Artifactory?
Set up retention policies using Artifactory's built-in cleanup features or JFrog CLI. Create a scheduled cleanup job that deletes artifacts older than X days, not downloaded in Y days, or matching specific patterns. Protect release artifacts from cleanup and always run cleanup in dry-run mode first to verify what will be removed.