Skip to content

Container Registry ​

The Container Registry gives your tenant a private OCI registry for your container images, hosted in the EU. Images are stored exclusively in the EU/EEA and are never public. Any OCI client works — docker, podman, a CI runner, or a Kubernetes cluster pulling with an image pull secret.

The registry endpoint is registry.sweden.frostmoln.cloud.

Pushes and pulls go straight to that endpoint with a credential you mint here. They do not travel through the Frostmoln API, so nothing in your build pipeline needs to know about the platform API at all.

Enable your registry ​

The registry is an explicit opt-in. Enabling it creates your namespace and the billable storage that comes with it — being permitted to use the offer does not create one by itself.

In the portal, go to Container Registry and choose Enable container registry.

bash
fm registry enable
fm registry status      # where to push, and whether it is enabled

Nothing is configurable at creation: your namespace is derived from your tenant id, so there is nothing a request could say that would change it. Re-running the opt-in through the CLI or Terraform on an already-enabled tenant is not an error — it reports the current state and exits 0, so it is safe in a provisioning script. (The API itself answers 409 there; the CLI and the Terraform provider absorb it.)

Enabling cannot be undone, and closing your tenant destroys your images

There is no "disable" action, in the portal, the CLI or the API. The only way a registry is removed is by closing the tenant, and that permanently destroys every image in it. Treat enabling as create-once, and contact support if a registry needs to be removed.

See What happens when you close your tenant below before you close one — there is a 30-day window to export from, and it is the only chance.

Your namespace and image references ​

Your namespace is t- followed by your tenant id with the dashes removed — for example t-3f2a9c1e4b7d4a2f8e1c5d6b7a8f9e0d. Every image reference has the shape:

registry.sweden.frostmoln.cloud/<your-namespace>/<image>:<tag>

The namespace is derived from your immutable tenant id, not from your tenant's name. Renaming your tenant does not change it, so image references you have already pinned in a Dockerfile, a Helm chart or a deployment manifest stay valid.

The portal shows your exact namespace under Where to push, and the CLI prints it:

bash
fm registry status

Credentials ​

A credential is the username and password you docker login with. Mint one in the portal under Credentials → Create credential, or from the CLI:

bash
fm registry credential create --name ci-pipeline          # pull-only (the default)
fm registry credential create --name ci-pipeline --capability push
fm registry credential list
fm registry credential get <id>
fm registry credential delete <id>                        # revoke (prompts first)
fm registry credential delete <id> --force                # same, unattended
  • Name is your own label — it is not part of the username. Name each credential after the single consumer that holds it. With no per-repository scoping, the label is the only thing that later tells you which credential leaked and what breaks when you revoke it.
  • Capability is pull (the default) or push. Push implies pull. Mint pull unless that consumer actually publishes images — a push credential can overwrite any existing tag in your namespace, including one your production manifests already reference.
  • The username carries a robot$ prefix, e.g. robot$t-3f2a9c1e4b7d4a2f8e1c5d6b7a8f9e0d-a1b2c3d4.
  • One credential reaches every namespace your tenant owns, because docker login is per host. There is no way to scope a credential to a single repository, so the blast radius of any credential is your whole namespace.
  • Credentials do not expire and cannot be rotated in place, so revoke and re-mint them deliberately — when someone leaves, when a CI system is replaced, or on a schedule of your own.
  • You may hold up to 20 at a time; minting past that is refused, and the remedy is to revoke one you no longer use.
  • Revoking takes effect immediately for anything that logs in again: the credential can no longer authenticate, with no grace period, so a pipeline or a cluster using it starts failing at once. A session that already holds a registry token may finish out that token's short lifetime, so revocation is the start of containment, not the whole of it.

The password is shown once

The password is displayed only at creation. Neither Frostmoln nor the registry keeps a retrievable copy, and there is no rotation endpoint. Capture it immediately and put it in Secrets or your own secret manager.

Keep it out of source control, CI job logs, Dockerfile ARG/ENV and --build-arg values (all of which persist in image layers and image history), plain Kubernetes manifests, support tickets or chat, and your clipboard history.

If you lose it, the only recovery is to revoke the credential and mint a new one. If it escapes rather than gets lost, see If a credential leaks.

Logging in ​

At a terminal, let docker prompt for the password — that keeps it out of your shell history and out of the process list:

bash
docker login registry.sweden.frostmoln.cloud -u 'robot$t-...-a1b2c3d4'

Quote the username in your shell

The username contains a $. In bash or zsh an unquoted robot$t-... is expanded away and the login is refused, which makes a perfectly good credential look broken. Always single-quote it.

In CI, where there is no prompt, feed the password on stdin — never with -p/--password, which puts it in the process arguments on a shared runner:

bash
printf '%s' "$REGISTRY_PASSWORD" | docker login registry.sweden.frostmoln.cloud \
  -u 'robot$t-...-a1b2c3d4' --password-stdin

Use printf '%s' rather than echo — echo interprets backslash escapes in zsh and eats a leading -n/-e in bash, either of which silently mangles a password you cannot read back.

Take $REGISTRY_PASSWORD from your runner's masked-secret mechanism, and make sure the step does not run under set -x: command tracing expands the variable, printing the password into the build log where masking may not reach it.

docker login stores the credential on disk

It writes to ~/.docker/config.json, base64-encoded, not encrypted, unless you have configured a credential helper (docker-credential-osxkeychain, pass, secretservice). That file is easy to copy into a build context, a backup or a synced dotfile repo. Configure a helper on a workstation, and run docker logout registry.sweden.frostmoln.cloud when you are done. podman writes to $XDG_RUNTIME_DIR/containers/auth.json instead — podman logout clears that one.

On a self-hosted or otherwise long-lived CI runner, credential helpers usually are not available and the file would outlive your job, where the next job can read it. Give each job its own config directory and remove it at the end:

bash
export DOCKER_CONFIG="$(mktemp -d)"
# ... login, build, push ...
docker logout registry.sweden.frostmoln.cloud; rm -rf "$DOCKER_CONFIG"

Getting a new secret into a secret store ​

The default table output already prints the secret to your terminal, and under -o json the CLI emits it unmasked — which is what lets you pipe it somewhere without it ever appearing on a command line:

bash
fm registry credential create --name ci -o json | jq -j .secret

Use jq -j, not jq -r: -r adds a trailing newline, and a store that keeps it gives you a password that fails every login — with no second copy to compare against.

No fm secrets command reads a value from stdin (fm secrets create takes --value only, which would put the secret back on a command line), so send it to the portal's Secrets → Create form, or to a manager whose CLI reads stdin:

bash
set -o pipefail
fm registry credential create --name ci -o json | jq -j .secret \
  | vault kv put secret/frostmoln/registry password=-

set -o pipefail matters here: without it a failure upstream still exits 0 and you close the terminal believing a one-time secret was saved. Read the value back and compare it before you close that terminal — this is the only moment when you still can. Note that vault kv put replaces every key at the path, so use vault kv patch (or a separate path) if you are also storing the username there.

Whichever sink you use: do not redirect it to a file, run it under set -x, or run it in a CI job that echoes commands. The secret is unmasked, and it is also sitting in your terminal scrollback and session log — clear those when you are done.

If a credential leaks ​

  1. Revoke it — fm registry credential delete <id>, or Revoke in the portal. No further logins with it succeed.
  2. Mint a replacement and update every consumer that held the old one. If a cluster pulls with it via the External Secrets Operator, force a refresh or delete the synced Secret — otherwise it keeps using the dead credential until its next refresh interval.
  3. Work out what else was in the same place. If it leaked through ~/.docker/config.json, Terraform state, a build log or a synced dotfile repo, every other credential in that artifact leaked with it.
  4. Assess the damage by capability.
    • A push credential could have overwritten any tag in your namespace. Compare the digests your manifests pin against docker manifest inspect <ref>; a mismatch is an overwrite. You can delete the resulting artifacts yourself — see Freeing space — but note that deleting an overwritten tag removes the CURRENT content, so re-push the digest you trust rather than deleting your way back to it.
    • A pull credential means everything in your namespace was readable, and revoking does not undo a read. Assume anything ever pushed there was taken, including secrets baked into image layers.

Frostmoln cannot tell you whether a leaked credential was actually used — there is no customer-visible access log for the registry — so the digests you recorded beforehand are your only evidence. That is the practical reason to pin by digest.

A suspended registry cannot be contained

While your registry is suspended, both revoke and mint are refused. A leaked credential stays live and you cannot replace it. Contact support immediately rather than working through the steps above.

Because nothing expires and nothing can be rotated in place, this is also the routine you run on a schedule rather than only after an incident.

Push and pull an image ​

bash
# 1. Log in (single-quote the username — it contains a $)
docker login registry.sweden.frostmoln.cloud -u 'robot$t-...-a1b2c3d4'

# 2. Tag your image for your namespace
docker tag my-app:latest \
  registry.sweden.frostmoln.cloud/t-3f2a9c1e4b7d4a2f8e1c5d6b7a8f9e0d/my-app:1.0.0

# 3. Push
docker push \
  registry.sweden.frostmoln.cloud/t-3f2a9c1e4b7d4a2f8e1c5d6b7a8f9e0d/my-app:1.0.0

# 4. Pull it back anywhere that has a credential
docker pull \
  registry.sweden.frostmoln.cloud/t-3f2a9c1e4b7d4a2f8e1c5d6b7a8f9e0d/my-app:1.0.0

podman takes the same commands. A pull credential is enough for step 4; step 3 needs push.

Pull from a managed Kubernetes cluster ​

Kubernetes authenticates to a private registry with an image pull secret — a Secret of type kubernetes.io/dockerconfigjson. Mint a credential with --capability pull and give the cluster its own, rather than sharing your CI one, so revoking either does not break the other.

The pulling workload also needs outbound internet access to reach the registry endpoint, which means its VPC needs a gateway. A VPC is created without one unless you ask for it, and without a gateway there is no internet access and no DNS resolution — so the pull fails before it starts.

On Frostmoln managed Kubernetes, select the External Secrets Operator add-on when you create the cluster, or add it to the running cluster first (see its guide); on a cluster you run elsewhere, install ESO yourself. Then store the credential's username and password in Secrets, named registry-username and registry-password to match the remoteRef.key values below — use the portal's Secrets → Create form, or the piping recipe above, rather than fm secrets create --value, which would put the password on a command line.

Then let the External Secrets Operator build the pull secret in the cluster, so the password never appears in a manifest:

A SecretStore is namespaced

SecretStore resolves in the ExternalSecret's own namespace, so the frostmoln store and the labelled API-key Secret behind it must both exist in my-app — repeat steps 1 and 2 of the External Secrets Operator guide there. Be deliberate about that: it places an API key that can read every secret in your tenant into the namespace, in exchange for keeping one registry password out of it. Scope that key to secrets read-only, and keep the namespace's RBAC tight.

yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: frostmoln-registry
  namespace: my-app
spec:
  refreshInterval: 1h
  secretStoreRef: { name: frostmoln, kind: SecretStore }
  target:
    name: frostmoln-registry
    template:
      # Without this type + key, ESO produces an Opaque Secret and kubelet
      # will not use it as a pull secret.
      type: kubernetes.io/dockerconfigjson
      data:
        .dockerconfigjson: |
          {"auths":{"registry.sweden.frostmoln.cloud":{"username":{{ .username | toJson }},"password":{{ .password | toJson }}}}}
  data:
    - secretKey: username
      remoteRef: { key: registry-username }
    - secretKey: password
      remoteRef: { key: registry-password }

toJson quotes and escapes the values, so a password containing a " or a \ still produces a valid document. Note the refreshInterval: after you replace a credential, the cluster keeps using the old one until the next refresh, so lower it or delete the synced Secret to force one.

One-off: create the Secret directly ​

This is the path on Frostmoln managed Kubernetes.

This puts the password in your shell history

The password appears in ~/.bash_history / ~/.zsh_history and in the process arguments (/proc/<pid>/cmdline) for the life of the call. Use this to bootstrap by hand, never in a script or a CI job — in a CI job with command tracing it lands in the build log. On a cluster that runs ESO, prefer the ESO recipe above.

bash
kubectl create secret docker-registry frostmoln-registry \
  --docker-server=registry.sweden.frostmoln.cloud \
  --docker-username='robot$t-...-a1b2c3d4' \
  --docker-password='<the password shown once>' \
  --namespace my-app

Either way, reference it from the pod spec:

yaml
spec:
  imagePullSecrets:
    - name: frostmoln-registry
  containers:
    - name: my-app
      image: registry.sweden.frostmoln.cloud/t-3f2a9c1e4b7d4a2f8e1c5d6b7a8f9e0d/my-app@sha256:<digest>

Pinning by digest rather than by tag is worth the extra characters here. Tags in your namespace are mutable and anyone holding a push credential can move one, so a digest is the only reference that cannot change under a running deployment.

Terraform ​

The opt-in and the credential are both Terraform resources:

hcl
resource "frostmoln_container_registry" "this" {}

resource "frostmoln_container_registry_credential" "ci" {
  name       = "ci-pipeline"
  capability = "pull" # the default; use "push" only for a publisher

  depends_on = [frostmoln_container_registry.this]
}

output "image_prefix" {
  value = "${frostmoln_container_registry.this.endpoint}/${frostmoln_container_registry.this.namespace}"
}

The credential's secret is written to Terraform state

It is returned only once, so the provider persists it. With no backend configured, Terraform writes terraform.tfstate as plaintext JSON in your working directory — usually inside a git repository, which contradicts every other rule on this page.

Use an encrypted remote backend (Frostmoln object storage via the S3 backend works), and add terraform.tfstate* to .gitignore — terraform.tfstate.backup is a second plaintext copy that survives the move to a remote backend.

The secret also reaches terraform show, JSON plan files, any CI artifact or cache that keeps state between runs, and TF_LOG=DEBUG/TRACE output, which records the provider's HTTP responses — including the one carrying the secret. Marking an output sensitive = true hides it from console output only — not from state, and not from terraform output -json.

terraform destroy does not delete the registry. There is no teardown endpoint — the resource is removed from state with a warning, while the namespace, its images and its billable state all survive. Removing the registry means closing the tenant, which destroys the images with it; see What happens when you close your tenant. The credential is immutable: changing name or capability replaces it and issues a new secret, so update your consumers when you do.

Storage and the limit ​

Your namespace has a hard storage cap: your tenant's container registry storage quota — 10 GB unless you have had an increase approved, in which case it is the approved amount. Each pull-through cache namespace has its own cap of up to 10 GiB (10737418240 bytes). A push that would cross it is refused with 403, and the refusal names all three figures — the size that would not fit, what you are already using, and your cap:

http
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8

{"errors":[{"code":"DENIED","message":"adding 900.0 KiB of storage resource, which when updated to current usage of 300.0 KiB will exceed the configured upper limit of 1.0 MiB."}]}

That is the registry's own reply, captured against a namespace with a deliberately small cap; yours names your own. What docker push or another client prints from it is up to that client. The figures are prose inside message, in whichever unit the registry picks — read them, do not parse them. The cap actually in force for your namespace is the one shown in the portal under Storage used and by fm registry status; read it there rather than assuming a figure.

That usage figure is what the registry counts against your cap: the deduplicated bytes stored in your namespace. It is not the sum of the image sizes you see in docker image ls, and the two genuinely differ — layers shared between tags in the same namespace are counted once. Counting is per namespace, and the cap is applied per namespace: the same layer held for a different namespace is counted again. Read it as "storage in use against your cap", and do not use it to reconcile a bill. It already reflects your push by the time the push completes — though a push that adds no new layers barely moves it.

Freeing space ​

Delete an image and the space comes back. In the portal, open a repository under Images and press Remove on the row; from the CLI:

bash
fm registry image list web/api                       # find what to remove
fm registry image list web/api -o json               # digests in full
fm registry image delete web/api sha256:1b0c8dd1...  # prompts first
fm registry image delete web/api sha256:1b0c8dd1... --force

It takes a digest, not a tag, and a tag is refused rather than guessed. A tag moves. If your pipeline re-pushed v1 between you reading the list and pressing enter, deleting "the one tagged v1" would destroy the image you meant to keep, and nothing would tell you. Copy the digest from the listing.

The image goes, not just the tag. Every tag pointing at it goes with it, and so does the manifest — afterwards it cannot be pulled by name or by digest. There is no untag operation: removing a label while leaving the content pullable by digest frees nothing and only looks like a delete.

Your storage figure drops immediately — by less than the size shown

The figure under Storage used, and in fm registry status, already reflects the delete by the time it returns. You do not wait for a cleanup window.

What it does not fall by is the size the image list showed for that image. That column is the image's own size and counts every layer in full; within a namespace your cap counts each stored layer once. Deleting an image whose base layers your other tags still use frees only the layers nothing else references — often a small fraction of the number you clicked on.

So: delete, then read the new figure. Do not predict it from the size column, and do not keep deleting to chase a number that was never going to arrive.

Start with the untagged ones. A row marked <untagged> is a manifest a later push orphaned: nothing can pull it by name, and it still counts against your cap. It is usually the bulk of what a full namespace is holding.

bash
# every untagged image in a repository, digests only
fm registry image list web/api --all -o json | jq -r '.data[] | select(.tags == []) | .digest'

To do this automatically, set a retention policy. It deletes untagged images, and tagged images beyond the newest ones you choose to keep, every night.

A multi-architecture image is deleted by its index. Its per-platform parts cannot be removed on their own and answer with a conflict; delete the index and they go with it.

A suspended registry cannot delete. Writes are refused while pulls and the listings keep working, so you can always still see what you hold.

docker rmi does not free anything. It removes a local image from your own machine and sends no request to the registry at all, so the storage counted against your cap is unchanged.

If no storage figure is shown at all, read that as "not reported", never as "unlimited" — pushes can be refused in that state. Contact support if you see it.

The portal and fm show sizes in SI units (1 GB = 10⁹ bytes), the same unit your storage quota is requested in. A pull-through cache's cap of 10737418240 bytes (10 GiB) therefore appears as about 10.7 GB; both describe the same limit.

Retention policies ​

A retention policy deletes old images for you, so CI pushes do not pile up against your cap. Your tenant has one policy. It applies to every repository in your namespace, and each repository is evaluated on its own.

The policy has two rules. Both are optional, but you set at least one:

RuleWhat it doesRange
Keep the last N tagged imagesKeeps the N most recently pushed tagged images in each repository and deletes the older ones.1-1000
Delete untagged images after X daysDeletes untagged images whose last push is more than X days ago.1-365

A rule you leave unset keeps everything in its class. With only the first rule, untagged images are never deleted; with only the second, tagged images are never deleted.

N counts images, not tags. An image with three tags counts once. Images pushed at the same moment are kept in no guaranteed order, so if two of them sit at the edge of N, do not rely on which one survives. Pushing an image again, or giving an existing image a new tag, counts as a fresh push: the image moves to the newest end for the keep-last rule.

A multi-architecture image is kept or deleted as one. Its per-platform parts, and any signatures attached to it, follow the image. A policy never leaves you half an image or strips a signature from one it keeps.

The policy runs every night, between 00:00 and 06:00 UTC. There is no custom schedule and no way to run it on demand. A policy you save today takes effect at the next run.

Deleted images cannot be recovered

A retention run deletes the image, every tag on it and its manifest, exactly as a manual delete does. Before you save a policy, check that nothing you deploy pins an image the policy would remove — for example a release you only pull by digest.

Pull-through caches are not affected. A retention policy covers your own namespace only. Each cache keeps its own rule — an image nobody pulls for 7 days is removed — and you cannot change it; see Pull-Through Caches.

The space shows up in your usage after the nightly run. Your storage figure, and therefore what you are billed for, reflects the freed space by the next morning. Read the figure then rather than counting on an exact time.

Saving a policy replaces the whole policy. A rule you leave out when you save is cleared, not kept from before.

Setting a policy needs permission to delete images. Because a policy deletes images, saving one requires registry:artifacts:delete as well as registry:settings:update (see Operations). Removing a policy needs only registry:settings:update.

Set a policy ​

In the portal, open Container Registry and use the retention card. From the CLI:

bash
fm registry retention set --keep-last-tagged 10 --delete-untagged-after-days 14
fm registry retention get      # what is set now
fm registry retention delete   # remove the policy; nothing more is deleted

Saving asks first when the new policy deletes more. If it would delete more than the current one (no policy before, a rule newly switched on, or a lower number), the portal and fm show both and ask you to confirm. In scripts, use fm registry retention set --force; without a terminal, set refuses unless --force is given.

With Terraform:

hcl
resource "frostmoln_container_registry_retention" "this" {
  keep_last_tagged           = 10
  delete_untagged_after_days = 14

  depends_on = [frostmoln_container_registry.this]
}

The depends_on points at the registry resource from Terraform above, so a configuration that also enables the registry creates it first.

Removing the resource, or running terraform destroy, removes the policy. It does not bring back images an earlier run deleted.

If your registry is suspended ​

A suspended registry still serves pulls. Pushing new images is blocked and your push credentials show as disabled — your pull credentials keep working, deliberately, because that is how you retrieve your images while suspended.

While suspended you can neither mint nor revoke a credential. So if you hold only push credentials, you cannot create the pull credential you would need to export. Keeping one standing pull credential avoids that.

Contact support to restore a suspended registry.

What happens when you close your tenant ​

Closing a tenant is the only way its registry is removed, and it permanently destroys every image in it. There is no recovery afterwards and no backup Frostmoln can restore from.

The sequence is fixed:

WhenWhat happens
Day 0, you close the tenantThe registry becomes read-only. Pushing is blocked and your push credentials are disabled. The pull credentials you already hold keep working.
Days 0-30Your export window. Pull whatever you need to keep.
Day 30Every image, every credential and the namespace itself are permanently deleted.

The pull credential has to exist before you close. While a tenant is closing you can neither mint nor revoke a credential, so a tenant holding only push credentials has no way to export at all — the credential you would need cannot be created any more. Keep one standing pull credential if you may ever need to export.

Exporting is an ordinary pull. List what you hold, then pull each reference:

bash
fm registry repository list
fm registry image list <repository>
docker pull registry.sweden.frostmoln.cloud/<your-namespace>/<repository>:<tag>

If you only want to free space rather than leave, see Freeing space — deleting artifacts does not close anything.