Skip to main content

Configuration Reference

Angos is configured via a TOML file (default: config.toml). The configuration is automatically reloaded when the file changes.

Multiple Configuration Files​

-c is repeatable. Files are merged in the order given, and a later file wins:

angos -c /etc/angos/config.toml -c /etc/angos/secrets.toml server

This keeps credentials out of the file your deployment tooling renders. The base file carries everything else, and a second file, sourced from a Kubernetes Secret written by External Secrets Operator, Vault or sealed-secrets, supplies only the sensitive values:

# config.toml
[blob_store.s3]
endpoint = "https://s3.example.com"
bucket = "my-registry"
region = "us-east-1"
# secrets.toml
[blob_store.s3]
access_key_id = "AKIA..."
secret_key = "..."

Merge rules:

ValueBehaviour
TableMerged recursively, so a later file can add keys to a table an earlier file opened
ScalarReplaced by the last file that sets it
ArrayReplaced as a whole, never appended to

Because arrays are replaced, an array of tables such as [[repository."hub".upstream]] must be declared entirely in one file. To give an upstream a password from a separate file, repeat the whole entry there.

Only the merged result has to be a valid configuration, so an overriding file holds just the keys it changes. Every file is watched, so rotating any of them reloads the merged configuration without a restart.

A syntax error names the file it came from. An error detected after merging names the offending key path and the files that were merged, because a merged document has no single source line to quote.

Hot Reloading​

Most configuration changes take effect immediately without restart. The following options require a restart:

  • server.bind_address
  • server.port
  • observability.tracing.sampling_rate
  • Enabling or disabling TLS
  • Changing storage backend type (filesystem ↔ S3)
  • Adding or removing [global.job_queue]
  • max_concurrent_cache_jobs / max_concurrent_replication_jobs on a standalone angos worker: the worker's pool size is fixed at startup, so a running worker must be restarted to change it (the server's in-process drain applies the change on reload).

TLS certificate files are also automatically reloaded when they change.


Server (server)​

OptionTypeDefaultDescription
bind_addressstringrequiredAddress to bind (e.g., "0.0.0.0", "127.0.0.1")
portu168000Port number
query_timeoutnon-zero u643600Query timeout in seconds
query_timeout_grace_periodnon-zero u6460Grace period for queries in seconds
handshake_timeoutnon-zero u6410Seconds a client may take to finish its handshake

Timeout values must be greater than zero.

TLS (server.tls)​

When omitted, the server runs without TLS (insecure). A section that is present but incomplete or invalid fails startup rather than falling back to a plaintext listener.

OptionTypeDefaultDescription
server_certificate_bundlestringrequiredPath to server certificate (PEM)
server_private_keystringrequiredPath to server private key (PEM)
client_ca_bundlestring-Path to client CA bundle for mTLS
client_authstring"optional""optional" accepts both anonymous clients and clients whose certificate validates against client_ca_bundle; "required" rejects the TLS handshake without a valid client certificate and needs client_ca_bundle set. Ignored when client_ca_bundle is unset

Global Options (global)​

OptionTypeDefaultDescription
max_concurrent_requestsnon-zero usize64Tokio worker threads (minimum 1, see Performance Tuning)
max_concurrent_cache_jobsusize4Maximum concurrent cache jobs (minimum 1). With [global.job_queue] enabled, also bounds the number of jobs each angos worker processes in parallel.
max_concurrent_replication_jobsnon-zero usize4Concurrency for replication jobs (minimum 1). Bounds how many replication pushes are handled in parallel by each angos worker, the server's in-process drain, and the angos reconcile replication end-of-run drain.
max_concurrent_scan_jobsusize2Worker concurrency for the scan queue
max_concurrent_index_jobsusize4Worker concurrency for the layer index queue; a job inflates one layer on one core
max_manifest_sizestring"5MiB"Maximum manifest body size accepted from clients or upstream registries
max_blob_sizestring"100GiB"Maximum total size of a single blob upload; a larger upload is rejected with BLOB_UPLOAD_INVALID (HTTP 413)
blob_stream_frame_sizestring"128KiB"Read buffer each frame of a streamed blob response is filled from; larger frames cost fewer allocations and body writes per blob served, at one buffer per in-flight response
update_pull_timeboolfalseTrack pull times for retention policies
enable_blob_redirectbooltrueAllow HTTP 307 redirects for blob downloads.
enable_manifest_redirectbooltrueAllow HTTP 307 redirects for manifest downloads. Manifest bodies served via response-content-type to preserve the media type across redirects.
immutable_tagsboolfalseGlobal immutable tags default
immutable_tags_exclusions[string][]Regex patterns for mutable tags
allow_missing_manifest_referencesbooltrueWhen true (default), accept a manifest push whose referenced blobs or child manifests are not yet present/owned in the namespace; the missing references stay unreadable until their content is pushed. Set to false to reject such pushes with MANIFEST_BLOB_UNKNOWN. See note below.
authorization_webhookstring-Name of webhook for authorization
event_webhooks[string][]Event webhook names for all repositories
shutdown_drain_secsu6430Seconds to keep draining in-flight work on shutdown before forcing exit: the server stops accepting, lets the requests in flight finish, then drains the queued webhook deliveries.
namespace_walk_concurrencyusize > 0128Concurrent directory scans a catalog / upload-namespace walk keeps in flight, hiding per-request backend latency on S3. Zero is refused.
listing_read_concurrencyusize > 016Concurrent reads an admin listing behind the web UI keeps in flight per request: revision records, referrer descriptors, job records. Raise it on a high-latency store to shorten the manifest view's load; zero is refused.
gc_grace_secsu64300Reclamation grace period, used by the serving process and scrub alike: young keys read as live, the push path re-checks the collector after this long, and gc run markers derive their TTL from it. Lower it only in a maintenance config for offline runs against a store with no live traffic; the serving processes must keep a value that exceeds clock skew plus the longest write stall.
trusted_proxies[string][]Proxy IPs or CIDR networks (e.g. "10.0.0.1", "10.0.0.0/8") whose X-Forwarded-For/X-Real-IP headers are honored as the client IP. From any other peer those headers are ignored and the socket address is used. The set must name proxies only: a range that also covers clients lets them spoof the forwarded header.

max_manifest_size, max_blob_size and blob_stream_frame_size must be greater than zero.

allow_missing_manifest_references​

This controls whether the live manifest-push path enforces the OCI distribution-spec option of rejecting a manifest whose descriptors reference content the registry does not have.

  • true (default). A push is accepted even if a referenced config, layer, or child manifest is absent from or not owned by the target namespace. The unowned references are not granted to the namespace: they resolve as unknown on a later pull (BLOB_UNKNOWN for a blob, MANIFEST_UNKNOWN for a child manifest) until their content is pushed. This maximizes compatibility with clients such as docker buildx/bake, which push multi-manifest image indexes and provenance/SBOM attestations whose children are not always namespace-local at validation time.
  • false. A push whose references are missing is rejected outright with MANIFEST_BLOB_UNKNOWN (HTTP 404). This is stricter and conformance-oriented.

Either setting preserves namespace isolation: a caller never gains read access to a blob digest it never uploaded. Inbound replicated manifest pushes follow the same rule; angos-to-angos replication pushes a manifest's children and blobs before the manifest itself, so its references are always owned. subject references (referrers) are always accepted regardless of this setting, per the spec. Pull-through cache-fill writes are trusted, independent of this flag.

Durable Job Queue (global.job_queue)​

Optional. Controls where the job queue is drained. When absent (default), angos server drains the queue itself in-process. When present, angos server enqueues jobs and publishes the queue-depth gauge on /metrics, and one or more angos worker processes drain it. Either way, jobs persist under the [metadata_store] backend's _jobs/ prefix and survive restarts.

The queue does not have its own storage backend. Durable jobs are written to the same backend configured for [metadata_store] (filesystem or S3, whichever metadata uses), under a hardcoded top-level _jobs/ prefix. There is no job-queue-level backend, credential, or prefix setting: the section accepts only the tunables below.

An honest atomic create is preferred and probed at startup. The durable queue is drained by separate processes that serialise on leased claim keys created atomically (link(2) on FS, If-None-Match: * on S3). A startup probe creates a scratch claim key twice; a backend where the second create succeeds cannot enforce the atomic create and degrades to advisory claims with a logged warning, where a claim race may run an idempotent job more than once. Correctness is unaffected either way.

OptionTypeDefaultDescription
pending_refresh_interval_secsu6415How often the server refreshes the angos_job_queue_pending gauge. Must be at least 5 (sub-5s ticks induce LIST storms on S3).
pending_ready_horizon_secsu64600Readiness horizon for the angos_job_queue_pending gauge. Only envelopes whose not_before falls within [..., now + horizon] are counted. Set comfortably larger than your worker pod startup time so KEDA has lead time to scale up before the work becomes claimable.
max_attemptsu325Times a failing job is retried before it is dead-lettered.
retry_backoff_min_msu64100First retry backoff delay; the exponential schedule grows from here.
retry_backoff_max_msu6410000Ceiling on the exponential retry backoff.
claim_ttl_secsu6460Lease on a job claim, in seconds. A crashed worker's jobs are taken over after this long; the holder refreshes the lease at a third of it. Minimum 3.

Crash takeover is bounded by claim_ttl_secs. A worker that claims a job leases the job's claim key for claim_ttl_secs and refreshes the lease at a third of it. If the holder dies mid-job, another worker takes the claim over once the lease lapses and re-runs the job; handlers are idempotent, so the re-run duplicates work at worst. Transient refresh errors are tolerated while the last verified lease still covers the holder.

See Enable Durable Cache Jobs for a full setup guide including angos worker invocation and KEDA autoscaling.

Global Access Policy (global.access_policy)​

OptionTypeDefaultDescription
defaultstring"deny"Default action when no rules match ("allow" or "deny").
rules[string][]CEL expressions for access control

Every policy table has this shape: access_policy, scan and index each take a default for what no rule matches and rules that decide the opposite.

Global Retention Policy (global.retention_policy)​

OptionTypeDefaultDescription
rules[string][]CEL expressions for retention

Pull History (global.pull_history)​

How each tag's and revision's pull history is kept once update_pull_time records it. Scrub compacts older pulls into chunks of up to 1000 and trims the history to its bounds; the newest pull is always kept, since retention reads it.

OptionTypeDefaultDescription
max_pullsu32 > 01000Pulls kept per target, which the pulls endpoint pages through.
max_age_secsu64unsetAge past which a pull leaves the history (e.g. 31536000 keeps a year).
compact_after_secsu643600Age past which a pull is compacted. The default applies only when compact_after_pulls is unset too.
compact_after_pullsu32 > 0unsetNewest pulls kept uncompacted per target. Set with compact_after_secs, a pull past either is compacted.

Compaction changes only how pulls are stored: each uncompacted pull is one key under !atime/, so the compact_after_* gates bound that key count, while max_pulls and max_age_secs bound what the history holds.

Index (global.index)​

The index policy of every repository without an index table of its own: which images have their filesystem indexed as they land, and whose listings angos reconcile index keeps. An image outside the policy is indexed the first time someone opens its filesystem.

OptionTypeDefaultDescription
defaultstringskipindex or skip: what an image no rule matches gets
rules[string][]CEL rules over the retention variables; an image a rule matches gets the opposite of default. last_pulled_at and top_pulled require update_pull_time = true

The table sets at least one of the two keys.


Cache (cache)​

What the registry caches: the JWKS and discovery documents of each OIDC provider, and the bearer tokens it holds against pull-through upstreams. All of it is derived and bounded by a TTL, so the default in-memory cache is safe across any number of replicas: nothing in it decides what a replica serves. Redis is optional, and shares those entries so a new replica starts warm and every replica refetches a JWKS once rather than once each.

Redis Cache (cache.redis)​

OptionTypeDefaultDescription
urlstringrequiredRedis URL (e.g., "redis://localhost:6379")
key_prefixstringrequiredPrefix for cache keys

Blob Storage (blob_store)​

Required. Choose one: blob_store.fs or blob_store.s3. A configuration naming neither fails to load, and so does an fs backend whose root_dir is empty, which would otherwise resolve every object against the process working directory.

Filesystem (blob_store.fs)​

OptionTypeDefaultDescription
root_dirstringrequiredDirectory for blob storage
sync_to_diskboolfalseForce fsync after writes

S3 (blob_store.s3)​

OptionTypeDefaultDescription
access_key_idstringrequiredAWS access key ID
secret_keystringrequiredAWS secret key
endpointstringrequiredS3 endpoint URL
bucketstringrequiredS3 bucket name
regionstringrequiredAWS region
key_prefixstring-Prefix for S3 keys
multipart_part_sizestring"50MiB"Minimum multipart part size (5 MiB to 5 GiB)
multipart_copy_thresholdstring"5GB"Blob size above which S3 upload completion uses multipart copy
multipart_copy_chunk_sizestring"100MB"Server-side part size for multipart copy
multipart_copy_jobsusize4Max concurrent multipart copy jobs
multipart_uniform_partsboolfalseUse uniform multipart upload mode
max_attemptsu323Retry attempts for S3 operations
operation_timeout_secsu64900Total operation timeout; a streamed upload body is exempt, since the pushing client paces it
operation_attempt_timeout_secsu64300Per-attempt timeout, and the read timeout that cuts a stalled transfer short; a streamed upload body is exempt
circuit_breaker_thresholdu325Consecutive failures that trip the circuit breaker open
circuit_breaker_cooldown_secsu6410Seconds the tripped breaker stays open before a half-open probe
children_scan_concurrencyusize16Concurrent range chains a truncated children/flat scan fans out to
presign_ttl_secsu641800Lifetime of a generated presigned download URL

S3 Blob Upload Modes​

The registry supports two modes for uploading blobs to S3, controlled by multipart_uniform_parts:

Non-uniform mode (default, multipart_uniform_parts = false)

Each OCI PATCH request streams into a long-lived S3 multipart upload, with no intermediate objects or assembly phase. When the client completes the upload with a PUT request, the multipart upload is finalized and the blob is copied to its content-addressed path. This mode works with most S3-compatible providers.

A PATCH that carries a Content-Length is uploaded directly as an UploadPart with that known length, split into equal parts only where it would breach S3's 5 GiB per-part ceiling. A chunked PATCH (no Content-Length, as docker push sends) is drained to EOF, flushing an UploadPart each time multipart_part_size bytes accumulate and restaging the trailing remainder.

Memory usage per upload: for a known-length PATCH, up to one ~1 MiB streaming read frame, with no data buffered beyond the current frame. For a chunked PATCH, one multipart_part_size part while it fills; the remainder the previous PATCH staged streams back into it, so the bound stays one part size per in-flight request.

Uniform mode (multipart_uniform_parts = true)

A long-lived S3 multipart upload is maintained across all PATCH requests. Both a known-length PATCH and a chunked PATCH (no Content-Length, as docker push sends) commit non-final parts of exactly multipart_part_size bytes; the final part may be smaller. The S3 protocol only requires non-final parts to be ≥ 5 MiB; uniform sizing is an additional constraint imposed by some S3 storage providers. Use this mode only if your provider rejects uploads with variable part sizes.

Memory usage per upload: streaming read frames for full parts, plus at most one trailing staged chunk smaller than multipart_part_size. A chunked PATCH buffers up to one multipart_part_size part, the same as the known-length remainder.

# Most S3 providers (AWS S3, Exoscale, etc.)
[blob_store.s3]
multipart_uniform_parts = false # Default

# Strict S3 providers (if non-uniform mode fails)
[blob_store.s3]
multipart_uniform_parts = true

Metadata Storage (metadata_store)​

Optional. Defaults to same backend as blob store.

Unknown Keys​

Unknown keys under any section are ignored, so configs carrying knobs of removed subsystems (lock_strategy, conditional_operations, access_time_debounce_secs, link_cache_ttl, cache_ttl) keep loading. Remove them at your convenience.

Filesystem (metadata_store.fs)​

OptionTypeDefaultDescription
root_dirstring-Directory for metadata (defaults to blob store)
sync_to_diskboolfalseForce fsync after writes

S3 (metadata_store.s3)​

The same connection options as blob_store.s3, and no others.

Warning: With update_pull_time enabled, every stamped manifest pull adds one storage write (the append-only access entry). At scale with many concurrent pulls this adds latency and API costs; disable access time tracking if it is not needed for retention policies.

Distributed Locking​

There is no lock backend: reads and writes are lock-free, blob reclamation is fenced by the v2/gc/ marker protocol, and the durable job queue serialises workers with atomically created claim keys. lock_strategy tables are ignored like any unknown key (see Unknown Keys).


Authentication (auth)​

Basic Auth (auth.identity.<name>)​

OptionTypeDefaultDescription
usernamestringrequiredUsername
passwordstringrequiredArgon2 password hash

Password hashes are validated when the configuration is parsed. An invalid Argon2 hash causes the server to fail to start with a clear error. Use angos argon to generate a valid hash.

Usernames must be unique across all auth.identity entries, and none may match an auth.oidc provider name: a Basic credential naming a provider is read as that provider's token. Either collision causes the server to fail to start.

OIDC (auth.oidc.<name>)​

Every provider takes the same options: a provider is an issuer plus how its tokens are validated, so there is no provider type to select.

OptionTypeDefaultDescription
issuerstringrequiredOIDC issuer URL
jwks_uristring-Custom JWKS URI (auto-discovered if not set)
server_ca_bundlestring-PEM CA bundle trusted for this provider's HTTPS fetches
client_certificate_bundlestring-PEM client certificate presented on those fetches, requires client_private_key
client_private_keystring-PEM key for client_certificate_bundle
bearer_token_filestring-File holding a bearer token sent on those fetches, read per fetch
required_claimsarray[]Claims a token must carry; a missing or null one is rejected
jwks_refresh_intervalu643600JWKS refresh interval (seconds)
required_audiencestring-Audience the token must carry and match; unset accepts any aud, including none
clock_skew_toleranceu6460Clock skew tolerance (seconds)
allowed_algorithmsarray["RS256"]Allowed JWT signing algorithms
http_request_timeout_secsu6430Timeout for a JWKS or discovery HTTP fetch (seconds)
jwks_refresh_timeout_secsu645Timeout for the forced JWKS refetch on key rotation (seconds)

GitHub Actions, for example, is one such entry:

[auth.oidc.github-actions]
issuer = "https://token.actions.githubusercontent.com"
jwks_uri = "https://token.actions.githubusercontent.com/.well-known/jwks"
required_claims = ["repository", "actor"]

Set server_ca_bundle for an issuer whose certificate the system roots do not cover, such as a kube-apiserver signed by the cluster CA. It applies to the discovery and JWKS fetches for that provider alone; other providers keep the system roots.

Set client_certificate_bundle and client_private_key for an issuer that refuses an anonymous caller on those endpoints, a kube-apiserver serving discovery to authenticated users only being the usual case. Configuring one without the other fails startup rather than fetching anonymously.

Set bearer_token_file instead for an issuer that authenticates callers with a token, such as the same kube-apiserver reached with angos's own projected service-account token. The file is read on every fetch, so a token the kubelet rotates in place stays current, and an unreadable path fails startup. The token is sent only to URLs on the issuer's own origin: jwks_uri comes out of the discovery document, and an issuer naming another host is not handed it.

required_claims checks presence only. Predicates over claim values belong in the access policy, which sees the whole claim map.

allowed_algorithms accepts JWT algorithm names such as "RS256", "RS384", "RS512", "ES256", and "ES384". Angos rejects tokens whose header claims an algorithm outside the provider allowlist before signature verification to prevent algorithm-confusion attacks.

Token Service (auth.token_service)​

Issues registry-signed bearer tokens at GET /token, so a client holding a short-lived credential can exchange it once and keep pushing after that credential expires. Present the section to enable it.

OptionTypeDefaultDescription
secret_keystringrequiredBase64 HMAC signing key, at least 32 bytes decoded
realmstring-Absolute token URL advertised to clients, path must end with /token
ttl_secsu643600Token lifetime in seconds, at most 86400

With the section present, a 401 carries WWW-Authenticate: Bearer instead of Basic, for every client rather than only OIDC ones. Left unset, the challenge is built from each request's own Host, which is what a registry serving several hostnames wants; behind a TLS-terminating proxy, list the proxy in global.trusted_proxies so its X-Forwarded-Proto decides the scheme. Set realm when anything in front of the registry caches responses, so the challenge cannot follow a Host a client chose, and when a proxy strips a path prefix, so the advertised URL is the prefixed one clients must call.

Generate secret_key with openssl rand -base64 32: it is decoded before use, so its strength is the randomness of those bytes and not the length of a passphrase. Rotating it invalidates every outstanding token; clients recover by fetching a new one. An issued token freezes the identity it was minted from but not its permissions: access policies are still evaluated per request. A token cannot otherwise be revoked before it expires, so ttl_secs is the window a stolen one stays usable. Removing or renaming an auth.oidc entry invalidates outstanding tokens minted from that provider. The section reloads without a restart, secret_key included, so rotating the key during an incident costs no downtime.

GET /token is subject to the access policy like any other route. Under default = "deny", add a rule for it:

[global.access_policy]
default = "deny"
rules = [
"request.action == 'get-token' && identity.oidc != null",
]

Webhooks (auth.webhook.<name>)​

OptionTypeDefaultDescription
urlstringrequiredWebhook URL
timeout_msu64requiredRequest timeout in milliseconds
bearer_tokenstring-Bearer token for authentication
basic_auth.usernamestring-Basic auth username
basic_auth.passwordstring-Basic auth password
client_certificate_bundlestring-Client cert for mTLS
client_private_keystring-Client key for mTLS
server_ca_bundlestring-CA bundle for server verification
forward_headers[string][]Headers to forward from client

url and forward_headers are validated when the configuration is loaded. If either client_certificate_bundle or client_private_key is set, both must be set.


Repository (repository."<namespace>")​

Repository namespace keys must not overlap: a key like team and a key like team/app are considered overlapping because one is a namespace-prefix of the other. The registry rejects this configuration at startup, as it does two repositories declaring the same namespace.

OptionTypeDefaultDescription
namespacestringnoneRegistry namespace this repository mirrors (docker.io), as a client names it in the ?ns= proxy parameter. A request naming it is served from this repository whatever path it asks for; see Upstream Selection
immutable_tagsboolfalseEnable immutable tags for this repository. The effective flag is this value OR global.immutable_tags, so a repository can add immutability but never opt out of a global true
immutable_tags_exclusions[string]inheritsReplaces the global exclusion list when non-empty
scantable-Which images pushed here, or stored by a cache miss in a pull-through repository, are sent to the scanner service, and when their reports are refreshed; see Scan
indextable-Which images pushed here, or stored by a cache miss, have their filesystem indexed as they land, so the web UI browses them at once; see Index
authorization_webhookstringinheritsWebhook name (empty to disable)
event_webhooks[string]inheritsEvent webhook names

Upstream (repository."<namespace>".upstream)​

Array of upstream registries for pull-through cache.

OptionTypeDefaultDescription
urlstringrequiredUpstream registry URL. A bare host pulls from the registry root after stripping the repository name (<repo>/x → upstream x); a path (https://host/team) becomes the upstream namespace prefix instead (<repo>/x → upstream team/x). Pulling the repository root <repo> itself maps to <repo> for a bare host and to <path> for a path URL. Angos talks to the OCI /v2/ root, so the path is mapped into the namespace, not the HTTP path.
max_redirectu85Maximum redirects to follow
connect_timeout_secsu6430Timeout for establishing the connection (TCP + TLS handshake)
read_timeout_secsu64300Per-read inactivity timeout during a transfer; not a whole-transfer cap, so a large blob is never limited by total time
server_ca_bundlestring-CA bundle for server verification
client_certificatestring-Client certificate for mTLS
client_private_keystring-Client key for mTLS
usernamestring-Basic auth username
passwordstring-Basic auth password

Downstream (repository."<namespace>".downstream)​

Array of downstream registries to which this repository's mutations are replicated. See Configure Replication.

OptionTypeDefaultDescription
namestringrequiredLocal identifier for this downstream (logs, downstream metric label)
urlstringrequiredDownstream registry URL. A bare host (http://host:8000) mirrors the namespace verbatim; a path (http://host:8000/team) becomes the namespace prefix the content lands under, replacing the source repository prefix (<repo>/x → team/x). The repository root <repo> itself maps to <repo> for a bare host and to <path> for a path URL. Angos serves the OCI API at the root, so the path is mapped into the namespace, not the HTTP path.
modestring"event+reconcile""event+reconcile", "event-only", or "reconcile-only"
namespace_filter[string][] (all)Regex patterns; a namespace replicates here only if it matches one
max_concurrent_pushesusize4Concurrent blob pushes per manifest for this downstream (positive integer, >= 1)
pruneboolfalseWhen true, reconciliation also deletes downstream-only tags (authoritative one-way mirror; unsafe for active-active peers)
max_redirectu85Maximum redirects to follow
connect_timeout_secsu6430Timeout for establishing the connection (TCP + TLS handshake)
read_timeout_secsu64300Per-read inactivity timeout during a transfer; not a whole-transfer cap, so a large blob push is never limited by total time
usernamestring-Basic auth username
passwordstring-Basic auth password
server_ca_bundlestring-CA bundle for downstream TLS verification
client_certificatestring-Client certificate for mTLS (requires client_private_key)
client_private_keystring-Client key for mTLS (requires client_certificate)

mode values:

  • event+reconcile (default): push on every local mutation and include in angos reconcile replication.
  • event-only: push on local mutations; excluded from angos reconcile replication reconciliation.
  • reconcile-only: excluded from live pushes; mirrored only via angos reconcile replication.

If either client_certificate or client_private_key is set, both must be set.

Access Policy (repository."<namespace>".access_policy)​

Same as global.access_policy.

Retention Policy (repository."<namespace>".retention_policy)​

Same as global.retention_policy.

Scan (repository."<namespace>".scan)​

The scan policy of this repository's images, in place of the one in global.scan: the same default, scan or skip, and rules. Requires [global.scan], and sets at least one of the two keys.

Index (repository."<namespace>".index)​

The index policy of this repository's images, in place of global.index: the same default, index or skip, and rules. Sets at least one of the two keys.


Event Webhooks (event_webhook.<name>)​

HTTP POST notifications for registry operations. See Event Webhooks Reference for full details.

OptionTypeDefaultDescription
urlstringrequiredHTTP/HTTPS endpoint URL
policystringrequiredDelivery policy: required, optional, async
events[string]requiredEvent types to deliver (at least one)
tokenstring-Bearer token and HMAC signing secret
timeout_msu645000HTTP request timeout in milliseconds
max_retriesu32policyMaximum retry attempts after initial failure (max 16); defaults to 3 for required, 0 otherwise
repository_filter[string]-Regex patterns to match repository names

url, events, token, and repository_filter are validated when the configuration is loaded. If token is set, it must not be empty.

Webhooks are enabled by referencing their names:

LocationOptionTypeDescription
globalevent_webhooks[string]Webhook names for all repositories
repository."<namespace>"event_webhooks[string]Webhook names for this repository

Scanning (global.scan)​

The scanner service scanning repositories send their image pushes to, and the scan policy of every repository without a scan table of its own.

OptionTypeDefaultDescription
urlstringrequiredBase URL of the scanner service; the job posts to its /scan
tokenstring-Bearer token the service expects, when it checks one
timeout_secsu64600Bound on one scan request, pull and analysis included
defaultstringskipscan or skip: what an image no rule matches gets, as it lands and on every angos reconcile scan run
rules[string][]CEL rules over the retention variables and image.scanned_at, the time of the image's newest report; an image a rule matches gets the opposite of default. last_pulled_at and top_pulled require update_pull_time = true

A repository with a scan table follows that table instead; one without [global.scan] fails validation. With neither default nor rules here, only repositories with a table scan. max_concurrent_scan_jobs in [global] sizes the scan queue's worker pool.

A policy is judged as an image lands, with image.scanned_at at 0 and the pushed tags as the whole ranking, and again by angos reconcile scan with the image's real times, so image.scanned_at < now() - days(30) alone scans a new image and refreshes its report monthly. See Scan Images. A report a newer one supersedes is judged by the retention rules like any untagged manifest; see Configure Retention Policies.

Scanner Service (scanner)​

Read by angos scanner alone, so a scanner host's configuration can hold this section and nothing else; the registry ignores it.

OptionTypeDefaultDescription
bind_addressstring0.0.0.0Address the service listens on
portu168766Port the service listens on
tokenstring-Bearer token a scan request must carry; unset accepts any
max_concurrent_scansusize2Scanner processes run at once; further requests wait. Trivy locks its cache and runs one at a time whatever the value
registry.urlstringrequiredRegistry the scanner pulls images from
registry.usernamestring-Identity the scanner pulls with
registry.passwordstring-Its password

Observability​

Tracing (observability.tracing)​

OptionTypeDefaultDescription
endpointstringrequiredOpenTelemetry endpoint
sampling_ratef64requiredSampling rate (0.0 - 1.0)

Prometheus Metrics​

Angos emits Prometheus metrics on the /metrics endpoint. See the Metrics Reference for the metric names and label values.


Web UI (ui)​

OptionTypeDefaultDescription
enabledboolfalseEnable web interface
namestring"Angos"Registry name in UI header

Performance Tuning​

max_concurrent_requests​

Controls the number of Tokio worker threads handling HTTP requests. Default: 64.

Registry operations are likely I/O-bound (network transfers, storage I/O), so more threads than CPU cores typically improves throughput.

Rule of thumb: Start with 8-16x your CPU core count and adjust based on monitoring.


Example Configuration​

[server]
bind_address = "0.0.0.0"
port = 8000

[server.tls]
server_certificate_bundle = "/tls/server.crt"
server_private_key = "/tls/server.key"

[global]
update_pull_time = true
immutable_tags = true
immutable_tags_exclusions = ["^latest$"]

[blob_store.fs]
root_dir = "/var/registry/blobs"

[metadata_store.fs]
root_dir = "/var/registry/metadata"

[cache.redis]
url = "redis://localhost:6379"
key_prefix = "angos"

[auth.identity.admin]
username = "admin"
password = "$argon2id$v=19$m=19456,t=2,p=1$..."

[auth.oidc.github-actions]
issuer = "https://token.actions.githubusercontent.com"
required_claims = ["repository", "actor"]

[global.access_policy]
default = "deny"
rules = ["identity.username != null"]

[repository."docker-io"]
[[repository."docker-io".upstream]]
url = "https://registry-1.docker.io"

[ui]
enabled = true
name = "My Registry"

S3-Only Multi-Instance Deployment​

This example uses S3 for both blob and metadata storage; multiple instances need no coordination infrastructure:

[server]
bind_address = "0.0.0.0"
port = 8000

[global]
update_pull_time = true

[blob_store.s3]
# Example credentials - replace for production
access_key_id = "your-access-key-id"
secret_key = "your-secret-key"
endpoint = "https://s3.example.com"
bucket = "registry"
region = "us-east-1"

[metadata_store.s3]
# Example credentials - replace for production
access_key_id = "your-access-key-id"
secret_key = "your-secret-key"
endpoint = "https://s3.example.com"
bucket = "registry-metadata"
region = "us-east-1"

[auth.identity.admin]
username = "admin"
password = "$argon2id$v=19$m=19456,t=2,p=1$..."