DevOpsil
JFrog Artifactory
92%
Needs Review

JFrog Artifactory as Your Docker Registry: Setup and Security

Sarah ChenSarah Chen6 min read

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:

TypeDescriptionUse Case
LocalStores artifacts you pushYour built images
RemoteProxy to external registryCache Docker Hub, mirror GCR
VirtualAggregates local + remote reposSingle 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

TaskCommand
Logindocker login yourcompany.jfrog.io
Push imagedocker push yourcompany.jfrog.io/docker/app:tag
Pull through cachedocker pull yourcompany.jfrog.io/docker-hub-remote/library/nginx:1.25
List reposcurl -u user:token ARTIFACTORY_URL/api/repositories?type=local
Promote imagePOST /api/docker/{repo}/v2/promote
Delete image tagDELETE /api/docker/{repo}/v2/{image}/manifests/{tag}
Search imagesPOST /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.

Share:

Was this article helpful?

Sarah Chen
Sarah Chen

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

Discussion