Configuration

ocync uses a YAML config file to define registries, target groups, defaults, and image mappings. A JSON schema is available for editor autocompletion and validation.

Minimal example

registries:
  source:
    url: cgr.dev
  target:
    url: 123456789012.dkr.ecr.us-east-1.amazonaws.com

defaults:
  source: source
  targets: target
  tags:
    glob: "*"

mappings:
  - from: chainguard/nginx
    to: nginx

Full example

global:
  max_concurrent_transfers: 50
  cache_ttl: "12h"
  staging_size_limit: "2GB"

registries:
  chainguard:
    url: cgr.dev
  ecr-east:
    url: 123456789012.dkr.ecr.us-east-1.amazonaws.com
  ecr-west:
    url: 123456789012.dkr.ecr.us-west-2.amazonaws.com

target_groups:
  prod:
    - ecr-east
    - ecr-west

defaults:
  source: chainguard
  targets: prod
  platforms:
    - linux/amd64
    - linux/arm64
  tags:
    semver: ">=1.0"
    semver_prerelease: include   # cgr.dev tags carry -rN build suffixes
    sort: semver
    latest: 10

mappings:
  - from: chainguard/nginx
    to: nginx
  - from: chainguard/python
    to: python
    tags:
      glob: "*-slim"

Global settings

Top-level global section controls engine-wide behavior:

global:
  max_concurrent_transfers: 50    # Maximum concurrent image syncs (default: 50)
  cache_dir: /var/cache/ocync     # Cache directory (default: next to config file)
  cache_ttl: "12h"                # Warm cache TTL (default: "12h", "0" disables)
  staging_size_limit: "2GB"       # Disk staging limit (SI prefixes, "0" disables)
FieldDefaultDescription
max_concurrent_transfers50Maximum number of images synced in parallel. Must be >= 1
cache_dirAdjacent to config fileDirectory for persistent cache and blob staging
cache_ttl"12h"How long cached blob existence checks are valid. Accepts bare integers (seconds) or integers with a suffix: s, m, h, d. "0" disables TTL expiry (lazy invalidation only)
staging_size_limitUnlimitedMaximum disk space for blob staging. Accepts "0" (disabled) or an integer with a suffix: B, KB, MB, GB, TB. Uses SI decimal prefixes (1 GB = 1,000,000,000 bytes)

Registries

Named registry definitions. Auth type is auto-detected from the hostname.

registries:
  my-registry:
    url: registry.example.com         # Required
    auth_type: ecr                    # Optional (see below)
    max_concurrent: 50                # Optional per-registry concurrency cap (>= 1)
    head_first: true                  # Optional: HEAD-check targets before source GET
    credentials:                      # For auth_type: basic
      username: myuser
      password: ${REGISTRY_PASSWORD}
    token: ${GITHUB_TOKEN}            # For auth_type: static_token
FieldRequiredDescription
urlYesRegistry hostname (e.g., cgr.dev, 123456789012.dkr.ecr.us-east-1.amazonaws.com)
auth_typeNoAuthentication method (see below). Auto-detected from hostname when omitted
max_concurrentNoPer-registry aggregate concurrency cap. Limits total simultaneous HTTP requests to this registry across all mappings. Must be >= 1
head_firstNoHEAD-check all targets against the source HEAD digest before pulling full source manifests on cache miss. Skips the expensive source GET when all targets already match. Useful for rate-limited sources (e.g., Docker Hub). Bypassed when platform filtering is active. Default: false
credentialsWhen auth_type: basicObject with username and password fields. Both required
tokenWhen auth_type: static_tokenPre-obtained bearer token string

Auth types

ValueDescription
ecrAWS ECR private. HTTP Basic using an SDK-issued token; ambient AWS credentials
garGoogle Artifact Registry (*-docker.pkg.dev); Application Default Credentials
gcrLegacy Google Container Registry (*.gcr.io); same provider as gar
acrAzure Container Registry; AAD credential chain via proprietary OAuth2 exchange
ghcrAlias for docker_config. Prefer static_token (e.g., GITHUB_TOKEN) or docker_config directly
basicHTTP Basic. Requires credentials with username and password
static_token or tokenPre-obtained bearer token. Requires token. Does not refresh
docker_configReads ~/.docker/config.json (or $DOCKER_CONFIG/config.json)
anonymousNo credentials; still performs the /v2/token Bearer exchange when challenged

Auto-detected from hostname. ECR (private and Public), GAR, GCR, and ACR each route to a dedicated native-auth provider. GHCR, Docker Hub, Chainguard, and any unrecognized hostname share the same fallback path: docker_config first (using ~/.docker/config.json or $DOCKER_CONFIG/config.json), then anonymous if no entry is found. Set auth_type explicitly to override detection.

See the registry guides for provider-specific auth details.

Target groups

Logical groupings for fan-out to multiple registries:

target_groups:
  prod:
    - ecr-east
    - ecr-west
  staging:
    - ecr-staging

When syncing to multiple targets, ocync pulls from source once and pushes to all targets in parallel.

Defaults

Applied to all mappings unless overridden at the mapping level:

defaults:
  source: chainguard           # Default source registry
  targets: prod                # Default target group
  artifacts:                   # OCI 1.1 referrer handling (default: enabled)
    enabled: true
  platforms:                   # Platform filter (omit to preserve multi-arch)
    - linux/amd64
    - linux/arm64
  tags:
    glob: "*"                  # Glob pattern
    semver: ">=1.0"            # Semver range
    semver_prerelease: exclude # Pre-release handling
    exclude:                   # Exclude patterns
      - "*-debug"
    sort: semver               # Sort order: semver, alpha
    latest: 10                 # Keep only N most recent after sort
    min_tags: 1                # Minimum tags required
FieldDefaultDescription
sourceNoneDefault source registry name for all mappings
targetsNoneDefault target group name or inline list
artifactsenabled: trueOCI 1.1 referrer handling (signatures, SBOMs, attestations). See Artifacts
platformsAll platformsPlatform filter. Each entry must be os/arch or os/arch/variant (e.g., linux/amd64, linux/arm/v7). When set, multi-arch indexes are rewritten at the target with a different digest than the source - bit-for-bit divergence. Omit to preserve multi-arch verbatim
tagsNoneTag filtering pipeline (see Tag filtering)

Mappings

Source-to-target image relationships:

mappings:
  - from: chainguard/nginx     # Source repository (required)
    to: nginx                  # Target repository name
    source: chainguard         # Override source registry
    targets: staging           # Override target group
    tags:                      # Override tag filters
      glob: "1.*"
      sort: semver             # Sort order: semver, alpha
      latest: 5
    platforms:                 # Override platform filter
      - linux/amd64
FieldRequiredDescription
fromYesSource repository path (e.g., chainguard/nginx)
toNoTarget repository name. When omitted, uses the full from path
sourceNoOverride source registry for this mapping
targetsNoOverride target group or inline list for this mapping
tagsNoOverride tag filtering for this mapping
platformsNoOverride platform filter for this mapping. List must not be empty if provided
artifactsNoOverride artifact handling for this mapping. See Artifacts

Artifacts

Controls whether OCI 1.1 referrers (cosign signatures, SBOMs, attestations) are discovered and transferred alongside their parent image manifests. Default: enabled.

defaults:
  artifacts:
    enabled: true                  # Default; set to false to skip referrers
    include:                       # When non-empty, only artifacts whose
      - application/vnd.dev.cosign.simplesigning.v1+json   # artifact_type matches one of these
      - application/vnd.cyclonedx+json
    exclude:                       # Always-skip artifacts whose type matches
      - application/vnd.in-toto+json
    require_artifacts: false       # When true, fail the sync if a parent has zero referrers
FieldDefaultDescription
enabledtrueWhen true, after each image manifest is synced, ocync queries the source’s /v2/<repo>/referrers/<digest> endpoint and transfers each referrer manifest plus its blobs to the target. Set to false to skip referrer discovery entirely
includeAll typesIf non-empty, only referrers whose artifact_type matches one of these MIME types are transferred. Empty means all types pass
excludeNoneReferrers whose artifact_type matches one of these are skipped, even if include matches
require_artifactsfalseWhen true, an image with zero discovered referrers fails the sync with an error. Use to enforce that every mirrored image carries provenance

Why default-on

Defaulting enabled: true is the only setting consistent with ocync’s bit-for-bit promise. If enabled: false were the default, the mirror would look correct (every image present, every digest valid) but cosign verify against the source’s signature would fail at deployment time, because the signature lives on a referrer that was never copied. The opt-out is there for users who deliberately want unsigned mirrors: testing setups, isolated networks, or hard byte budgets.

Opt-out

defaults:
  artifacts:
    enabled: false              # Skip all referrer discovery and transfer

Type filtering

Mirror only SBOMs:

defaults:
  artifacts:
    enabled: true
    include:
      - application/vnd.cyclonedx+json
      - application/spdx+json

Hard-fail on missing signatures

defaults:
  artifacts:
    enabled: true
    require_artifacts: true     # Fail sync if any image lacks referrers

Tag filtering

Tags are filtered through a pipeline in order:

  1. glob: include tags matching the glob pattern (string or list)
  2. semver: include tags satisfying the semver range
  3. semver_prerelease: control pre-release tag handling when semver is set
  4. exclude: remove tags matching any exclude pattern (string or list)
  5. sort: order remaining tags (semver, alpha)
  6. latest: keep only the N most recent after sorting
  7. min_tags: validate at least N tags survived the pipeline (error if fewer)

All filters are optional. Without any filters, all tags are synced.

FieldTypeDescription
globstring or listInclude tags matching glob pattern(s). A single string or a list of patterns
semverstringInclude tags satisfying a semver range (e.g., ">=1.0", "^2", "1.x")
semver_prereleasestringHow to handle pre-release tags when semver is set. Values: include, exclude, only. Default: exclude. Requires semver to be set
excludestring or listRemove tags matching these glob pattern(s)
sortstringSort order for remaining tags: semver or alpha
latestintegerKeep only the N most recent tags after sorting
min_tagsintegerMinimum number of tags that must survive the filtering pipeline. If fewer tags remain, the sync for this mapping fails with an error
immutable_tagsstringGlob pattern marking tags that never change content (e.g. "v?[0-9]*.[0-9]*.[0-9]*"). When a tag matches AND already exists in every target’s tag list, the sync skips it with zero source and target requests. Useful for long-running mirrors of registries that publish many semver-pinned tags

Validation constraints:

  • semver_prerelease requires semver to be set
  • latest requires sort to be set

Override semantics: when a mapping defines tags:, the entire block replaces defaults.tags - fields are not merged. If you want a mapping to inherit some default fields and override others, repeat the inherited fields in the mapping’s tags: block.

Environment variables

Config values support environment variable substitution:

SyntaxBehavior
${VAR}Substitute value, empty string if unset
${VAR:-default}Substitute value, use default if unset
${VAR:?error message}Substitute value, error with message if unset
registries:
  ecr:
    url: ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com

The DOCKER_CONFIG environment variable controls the Docker config file location. If set, ocync reads $DOCKER_CONFIG/config.json. Otherwise, it defaults to ~/.docker/config.json.

Example configurations

Chainguard to ECR

Single region. Sync a curated set of Chainguard base images to ECR, keeping the latest 5 semver-pinned tags for linux/amd64 and linux/arm64. Chainguard tags carry a -rN build-revision suffix (1.25.5-r0, 1.25.5-r1); the SemVer spec treats anything after - as a prerelease, so semver_prerelease: include is required for any cgr.dev tag to survive a semver: range:

registries:
  chainguard:
    url: cgr.dev
  ecr:
    url: ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com

defaults:
  source: chainguard
  targets: ecr
  platforms:
    - linux/amd64
    - linux/arm64
  tags:
    semver: ">=1.0"
    semver_prerelease: include
    sort: semver
    latest: 5

mappings:
  - from: chainguard/nginx
    to: nginx
  - from: chainguard/python
    to: python
  - from: chainguard/node
    to: node

Docker Hub fan-out

Mirror Docker Hub images to multiple ECR regions with authenticated pulls and debug tag exclusion:

registries:
  dockerhub:
    url: docker.io
    auth_type: basic
    credentials:
      username: ${DOCKERHUB_USERNAME}
      password: ${DOCKERHUB_ACCESS_TOKEN}
  ecr-east:
    url: ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com
  ecr-west:
    url: ${AWS_ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com

target_groups:
  prod:
    - ecr-east
    - ecr-west

defaults:
  source: dockerhub
  targets: prod
  platforms:
    - linux/amd64
    - linux/arm64
  tags:
    exclude:
      - "*-debug"
      - "*-rc*"
    sort: semver
    latest: 10
    min_tags: 1

mappings:
  - from: library/nginx
    to: nginx
  - from: library/redis
    to: redis
  - from: library/postgres
    to: postgres
    tags:
      # Mapping tags: replaces defaults.tags entirely. Repeat the
      # inherited fields explicitly when a mapping needs both.
      semver: ">=15"
      exclude:
        - "*-debug"
        - "*-rc*"
      sort: semver
      latest: 3
      min_tags: 1

GHCR to ECR with glob filtering

Sync specific tagged releases from GitHub Container Registry:

registries:
  ghcr:
    url: ghcr.io
    auth_type: static_token
    token: ${GITHUB_TOKEN}
  ecr:
    url: ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com

mappings:
  - from: my-org/my-app
    to: my-app
    source: ghcr
    targets: ecr
    tags:
      glob: "v*"
      exclude: "*-alpha"
      sort: semver
      latest: 5
    platforms:
      - linux/amd64

JSON schema

A JSON schema is available for editor autocompletion and validation. Add the schema comment to the top of your config file:

# yaml-language-server: $schema=https://clowdhaus.github.io/ocync/config.schema.json

registries:
  source:
    url: cgr.dev
  target:
    url: 123456789012.dkr.ecr.us-east-1.amazonaws.com

defaults:
  source: source
  targets: target
  tags:
    glob: "*"

mappings:
  - from: chainguard/nginx
    to: nginx

This works with any editor that supports the YAML Language Server (VS Code with the YAML extension, Neovim with yaml-language-server, JetBrains IDEs).

The schema is generated from the Rust config types and verified in CI to stay in sync. View the full schema at /ocync/config.schema.json.

Validation

Validate config syntax without connecting to registries:

ocync validate config.yaml

Show config with all environment variables resolved:

ocync expand config.yaml

Use --show-secrets to display credentials instead of redacting them (do not pipe to logs):

ocync expand config.yaml --show-secrets