<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:hub="http://hub.upcloud.com/"><channel><title>UpCloud Container Registry Guides</title><link>https://upcloud.com/docs/guides/container-registry/</link><description>Recent content in Container Registry Guides on Documentation</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>hello@upcloud.com (UpCloud)</managingEditor><webMaster>hello@upcloud.com (UpCloud)</webMaster><copyright>© 2026 UpCloud Ltd</copyright><atom:link href="https://upcloud.com/docs/guides/container-registry/index.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Mon, 09 Nov 2026 10:08:54 +0000</lastBuildDate><item><title>How to deploy automatically with Container Registry webhooks</title><link>https://upcloud.com/docs/guides/deploy-automatically-container-registry-webhooks/</link><pubDate>Mon, 09 Nov 2026 10:08:54 +0000</pubDate><dc:creator>UpCloud</dc:creator><guid>https://upcloud.com/docs/guides/deploy-automatically-container-registry-webhooks/</guid><hub:summary/><hub:linkHref/><hub:linkTitle/><description>
Beta notice UpCloud Managed Container Registry is in beta. Features shown in this guide may change before general availability.
When your app runs in a container on a Cloud Server, the server can update the app automatically each time you push a new version of its container image to UpCloud Managed Container Registry. A registry webhook tells the server about the push, and the server pulls the image and restarts the app with Docker Compose.
This guide walks you through setting this up for a web app, from preparing the server to pushing your first update. The examples use the demo/hello-upcloud image, a small web page served on port 80, but you can follow along with your own image.
How it works Everything runs on one Cloud Server. Docker Compose runs the app in a container, a small Python receiver handles the webhooks, and Caddy serves both over HTTPS.
Caddy is an open-source web server that gets and renews TLS certificates automatically, so there's no certificate setup to do. If you already use another web server, such as nginx, you can use it instead: it needs to serve app.example.com over HTTPS, pass requests for /registry-webhook to 127.0.0.1:9000, and pass everything else to 127.0.0.1:8080.
The server runs whichever image the latest tag points to. When you push a new image to latest:
The registry sends an image.updated event to https://app.example.com/registry-webhook. Caddy passes the request to the receiver. The receiver checks the webhook's secret and, because the event is for latest, starts a deploy. The deploy pulls the new image and recreates the app's container with Docker Compose. To roll back, you point latest at an earlier image, and the server deploys that one the same way.
Before you start You need:
An image of a web app in your registry, such as the demo/hello-upcloud:1.0 image you push in How to get started with UpCloud Container Registry. Docker on your workstation, logged in to the registry as a user with the Read, Create, Update permissions. You need Update to move latest to a different image, as described in Container Registry access control. A Cloud Server with a public IPv4 address. The examples use Ubuntu 26.04 LTS, with the commands run as root. A domain name, such as app.example.com, with an A record that points to the server's public IPv4 address. Caddy uses it to get a TLS certificate for the server. The registry delivers webhooks over the public internet, and Caddy also uses port 80 to get its certificate. If you use the Cloud Server firewall, allow incoming TCP traffic on ports 80 and 443.
Install Docker and Caddy On the server, install Docker Engine, the Docker Compose plugin, and Caddy. On Ubuntu, all three come from the package archive:
apt update apt install -y docker.io docker-compose-v2 caddy If you use another web server instead of Caddy, leave caddy out of the command.
Docker and Caddy start right after the installation and again every time the server boots.
On other Linux distributions, follow Docker's Install Docker Engine and Caddy's Install instructions instead, and make sure both are set to start at boot.
Log in to the registry from the server The server only pulls images, so give it a registry user of its own with just the Read permission. On the registry's Users tab, add a user such as app-server with Read and copy its password, as described in How to get started with UpCloud Container Registry.
Then log in on the server, and enter the password when prompted. Replace my-registry-1b77d021.upcloudcr.com with your registry's endpoint, which is shown on its Overview tab:
docker login my-registry-1b77d021.upcloudcr.com -u app-server Output Login Succeeded Docker warns that it stores the credentials unencrypted in /root/.docker/config.json. That's fine here: only root can read the file, and the user can only pull images.
Tag the image as latest Because the server deploys whatever latest points to, start by adding the latest tag to the image you've already pushed, demo/hello-upcloud:1.0 in the examples. From your workstation, run:
docker buildx imagetools create \ --tag my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:latest \ my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 This tags the image in the registry without pulling or rebuilding it.
Run the app with Docker Compose On the server, create a directory for the app. The examples call the app hello-upcloud, which also names its deploy service later on, so if you use another name, use it in those places too:
mkdir -p /opt/hello-upcloud Then create /opt/hello-upcloud/compose.yaml with the following content. If you're deploying your own app, change image to your image:
services: web: image: my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:latest ports: - "127.0.0.1:8080:80" restart: unless-stopped The ports line makes the app, which listens on port 80 inside its container, available on port 8080 of the server's loopback address (127.0.0.1, also known as localhost). Nothing else needs to reach it there, because Caddy serves the app to the internet. If your app listens on a different port, change 80 to match. restart: unless-stopped makes Docker start the app again whenever the server reboots.
Start the app:
cd /opt/hello-upcloud docker compose up -d Then check that the app answers. The command prints the HTTP status code of the response, and 200 means the app served the page:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080 Output 200 Serve the app over HTTPS If you use another web server, configure it as described in How it works, then continue with Create the deploy service.
Replace the contents of /etc/caddy/Caddyfile with the following, using your own domain name:
app.example.com { handle /registry-webhook { reverse_proxy 127.0.0.1:9000 } handle { reverse_proxy 127.0.0.1:8080 } } Caddy passes requests for /registry-webhook to the webhook receiver, which you set up later in this guide, and everything else to the app. Reload Caddy to apply the change:
systemctl reload caddy You can now open https://app.example.com in a browser to see the app.
Create the deploy service A deploy pulls the image that latest points to, and recreates the app's container if the image has changed. It runs as its own systemd service, so a deploy always runs to the end, even if the receiver restarts while it's running.
Create /etc/systemd/system/hello-upcloud-deploy.service:
[Unit] Description=Deploy hello-upcloud from Container Registry After=docker.service [Service] Type=oneshot WorkingDirectory=/opt/hello-upcloud ExecStart=/usr/bin/docker compose pull ExecStart=/usr/bin/docker compose up -d Load the service and run a deploy by hand to check that the server can pull from the registry:
systemctl daemon-reload systemctl start hello-upcloud-deploy journalctl -u hello-upcloud-deploy -o cat --no-pager Output Starting hello-upcloud-deploy.service - Deploy hello-upcloud from Container Registry... web Pulling web Pulled Container hello-upcloud-web-1 Running hello-upcloud-deploy.service: Deactivated successfully. Finished hello-upcloud-deploy.service - Deploy hello-upcloud from Container Registry. The app is already running the image that latest points to, so the deploy leaves the container running as it is.
Create the webhook receiver The receiver is a short Python program that uses only the standard library. It accepts a request only if it carries the webhook's secret, and starts the deploy service only for an image.updated event for the latest tag of your app's repository.
A single push sends an image.updated event for every manifest it uploads, for example one for each platform of a multi-platform image. Only the manifest pushed with the tag has the tag as its reference, so checking the reference makes the receiver deploy once per push.
Generate a secret and store it in a file that only root can read:
touch /etc/registry-webhook.env chmod 600 /etc/registry-webhook.env echo "WEBHOOK_SECRET=$(openssl rand -hex 32)" &gt; /etc/registry-webhook.env Create a directory for the receiver:
mkdir -p /opt/registry-webhook Then create /opt/registry-webhook/receiver.py:
import hmac import json import os import subprocess from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer SECRET = os.environ["WEBHOOK_SECRET"] REPOSITORY = "demo/hello-upcloud" TAG = "latest" DEPLOY_SERVICE = "hello-upcloud-deploy.service" class WebhookHandler(BaseHTTPRequestHandler): def do_POST(self): token = self.headers.get("Authorization", "") if not hmac.compare_digest(token.encode(), f"Bearer {SECRET}".encode()): self.send_response(401) self.end_headers() return length = int(self.headers.get("Content-Length", 0)) event = json.loads(self.rfile.read(length) or b"{}") if ( self.headers.get("ce-type") == "upcloud.container-registry.image.updated" and event.get("name") == REPOSITORY and event.get("reference") == TAG ): print(f"Deploying {REPOSITORY}:{TAG} {event.get('digest')}", flush=True) subprocess.run(["systemctl", "restart", "--no-block", DEPLOY_SERVICE]) self.send_response(204) self.end_headers() ThreadingHTTPServer(("127.0.0.1", 9000), WebhookHandler).serve_forever() The receiver listens on port 9000 of the loopback address, where Caddy sends the webhook requests. Requests without the right secret get 401 Unauthorized, and every other request gets 204 No Content. Because the receiver restarts the deploy service rather than just starting it, a push that arrives during a deploy starts the deploy over, so the server always ends up on the newest image.
If you're deploying your own app, set REPOSITORY to your image's repository, without the registry endpoint, and DEPLOY_SERVICE to your app's deploy service. To deploy a tag other than latest, change TAG.
Create /etc/systemd/system/registry-webhook.service:
[Unit] Description=Container Registry webhook receiver After=network-online.target docker.service Wants=network-online.target [Service] EnvironmentFile=/etc/registry-webhook.env ExecStart=/usr/bin/python3 /opt/registry-webhook/receiver.py Restart=on-failure [Install] WantedBy=multi-user.target Start the receiver, and have it start every time the server boots:
systemctl daemon-reload systemctl enable --now registry-webhook Check that Caddy passes webhook requests to the receiver by sending one without the secret, which the receiver refuses:
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://app.example.com/registry-webhook Output 401 Add the webhook On the server, show the secret:
cut -d= -f2 /etc/registry-webhook.env In the Control Panel, open the registry's Webhooks tab and select Add webhook.
Enter a Name, such as app-server, and the URL https://app.example.com/registry-webhook.
Paste the secret into the Secret field, and select Add webhook.
Push a new version On your workstation, build the new version of your image and push it with both a new version tag and the latest tag. If you're using the example image, first change the heading in index.html, for example to Hello again from UpCloud Container Registry, and then run the following from the directory with the Dockerfile:
docker buildx build --platform linux/amd64,linux/arm64 \ -t my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.1 \ -t my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:latest --push . Tagging each build with a version as well as latest keeps every version in the registry, so you can roll back to it later. If you build images in CI, for example with GitHub Actions as shown in How to get started with UpCloud Container Registry, add the latest tag to the pipeline's tags and give its registry user the Update permission.
To follow the deploy as it happens, watch the logs on the server:
journalctl -u registry-webhook -u hello-upcloud-deploy -f First the receiver logs a Deploying line with the digest of the new image, and then the deploy service recreates the app's container with it. Once the deploy finishes, https://app.example.com serves the new version, which for the example image means the new heading.
Roll back To go back to an earlier version, point latest at it. For example, to return to 1.0:
docker buildx imagetools create \ --tag my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:latest \ my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 The registry sends an image.updated event for latest, just as it does for a push, and the server deploys 1.0.
Troubleshooting Nothing happens after a push. Check the receiver's log with journalctl -u registry-webhook:
If there are no POST /registry-webhook lines, the registry can't reach the server. Check that the domain points to the server, that ports 80 and 443 are open, and that https://app.example.com loads. If the lines end in 401, the webhook's secret doesn't match /etc/registry-webhook.env. Delete the webhook and add it again with the value from the file. If the lines end in 204 but there's no Deploying line, the push wasn't to the repository and tag the receiver deploys. Check the repository and tag you pushed, and REPOSITORY and TAG in the receiver. The deploy fails with pull access denied. If the error ends in no basic auth credentials, the server isn't logged in to the registry, so run docker login on the server again.
A push happened while the server was down. The registry delivers each event once and doesn't send it again, so a push that happens while the server is down or restarting doesn't reach it. To catch up, run a deploy by hand:
systemctl start hello-upcloud-deploy</description></item><item><title>How to get started with UpCloud Container Registry</title><link>https://upcloud.com/docs/guides/get-started-upcloud-container-registry/</link><pubDate>Mon, 09 Nov 2026 10:08:54 +0000</pubDate><dc:creator>UpCloud</dc:creator><guid>https://upcloud.com/docs/guides/get-started-upcloud-container-registry/</guid><hub:summary/><hub:linkHref/><hub:linkTitle/><description>
Beta notice UpCloud Managed Container Registry is in beta. Features shown in this guide may change before general availability.
What a container registry is A container registry stores container images and serves them to the machines that run them. You build an image once, push it to the registry, and every server, Kubernetes node, or CI job that needs it pulls it from there.
UpCloud Managed Container Registry is a private, OCI-compliant registry maintained by UpCloud. It stores container images and other OCI artifacts, such as Helm charts, and works with the Docker CLI and any other tool that speaks the OCI distribution protocol.
Before you start You need:
An UpCloud account. Docker or another OCI-compatible client on the machine you push from. An image to push. If you don't have one, this guide builds a small example. Create a registry In the Control Panel, open Container Registry in the left-hand menu and select Create your first registry, or New Container Registry if you already have one. Fill in the sections below and select Create registry at the bottom of the page. Region Choose the region where your images are stored. Pick the one closest to the servers and clusters that pull from the registry.
More regions are being added during the beta. Container Registry availability lists the current ones and their locations.
Plan Choose Starter or Pro. Pro has more storage, repositories, webhooks, and IP allowlist entries, a higher request rate, and is the only plan that can connect to SDN Private Networks. Storage beyond the included amount is billed per GiB.
You can change the plan later from the registry's Settings tab. See Plans.
Private networks On the Pro plan, you can attach SDN Private Networks here or later from the registry's Networks tab, as described in Private networks.
Name Give the registry a Name. The name becomes part of the registry's hostname.
Wait until the registry's status changes to Running before you continue.
Registry endpoint Open the registry to see its Overview tab. The Registry endpoint is the hostname you use in every Docker command. It's made of the registry name, the first part of its UUID, and upcloudcr.com:
my-registry-1b77d021.upcloudcr.com Right after the registry is created, the endpoint may not resolve in DNS yet.
Add a registry user Registry users are separate from your UpCloud account users. Create a separate user for each person or system that needs access, so you can remove one without affecting the others.
Open the Users tab and select Add user.
Enter a Username.
The Initial password field is pre-filled with a strong random password. Keep it, or select Regenerate for a new one.
Choose the Permissions. Every user can pull, and the other permissions add to that:
Permissions Use for Read Servers and Kubernetes clusters that run images. Read, Create CI pipelines that push a new tag for every build. Read, Create, Update Engineers, and pipelines that also move tags such as latest. The combinations with Delete also let the user delete tags and images. See Container Registry access control for what each permission allows.
Select Add user.
A User credentials dialog then shows the username, the password, and a ready-to-use docker login command. The password is shown only once, so copy it into your password manager or secret store before you close the dialog. If a password is ever lost, select View next to the user and then Rotate password to get a new one. The old password then stops working, so update any client that uses it.
Each registry also has a built-in upadmin user, marked Primary, which is created with the registry and can't be deleted.
Log in to the registry On the machine you push from, log in with the registry endpoint and the user you created:
docker login my-registry-1b77d021.upcloudcr.com -u app-developer Enter the password when prompted. Docker stores the credentials, so you only need to log in once per machine:
Output Login Succeeded To log in from a script or CI job, pass the password on standard input instead:
echo "$REGISTRY_PASSWORD" | docker login my-registry-1b77d021.upcloudcr.com -u app-developer --password-stdin Push an image To push an image, name it with the registry endpoint, a repository name, and a tag. Repositories are created automatically on the first push.
This example builds a small web server image that serves a single page. Create a directory with two files.
index.html:
&lt;!doctype html&gt; &lt;html&gt; &lt;head&gt;&lt;title&gt;Hello from UpCloud&lt;/title&gt;&lt;/head&gt; &lt;body&gt; &lt;h1&gt;Hello from UpCloud Container Registry&lt;/h1&gt; &lt;p&gt;This page is served by an image pulled from a private registry.&lt;/p&gt; &lt;/body&gt; &lt;/html&gt; Dockerfile:
FROM nginx:1.27-alpine COPY index.html /usr/share/nginx/html/index.html Build the image and push it to your registry:
docker buildx build --platform linux/amd64,linux/arm64 \ -t my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 --push . --platform builds the image for both linux/amd64, which Cloud Servers and Managed Kubernetes nodes run, and linux/arm64, which Apple silicon Macs run. The same tag then works on all of them.
Near the end of the output, a line shows the digest of the pushed image:
Output =&gt; =&gt; pushing manifest for my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0@sha256:e1fc09333be0efed4bfccd72eddb33435b5042ec773839b61d312a169aa3502a The image now appears on the registry's Repositories tab. Select the repository to see the tag and the start of the same digest:
The part of the name between the endpoint and the tag, demo/hello-upcloud, is the repository. You can use a single name such as hello-upcloud or group repositories under a prefix such as demo/.
Pull and run an image On any machine that is logged in to the registry with a user that can pull, pull the image and run it:
docker pull my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 docker run -d --name hello-upcloud -p 8080:80 my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 curl -s http://localhost:8080 | grep h1 Output &lt;h1&gt;Hello from UpCloud Container Registry&lt;/h1&gt; Deploy to Managed Kubernetes Kubernetes pulls images from a private registry with an image pull secret that holds a registry user's credentials. Use a user with only the Read permission, so the cluster can pull images but never change them.
On the Users tab, add a user with the Read permission, for example k8s-puller, and copy its password.
Create an image pull secret in the namespace you deploy to:
kubectl create secret docker-registry upcloud-registry \ --docker-server=my-registry-1b77d021.upcloudcr.com \ --docker-username=k8s-puller \ --docker-password='&lt;password&gt;' Output secret/upcloud-registry created Keep the single quotes around the password, so that characters such as $ are passed through unchanged.
Reference the secret under imagePullSecrets in the pod spec of your workloads.
For a complete deployment, pulls over a private network, and installing your app from a Helm chart stored in the registry, see How to use Container Registry with Managed Kubernetes.
Build and push from GitHub Actions A CI pipeline can build your image and push it to the registry on every commit. This example uses GitHub Actions. Other CI systems run the same docker login and docker buildx build --push commands.
On the Users tab, add a user for the pipeline with the Read, Create permissions, for example github-actions, and copy its password. The workflow below tags every build with the commit ID, so it only ever creates new tags. If your workflow also pushes a tag that already exists, such as latest, choose Read, Create, Update instead.
In your GitHub repository, open Settings → Secrets and variables → Actions and add two repository secrets: REGISTRY_USERNAME with the username and REGISTRY_PASSWORD with the password.
Add the Dockerfile and index.html from Push an image to the repository, and save the following workflow as .github/workflows/build.yml:
name: Build and push on: push: branches: [main] workflow_dispatch: env: REGISTRY: my-registry-1b77d021.upcloudcr.com IMAGE: demo/hello-upcloud jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: docker/setup-qemu-action@v4 - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} Commit and push to the main branch. The workflow runs on the Actions tab of the repository.
When the run finishes, the new tag appears on the registry's Repositories tab.
Manage repositories and tags The Repositories tab lists every repository in the registry. Select a repository to see its tags, with each tag's digest, size, and last update time. Expand a tag to see the platforms it was built for, such as linux/amd64 and linux/arm64.
From the repository view, you can:
Add a new tag to an existing image without pushing it again. Select Promote to… on a tag's row, or Tag a digest at the top, enter the Target tag, and select Tag this digest. For example, after testing 1.0, tag the same digest as stable. The target tag must not exist yet. To move an existing tag to a different image, delete the tag first. Remove a single tag with Delete tag. The image stays available through its other tags. Remove the image and every tag that points to it with Delete image (all tags), the ⋮ button. Remove the repository and all of its tags with Delete repository. Type the repository name to confirm. Deleting a tag, image, or repository can't be undone. Pulls of a deleted tag return not found, and the storage is reclaimed on the next garbage collection run.
Control access IP allowlist By default, the registry accepts connections from any IP address, and every request still needs a valid registry user. To accept connections only from your own networks, open the IP Allowlist tab, select Add CIDR, and enter an address range such as 192.0.2.0/24 with an optional description.
Requests from addresses outside the list are refused with 403 Forbidden, even with valid credentials, so add the addresses of every workstation, server, cluster, and CI runner that uses the registry.
Allow anonymous pulls To publish images that anyone can pull without logging in, such as open-source releases, turn on Allow anonymous pulls on the Settings tab and confirm.
This makes every repository in the registry publicly pullable, and anyone can also list its repositories and their tags. Pushing still requires a registry user. If only some of your images should be public, create a separate registry for them.
Private networks Attach an SDN Private Network so that servers and clusters in that network reach the registry without going over the public internet.
Pro plan only Private networks are available on the Pro plan. To use them with a Starter registry, change its plan on the Settings tab first.
The network must be in one of the locations listed for the registry's region and have an SDN Router attached. See How to connect private networks using SDN Router.
Open the Networks tab and select Attach private network.
Pick the network and select Attach.
The network shows as Provisioning until it becomes Active. Meanwhile, the registry sets up its private endpoint, and its status at the top of the page shows Setting up private access. Wait until the status is back to Running before you use the private endpoint. To remove the network, select the detach button at the end of its row.
The registry then has a second endpoint, shown as Private endpoint on the Overview tab, with -private added to its name, such as my-registry-1b77d021-private.upcloudcr.com. Use it in image references on servers and clusters in that network. The public endpoint keeps working alongside the private one.
Credentials are matched by hostname, so clients log in to the private endpoint separately from the public one. To pull through the private endpoint from Managed Kubernetes, see How to use Container Registry with Managed Kubernetes.
To make the registry reachable only from attached private networks, turn off Public connection on the Overview tab. Once the change is applied, the public endpoint answers every request with 404 Not Found. Turn it back on the same way. The private connection can't be turned off while the public connection is off.
For more about the public and private endpoints, see Container Registry networking.
Webhooks To send registry events to an HTTP endpoint you control, for example to start a deployment when an image is pushed, add a webhook from the Webhooks tab. See Container Registry webhooks for the events and the delivery format, and How to deploy automatically with Container Registry webhooks for a server that redeploys its app on every push.
Metrics The Metrics tab shows pulls, pushes, storage, and network traffic over a selected time range of up to 90 days.
Change plan Open the Settings tab, select the other plan under Plan, and select Save. The Storage section on the same tab shows the included storage, the storage used, and any additional storage you are billed for.
To change from Pro to Starter, first detach all private networks on the Networks tab.
Delete a registry Deleting a registry permanently deletes all of its images, users, and credentials, and its endpoint stops serving. Update any servers, clusters, or pipelines that pull from it first.
Open the registry and select the delete button next to Restart at the top of the page. Type the registry name to confirm and select Delete forever. The registry shows as Deleting in the list until it's removed.
Troubleshooting docker login fails with no such host. The registry endpoint isn't in DNS yet. This happens right after you create the registry, and with the private endpoint while the registry's status shows Setting up private access. Wait until the status is Running and the name resolves, for example until nslookup returns an address for it, and try again.
unauthorized: authentication required or 401 Unauthorized. The client isn't logged in, or the username or password is wrong. Run docker login with the registry endpoint again.
403 Forbidden with RBAC: access denied. Your IP address isn't in the registry's IP allowlist. Add it on the IP Allowlist tab.
error from registry: too many requests when pushing a new repository. The registry has reached the repository limit of its plan. Delete repositories you no longer need, or change to the Pro plan. Pushing new tags to existing repositories keeps working.
docker push fails with 500 Internal Server Error. Run the same docker push command again. Layers that were already uploaded show as Already exists, and the push completes.
Push fails with denied: requested access to the resource is denied. The registry user's permissions don't allow the push. Pushing a new tag or repository needs Create, and pushing a different image to a tag that already exists needs Update. Add a user with the permissions it needs.
Kubernetes pods stay in ImagePullBackOff with no basic auth credentials. The pod has no image pull secret for the registry. Create a docker-registry secret in the pod's namespace and reference it under imagePullSecrets.
Kubernetes pods stay in ImagePullBackOff with not found. The tag in the image reference doesn't exist in the registry. Check the tag on the Repositories tab, or push it.
Every request to the public endpoint returns 404 Not Found. Public connection is turned off on the registry's Overview tab. Turn it on, or use the private endpoint from an attached network.
exec format error when a container starts. The image was built for a different CPU architecture than the machine running it. Rebuild for linux/amd64, or build a multi-platform image with docker buildx.</description></item><item><title>How to store OpenTofu modules and other files in Container Registry</title><link>https://upcloud.com/docs/guides/store-opentofu-modules-container-registry/</link><pubDate>Mon, 09 Nov 2026 10:08:54 +0000</pubDate><dc:creator>UpCloud</dc:creator><guid>https://upcloud.com/docs/guides/store-opentofu-modules-container-registry/</guid><hub:summary/><hub:linkHref/><hub:linkTitle/><description>
Beta notice UpCloud Managed Container Registry is in beta. Features shown in this guide may change before general availability.
UpCloud Managed Container Registry can store more than container images. Any file can go into it as an OCI artifact, so configuration bundles, release files, and infrastructure code can sit next to your images, behind the same registry users and permissions. OpenTofu can also install modules straight from the registry, which makes it a private module registry for your team.
This guide walks you through storing a file with the ORAS command-line tool, then publishing an OpenTofu module and using it in a configuration. It ends with releasing a new version of the module and using the module with Terraform.
Before you start You need:
A registry and a registry user with the Create permission, such as the app-developer user from How to get started with UpCloud Container Registry. The ORAS CLI on your workstation. ORAS is an open-source tool for pushing files to OCI registries and pulling them back. For the module part, OpenTofu 1.10 or later, and an UpCloud API token. If you don't have a token yet, see How to create and use UpCloud API Tokens. The zip and unzip commands, which some Linux distributions don't install by default. On Ubuntu, install them with sudo apt install zip unzip. The examples use the registry endpoint my-registry-1b77d021.upcloudcr.com. Replace it with your own, which is shown on the registry's Overview tab.
Log in with ORAS ORAS logs in to the registry the same way Docker does. Log in as your registry user, and enter its password when prompted:
oras login my-registry-1b77d021.upcloudcr.com -u app-developer Output Password: Login Succeeded ORAS saves the login in ~/.docker/config.json, the file Docker uses, whether or not Docker is installed. With Docker Desktop, the login goes to Docker Desktop's credential store instead. OpenTofu reads the login from the same place, so you only need to log in once.
Store a file You can store any file in the registry, for example a configuration bundle that your servers download when they start. Create an example file called app-config.yaml:
# Settings for the web app log_level: info feature_flags: new_checkout: true Like an image, the file goes into a repository with a tag. The following command pushes it to the files/app-config repository with the tag 2026-11. The part after the file name, application/yaml, is the file's media type, which tells other tools what kind of file it is:
oras push my-registry-1b77d021.upcloudcr.com/files/app-config:2026-11 app-config.yaml:application/yaml Output ... Pushed [registry] my-registry-1b77d021.upcloudcr.com/files/app-config:2026-11 ArtifactType: application/vnd.unknown.artifact.v1 Digest: sha256:c606f03a901a694b5ca3552fd41651b0cef34071da83f7d524e3b85d298a0154 The repository now shows on the registry's Repositories tab with the type Artifact. You manage its tags there the same way as an image's, as described in How to get started with UpCloud Container Registry.
To download the file, for example on a server that uses it, pull it into a directory:
oras pull my-registry-1b77d021.upcloudcr.com/files/app-config:2026-11 -o config Output ... Pulled [registry] my-registry-1b77d021.upcloudcr.com/files/app-config:2026-11 Digest: sha256:c606f03a901a694b5ca3552fd41651b0cef34071da83f7d524e3b85d298a0154 ORAS saves the file under its original name, so it's now in config/app-config.yaml.
Publish an OpenTofu module For OpenTofu to install a module from the registry, the module must be a zip file of its directory, pushed with a type that marks it as a module package. As an example, create a module that sets up an SDN Private Network with an SDN Router, in a directory called modules/network.
modules/network/main.tf creates the router and the network:
terraform { required_providers { upcloud = { source = "UpCloudLtd/upcloud" } } } resource "upcloud_router" "this" { name = "${var.name}-router" } resource "upcloud_network" "this" { name = "${var.name}-network" zone = var.zone router = upcloud_router.this.id ip_network { address = var.cidr dhcp = true family = "IPv4" } } modules/network/variables.tf defines the module's inputs:
variable "name" { description = "Prefix for the network and router names" type = string } variable "zone" { description = "Location of the network" type = string default = "fi-hel2" } variable "cidr" { description = "Address range of the network" type = string default = "10.10.0.0/24" } modules/network/outputs.tf returns the network's UUID:
output "network_id" { value = upcloud_network.this.id } OpenTofu reads the module from the top level of the zip file, so zip the files from inside the module's directory:
(cd modules/network &amp;&amp; zip -r ../../network.zip .) Then push the zip file as version 1.0.0 of the modules/network repository. Use the artifact type application/vnd.opentofu.modulepkg and the media type archive/zip exactly as shown, because OpenTofu checks them before it installs the module:
oras push my-registry-1b77d021.upcloudcr.com/modules/network:1.0.0 \ --artifact-type application/vnd.opentofu.modulepkg \ network.zip:archive/zip Output ... Pushed [registry] my-registry-1b77d021.upcloudcr.com/modules/network:1.0.0 ArtifactType: application/vnd.opentofu.modulepkg Digest: sha256:40d7d5a34aac41e697a03d1ed9ed1f21733bbb07d8cbcef1badd3be84c5c6037 Use the module in a configuration To use the module, create an OpenTofu configuration in a new directory. provider.tf sets up the UpCloud provider:
terraform { required_providers { upcloud = { source = "UpCloudLtd/upcloud" version = "~&gt; 5.49" } } } provider "upcloud" {} The UpCloud provider reads your API token from the UPCLOUD_TOKEN environment variable, so set it in the terminal you run OpenTofu from:
export UPCLOUD_TOKEN=ucat_Your_API_Token main.tf uses the module from the registry. The oci:// address points to the module's repository, and ?tag=1.0.0 picks the version:
module "network" { source = "oci://my-registry-1b77d021.upcloudcr.com/modules/network?tag=1.0.0" name = "prod" } output "network_id" { value = module.network.network_id } For an oci:// source, the tag is the only way to choose a version, so leave out the module block's version argument.
Run tofu init to download the module and the provider. OpenTofu logs in to the registry with the credentials that ORAS saved:
tofu init Output Initializing the backend... Initializing modules... Downloading oci://my-registry-1b77d021.upcloudcr.com/modules/network?tag=1.0.0 for network... - network in .terraform/modules/network Initializing provider plugins... - Finding upcloudltd/upcloud versions matching "~&gt; 5.49"... - Installing upcloudltd/upcloud v5.49.0... ... OpenTofu has been successfully initialized! ... From here on, the module works like any other. tofu plan shows the router and the network it creates, and tofu apply creates them:
tofu plan Output ... # module.network.upcloud_network.this will be created ... # module.network.upcloud_router.this will be created ... Plan: 2 to add, 0 to change, 0 to destroy. ... The example creates a real network and router, so when you no longer need them, remove them with tofu destroy.
Release a new version When you change the module, push it again with a new tag, so that configurations move to the new version only when you choose. With ORAS, pushing to a tag that already exists needs the Update permission, even if the file hasn't changed.
For example, to release version 1.1.0 with an output for the router's UUID, add the output to modules/network/outputs.tf. Then zip the module again and push it, deleting the old zip file first so that the new one only contains the module's current files:
rm network.zip (cd modules/network &amp;&amp; zip -r ../../network.zip .) oras push my-registry-1b77d021.upcloudcr.com/modules/network:1.1.0 \ --artifact-type application/vnd.opentofu.modulepkg \ network.zip:archive/zip Output ... Pushed [registry] my-registry-1b77d021.upcloudcr.com/modules/network:1.1.0 ArtifactType: application/vnd.opentofu.modulepkg Digest: sha256:d4ff296a5437cb5bd836ae0c4d0663867a743ccf8f00fe80edde0927e8d3e8d1 Configurations keep using 1.0.0 until you change the tag in their source. Change it to ?tag=1.1.0, and run tofu init again to download the new version:
Output ... Initializing modules... Downloading oci://my-registry-1b77d021.upcloudcr.com/modules/network?tag=1.1.0 for network... - network in .terraform/modules/network ... A registry user with the Update permission can push a different module to an existing tag. To make sure a configuration always gets the same module, use the digest that oras push printed instead of the tag:
source = "oci://my-registry-1b77d021.upcloudcr.com/modules/network?digest=sha256:d4ff296a5437cb5bd836ae0c4d0663867a743ccf8f00fe80edde0927e8d3e8d1" A source address can have a tag or a digest, but not both.
Use the module with Terraform Terraform can't install modules from an OCI registry, so with an oci:// source, terraform init stops with download not supported for scheme 'oci'. To use a module from your registry with Terraform, download it with ORAS and point Terraform to the local copy instead.
In your Terraform configuration's directory, pull the module into modules/network, then unzip it:
oras pull my-registry-1b77d021.upcloudcr.com/modules/network:1.1.0 -o modules/network unzip modules/network/network.zip -d modules/network rm modules/network/network.zip Then use the directory as the module's source:
module "network" { source = "./modules/network" name = "prod" } terraform init now installs the module from the directory. To move to a new version, delete the modules/network directory, then pull and unzip the new version.
Troubleshooting tofu init fails with basic credential not found. OpenTofu found no login for the registry. Log in with oras login as the user that runs OpenTofu, as described in Log in with ORAS, and run tofu init again.
tofu init fails with unexpected artifact type "application/vnd.unknown.artifact.v1". The module was pushed without --artifact-type application/vnd.opentofu.modulepkg. Push it again with the artifact type, as described in Publish an OpenTofu module.
tofu init fails with image manifest contains no layers of types supported as module packages by OpenTofu. The zip file was pushed with a media type other than archive/zip, such as application/zip. Push it again with network.zip:archive/zip, as described in Publish an OpenTofu module.
terraform init fails with download not supported for scheme 'oci'. Terraform can't install modules from an OCI registry. Download the module with ORAS instead, as described in Use the module with Terraform.</description></item><item><title>How to use Container Registry with Managed Kubernetes</title><link>https://upcloud.com/docs/guides/use-container-registry-managed-kubernetes/</link><pubDate>Mon, 09 Nov 2026 10:08:54 +0000</pubDate><dc:creator>UpCloud</dc:creator><guid>https://upcloud.com/docs/guides/use-container-registry-managed-kubernetes/</guid><hub:summary/><hub:linkHref/><hub:linkTitle/><description>
Beta notice UpCloud Managed Container Registry is in beta. Features shown in this guide may change before general availability.
UpCloud Managed Kubernetes clusters can run images straight from your UpCloud Managed Container Registry. The cluster logs in to the registry as a registry user, whose credentials you store in the cluster as an image pull secret, so your images can stay private.
This guide walks you through running an image from your registry on a cluster, moving the image pulls to a private network, and then packaging your app as a Helm chart that you store in the registry next to its images. The examples use the demo/hello-upcloud image, a small web page served on port 80, but you can follow along with your own image.
Before you start You need:
An image in your registry, such as the demo/hello-upcloud:1.0 image you push in How to get started with UpCloud Container Registry. A Managed Kubernetes cluster and kubectl set up to use it, as described in How to get started with Managed Kubernetes. For the Helm part, Helm 3.8 or later on your workstation. For the private network part, a registry on the Pro plan in a region that includes the cluster's location. The commands work in the default namespace. To use another one, add -n &lt;namespace&gt; to the kubectl and helm commands.
Create an image pull secret The cluster only pulls images, so give it a registry user of its own with just the Read permission. On the registry's Users tab, add a user such as k8s-puller with Read and copy its password, as described in How to get started with UpCloud Container Registry.
Then store the user's credentials in the cluster as an image pull secret. Replace my-registry-1b77d021.upcloudcr.com with your registry's endpoint, which is shown on its Overview tab:
kubectl create secret docker-registry upcloud-registry \ --docker-server=my-registry-1b77d021.upcloudcr.com \ --docker-username=k8s-puller \ --docker-password='&lt;password&gt;' Output secret/upcloud-registry created Keep the single quotes around the password, so your shell passes characters such as $ through unchanged.
A secret belongs to a single namespace, and pods can only use secrets from their own, so create the secret in every namespace that runs images from the registry.
Deploy an image To use the secret, name it under imagePullSecrets in your workload, and the cluster logs in with it whenever it pulls the image. Save the following Deployment as hello-upcloud.yaml. If you're deploying your own app, change image to your image and containerPort to the port your app listens on:
apiVersion: apps/v1 kind: Deployment metadata: name: hello-upcloud spec: replicas: 1 selector: matchLabels: app: hello-upcloud template: metadata: labels: app: hello-upcloud spec: imagePullSecrets: - name: upcloud-registry containers: - name: web image: my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 ports: - containerPort: 80 The examples call the app hello-upcloud throughout. If you use another name, use it in the later commands too.
Apply the file and wait for the rollout:
kubectl apply -f hello-upcloud.yaml kubectl rollout status deploy/hello-upcloud Output deployment.apps/hello-upcloud created deployment "hello-upcloud" successfully rolled out Then check that the Deployment is ready and running the image from your registry:
kubectl get deploy hello-upcloud -o wide Output NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR hello-upcloud 1/1 1 1 2s web my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 app=hello-upcloud Use the secret for every pod in a namespace Instead of naming the secret in each workload, you can add it to the namespace's default service account. Every pod that runs as that service account then gets the secret automatically:
kubectl patch serviceaccount default \ -p '{"imagePullSecrets": [{"name": "upcloud-registry"}]}' Output serviceaccount/default patched Pods that don't set a service account run as default. Only pods created after the change get the secret.
Pull over a private network On the Pro plan, the cluster can pull images through the registry's private endpoint instead of the public one. The pulls then stay on UpCloud's private network, and since the IP allowlist only applies to the public endpoint, you don't need to add your nodes' addresses to it.
Attach the cluster's network to the registry, as described in How to get started with UpCloud Container Registry. A cluster's network always has an SDN Router, so you can attach it without any changes.
Wait until the network shows as Active and the registry's status is back to Running. The registry's Overview tab then lists its private endpoint, such as my-registry-1b77d021-private.upcloudcr.com.
Nodes created after the attach reach the private endpoint straight away, but nodes that were already running only start using it when they next renew their network settings. Until then, pulls through the private endpoint fail on those nodes with i/o timeout and Kubernetes retries them, so a rollout can take longer to finish.
Credentials are matched by hostname, so create a second image pull secret for the private hostname:
kubectl create secret docker-registry upcloud-registry-private \ --docker-server=my-registry-1b77d021-private.upcloudcr.com \ --docker-username=k8s-puller \ --docker-password='&lt;password&gt;' Output secret/upcloud-registry-private created In hello-upcloud.yaml, switch the image reference to the private hostname and the pull secret to the new one:
imagePullSecrets: - name: upcloud-registry-private containers: - name: web image: my-registry-1b77d021-private.upcloudcr.com/demo/hello-upcloud:1.0 Apply the change and wait for the rollout:
kubectl apply -f hello-upcloud.yaml kubectl rollout status deploy/hello-upcloud Output deployment.apps/hello-upcloud configured deployment "hello-upcloud" successfully rolled out Store and install the app as a Helm chart Helm installs an app on Kubernetes from a chart: a package of Kubernetes manifests with settings, such as the image tag, that are filled in when you install it. The registry stores charts next to your images, with the same users and permissions.
Helm runs on your workstation: it downloads the chart from the registry with its own login, fills in the settings, and sends the result to the cluster. The cluster never sees the chart itself, only the images it names, which it pulls with the image pull secret as before.
Create the chart Create a directory for the chart, named after your app, such as hello-upcloud, and add the following three files to it.
hello-upcloud/Chart.yaml gives the chart its name and versions: version is the version of the chart itself, and appVersion is the image tag it deploys:
apiVersion: v2 name: hello-upcloud description: A web page served from UpCloud Container Registry version: 0.1.0 appVersion: "1.0" hello-upcloud/values.yaml holds the settings you can change at install time, such as which image to run. repository is your image without its tag, because the chart adds the tag for you. If you pull over the private network, point repository at the private hostname and use the upcloud-registry-private secret instead:
replicaCount: 1 image: repository: my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud tag: "" imagePullSecrets: - name: upcloud-registry hello-upcloud/templates/deployment.yaml is the Deployment from Deploy an image, with the settings filled in from values.yaml. Because image.tag is empty, the chart uses appVersion as the tag, so each new chart version can move the app to a new image. As before, set containerPort to the port your app listens on:
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: app: {{ .Release.Name }} template: metadata: labels: app: {{ .Release.Name }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} containers: - name: web image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" ports: - containerPort: 80 Push the chart to the registry Helm pushes charts as archives, so first package the chart:
helm package hello-upcloud Output Successfully packaged chart and saved it to: /home/user/hello-upcloud-0.1.0.tgz Then log in to the registry with Helm as a user that can push, such as the app-developer user from How to get started with UpCloud Container Registry, and enter the password when prompted. Pushing a new chart, or a new version of one, needs the Create permission.
helm registry login my-registry-1b77d021.upcloudcr.com -u app-developer Output Login Succeeded Push the chart under a prefix such as charts, so your charts are easy to tell apart from your images:
helm push hello-upcloud-0.1.0.tgz oci://my-registry-1b77d021.upcloudcr.com/charts Output Pushed: my-registry-1b77d021.upcloudcr.com/charts/hello-upcloud:0.1.0 Digest: sha256:65f7ced91761c148197f4617eb9d6eeb3398874306af4da03145d45b1f6512c6 The chart now appears on the registry's Repositories tab as charts/hello-upcloud, with the chart version as its tag.
Install the chart The chart creates a Deployment called hello-upcloud, so first delete the one you created with kubectl:
kubectl delete deployment hello-upcloud Then install the chart straight from the registry:
helm install hello-upcloud oci://my-registry-1b77d021.upcloudcr.com/charts/hello-upcloud --version 0.1.0 Output Pulled: my-registry-1b77d021.upcloudcr.com/charts/hello-upcloud:0.1.0 Digest: sha256:65f7ced91761c148197f4617eb9d6eeb3398874306af4da03145d45b1f6512c6 NAME: hello-upcloud LAST DEPLOYED: Thu Oct 22 07:34:25 2026 NAMESPACE: default STATUS: deployed REVISION: 1 DESCRIPTION: Install complete TEST SUITE: None Wait for the rollout and check the image, as before:
kubectl rollout status deploy/hello-upcloud kubectl get deploy hello-upcloud -o wide Output deployment "hello-upcloud" successfully rolled out NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR hello-upcloud 1/1 1 1 1s web my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 app=hello-upcloud Release a new version Releasing a new version of your app takes two pushes: the new image, with a new tag, and a new chart version that points to it.
Start with the image. If you're using the example image, change the heading in index.html, for example to Hello again from UpCloud Container Registry, and then build and push it as 1.1 from the directory with the Dockerfile:
docker buildx build --platform linux/amd64,linux/arm64 \ -t my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.1 --push . Then, in hello-upcloud/Chart.yaml, raise the chart version and set appVersion to the new image tag:
version: 0.2.0 appVersion: "1.1" Give every change to the chart a new version, even a small one. Each chart version is a tag in the registry, so pushing a version that's already there needs the Update permission and changes what that version installs.
Package and push the new version, and upgrade the installed release to it:
helm package hello-upcloud helm push hello-upcloud-0.2.0.tgz oci://my-registry-1b77d021.upcloudcr.com/charts helm upgrade hello-upcloud oci://my-registry-1b77d021.upcloudcr.com/charts/hello-upcloud --version 0.2.0 The last lines of the output show the new revision:
Output Release "hello-upcloud" has been upgraded. Happy Helming! NAME: hello-upcloud LAST DEPLOYED: Thu Oct 22 07:34:55 2026 NAMESPACE: default STATUS: deployed REVISION: 2 DESCRIPTION: Upgrade complete TEST SUITE: None The registry's Repositories tab now lists both chart versions under charts/hello-upcloud:
Once the rollout finishes, the Deployment is running the new image:
kubectl rollout status deploy/hello-upcloud kubectl get deploy hello-upcloud -o wide Output deployment "hello-upcloud" successfully rolled out NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR hello-upcloud 1/1 1 1 66s web my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.1 app=hello-upcloud Roll back Helm keeps a numbered revision for every install, upgrade, and rollback:
helm history hello-upcloud Output REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION 1 Thu Oct 22 07:34:25 2026 superseded hello-upcloud-0.1.0 1.0 Install complete 2 Thu Oct 22 07:34:55 2026 deployed hello-upcloud-0.2.0 1.1 Upgrade complete To go back to the previous version, roll back to its revision:
helm rollback hello-upcloud 1 Output Rollback was a success! Happy Helming! Helm records the rollback as revision 3, and once the rollout finishes, the Deployment is back on the 1.0 image:
kubectl rollout status deploy/hello-upcloud kubectl get deploy hello-upcloud -o wide Output deployment "hello-upcloud" successfully rolled out NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR hello-upcloud 1/1 1 1 72s web my-registry-1b77d021.upcloudcr.com/demo/hello-upcloud:1.0 app=hello-upcloud Troubleshooting Pods stay in ImagePullBackOff. Run kubectl describe pod &lt;pod&gt; and look for the Failed to pull image event at the end of the output:
no basic auth credentials: the pod has no image pull secret for the registry. Create the secret in the pod's namespace and name it under imagePullSecrets, or add it to the namespace's default service account. If the image reference uses the private endpoint, the secret must use the private hostname too. 401 Unauthorized: the username or password in the secret is wrong. Delete the secret with kubectl delete secret upcloud-registry and create it again. not found: the tag in the image reference doesn't exist in the registry. Check the tag on the Repositories tab, or push it. 403 Forbidden: the registry's IP allowlist doesn't include the addresses the nodes connect from. Pull through the private endpoint, or add those addresses to the allowlist. For nodes with public IP addresses, kubectl get nodes -o wide shows them under EXTERNAL-IP. i/o timeout with the private hostname: the node can't reach the private endpoint. Check that the cluster's network is attached to the registry and Active on the Networks tab. A node that was running before you attached the network starts using the private endpoint when it next renews its network settings, and the pull then succeeds on its own. Helm fails with basic credential not found. Helm isn't logged in to the registry on this machine. Log in with helm registry login and a registry user, as in Push the chart to the registry.
helm push fails with 403: denied. The registry user's permissions don't allow the push. A new chart or chart version needs Create, and a version that's already in the registry needs Update, so give the chart a new version instead.</description></item></channel></rss>