JFrog Artifactory as Your Docker Registry: Setup and Security
JFrog Artifactory as Your Docker Registry: Setup and Security
Docker Hub rate limits, ECR's per-account complexity, and Harbor's operational overhead have pushed many teams toward JFrog Artifactory. It's more than a Docker registry — it's a universal artifact manager. But that breadth can make it confusing to set up correctly. This guide focuses specifically on Docker: how to create repositories, control access, promote images, and keep your registry clean.
Repository Types: The Mental Model
Artifactory has three repository types. Understanding them upfront saves a lot of confusion:
| Type | Description | Use Case |
|---|---|---|
| Local | Stores artifacts you push | Your built images |
| Remote | Proxy to external registry | Cache Docker Hub, mirror GCR |
| Virtual | Aggregates local + remote repos | Single endpoint for your team |
The recommended setup for Docker: two local repos (dev and prod), one remote (Docker Hub cache), and one virtual repo that combines all three. Your team pulls and pushes only to the virtual repo.
Create Repositories via API
ARTIFACTORY_URL="https://yourcompany.jfrog.io/artifactory"
ADMIN_TOKEN="your-admin-token"
# Create local repo for dev images
curl -u "admin:${ADMIN_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/repositories/docker-dev-local" \
-d '{
"rclass": "local",
"packageType": "docker",
"description": "Docker images for development and testing",
"repoLayoutRef": "simple-default",
"xrayIndex": true,
"dockerApiVersion": "V2",
"enableDockerSupport": true,
"maxUniqueTags": 30
}'
# Create local repo for prod images
curl -u "admin:${ADMIN_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/repositories/docker-prod-local" \
-d '{
"rclass": "local",
"packageType": "docker",
"description": "Promoted, production-ready Docker images",
"repoLayoutRef": "simple-default",
"xrayIndex": true,
"dockerApiVersion": "V2",
"enableDockerSupport": true,
"maxUniqueTags": 10
}'
# Create remote repo caching Docker Hub
curl -u "admin:${ADMIN_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/repositories/docker-hub-remote" \
-d '{
"rclass": "remote",
"packageType": "docker",
"url": "https://registry-1.docker.io/",
"description": "Proxy for Docker Hub",
"externalDependenciesEnabled": true,
"enableTokenAuthentication": true,
"blockMismatchingMimeTypes": false
}'
# Create virtual repo that wraps all three
curl -u "admin:${ADMIN_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/repositories/docker" \
-d '{
"rclass": "virtual",
"packageType": "docker",
"description": "Virtual Docker registry — use this endpoint",
"repositories": ["docker-dev-local", "docker-prod-local", "docker-hub-remote"],
"defaultDeploymentRepo": "docker-dev-local",
"resolveDockerTagsByTimestamp": true
}'
Configure Docker Client
# Log in to your virtual repo
docker login yourcompany.jfrog.io \
--username your-user \
--password-stdin <<< "your-api-token"
# Pull from Docker Hub through your Artifactory cache
# This avoids Docker Hub rate limits and speeds up pulls
docker pull yourcompany.jfrog.io/docker-hub-remote/library/nginx:1.25
# Build and push your app image to the dev local repo
docker build -t yourcompany.jfrog.io/docker/myapp:1.0.0 .
docker push yourcompany.jfrog.io/docker/myapp:1.0.0
Access Control with Groups and Permission Targets
# Create a group for developers
curl -u "admin:${ADMIN_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/security/groups/dev-team" \
-d '{
"name": "dev-team",
"description": "Development team",
"autoJoin": false,
"adminPrivileges": false
}'
# Create permission target: dev-team can push to docker-dev-local
curl -u "admin:${ADMIN_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/v2/security/permissions/dev-docker-push" \
-d '{
"name": "dev-docker-push",
"repo": {
"include-patterns": ["**"],
"exclude-patterns": [],
"repositories": ["docker-dev-local"],
"actions": {
"groups": {
"dev-team": ["read", "write", "annotate"]
}
}
}
}'
# Create permission target: only CI/CD service account can push to prod
curl -u "admin:${ADMIN_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/v2/security/permissions/prod-docker-push" \
-d '{
"name": "prod-docker-push",
"repo": {
"include-patterns": ["**"],
"repositories": ["docker-prod-local"],
"actions": {
"users": {
"ci-service-account": ["read", "write", "annotate", "delete"]
},
"groups": {
"dev-team": ["read"]
}
}
}
}'
Image Promotion: Dev to Prod
Promotion copies a tested, approved image from docker-dev-local to docker-prod-local without rebuilding it. The SHA256 digest is preserved, guaranteeing what was tested is what gets deployed.
# Promote image from dev to prod via API
curl -u "admin:${ADMIN_TOKEN}" \
-X POST \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/docker/docker-dev-local/v2/promote" \
-d '{
"targetRepo": "docker-prod-local",
"dockerRepository": "myapp",
"tag": "1.0.0",
"targetTag": "1.0.0",
"copy": true,
"failFast": true
}'
Wire this into your CI pipeline (example for GitHub Actions):
# .github/workflows/promote.yml
name: Promote to Production
on:
workflow_dispatch:
inputs:
image_tag:
description: 'Image tag to promote'
required: true
jobs:
promote:
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- name: Promote image to prod registry
run: |
curl -u "${{ secrets.ARTIFACTORY_USER }}:${{ secrets.ARTIFACTORY_TOKEN }}" \
-X POST \
-H "Content-Type: application/json" \
"${{ secrets.ARTIFACTORY_URL }}/api/docker/docker-dev-local/v2/promote" \
-d '{
"targetRepo": "docker-prod-local",
"dockerRepository": "myapp",
"tag": "${{ github.event.inputs.image_tag }}",
"targetTag": "${{ github.event.inputs.image_tag }}",
"copy": true
}'
- name: Verify promoted image
run: |
docker pull \
${{ secrets.ARTIFACTORY_URL }}/docker-prod-local/myapp:${{ github.event.inputs.image_tag }}
docker inspect \
${{ secrets.ARTIFACTORY_URL }}/docker-prod-local/myapp:${{ github.event.inputs.image_tag }} \
| jq '.[0].RepoDigests'
Cleanup Policies (Artifact Eviction)
Without cleanup policies, your registry grows forever. Set retention rules via the UI or API:
# Create an artifact cleanup policy
curl -u "admin:${ADMIN_TOKEN}" \
-X POST \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/ui/api/v1/ui/artifactcleanup" \
-d '{
"name": "dev-cleanup",
"description": "Keep only last 30 tags in dev repo, delete anything older than 14 days",
"enabled": true,
"cronExp": "0 0 2 * * ?",
"repos": ["docker-dev-local"],
"keepLastDeployedArtifacts": false,
"keepLastXDeployedArtifacts": 30,
"deleteLastDownloadedArtifacts": false,
"retentionPeriodInDays": 14,
"skipTrashcan": true
}'
For production images, use a more conservative policy — keep at least 3 versions, never delete anything less than 30 days old.
Service Account Tokens for CI/CD
Create scoped access tokens for your CI/CD pipeline instead of using admin credentials:
# Create a service account
curl -u "admin:${ADMIN_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"${ARTIFACTORY_URL}/api/security/users/ci-service-account" \
-d '{
"name": "ci-service-account",
"email": "[email protected]",
"password": "initial-placeholder",
"admin": false,
"groups": []
}'
# Generate a scoped token with 90-day expiry
curl -u "admin:${ADMIN_TOKEN}" \
-X POST \
"${ARTIFACTORY_URL}/api/security/token" \
-d "username=ci-service-account&scope=member-of-groups:readers%20api:docker&expires_in=7776000"
Store the token in your CI secrets and rotate it every 90 days. Never use your personal API token in automation.
Quick Reference
| Task | Command |
|---|---|
| Login | docker login yourcompany.jfrog.io |
| Push image | docker push yourcompany.jfrog.io/docker/app:tag |
| Pull through cache | docker pull yourcompany.jfrog.io/docker-hub-remote/library/nginx:1.25 |
| List repos | curl -u user:token ARTIFACTORY_URL/api/repositories?type=local |
| Promote image | POST /api/docker/{repo}/v2/promote |
| Delete image tag | DELETE /api/docker/{repo}/v2/{image}/manifests/{tag} |
| Search images | POST /api/search/aql with AQL query |
Artifactory's Docker support goes well beyond basic push/pull. The combination of virtual repos, granular permission targets, and promotion workflows gives you the control a production environment needs.
Was this article helpful?
CI/CD Engineering Lead
Automation evangelist who believes no deployment should require a human. I write pipelines, break pipelines, and write about both. Code-first, always.
Related Articles
JFrog Artifactory: Repository Setup and Access Control
Install JFrog Artifactory, configure local, remote, and virtual repositories, set up Docker registries, and manage permissions with access control.
Configuring JFrog Artifactory Virtual Repositories For Multi-Team Dependency Resolution
Virtual repositories are the glue that holds multi-team artifact management together. Instead of pointing every team at a dozen different repos, you expose...
Automating Artifactory Cleanup Policies With AQL To Reduce Storage Costs
If you've been running JFrog Artifactory for more than a few months, you've almost certainly had the conversation: *"Why is our storage bill so high?"* Art...
JFrog Artifactory: Setup, Repository Configuration, and Pipeline Integration
How to set up JFrog Artifactory, configure local and remote repositories, integrate with Docker and build tools, use Artifactory query language for artifact searches, and set up release bundles.
JFrog Xray: Vulnerability Scanning for Your Artifact Pipeline
Integrate JFrog Xray into your artifact pipeline to detect CVEs, license violations, and block insecure images from reaching production.
Nexus Repository Docker Registry Proxy Configuration For Air-Gapped Environments
Air-gapped environments are a reality for many organizations dealing with sensitive data, regulatory compliance, or high-security requirements. When you ca...