If you're getting started with Kamal, you saw that a container registry is necessary to store your Docker images. If your code is public, that's easy enough to find, but if it's private, you might not want to get a subscription only for a Docker registry. Or you might enjoy the idea of handling everything from end-to-end... in any case, here's how to do it.
Going with Cloudflare R2 and Workers
We'll be using Cloudflare's R2. It is their response to Amazon's S3. It offers free egress bandwidth and a decent free plan with 5GB of storage (over this threshold, the pricing is $0.015 per GB per month, which is super low anyway). At the end of the article, I'll show how to make sure you stay within the free plan.
Since R2 is just the storage portion, we also need the system that will process all the Docker push, pull, and other commands we'll need. There are multiple options available to us, but until now, they all relied on us hosting a Docker container, which was a bit more work and maintenance than I liked.
Thankfully, Cloudflare released an OCI-compliant registry that runs entirely on their workers platform. The big advantage for us is that Cloudflare Workers have a very large free plan, the deployment is easy, and once deployed, we do not have to do any kind of maintenance work.
Deploying the container registry
To get started, clone the repository:
git clone https://github.com/cloudflare/serverless-registry.git lang-bash
Note that by directly cloning the repo, we'll just have to pull future updates and redeploy when we want to update our registry.
To get it running, just follow the instructions in their readme, it won't take more than a few minutes. The most important task is setting up an R2 bucket to store your images and configuring credentials.
Once done, you'll have a dedicated domain to access your private container registry. Pretty neat!
Configuring Kamal to use your new registry
Now that you have the registry's URL and credentials, open up the deploy.yml configuration.
registry: server: YOUR.REGISTRY-DOMAIN.workers.dev username: yourusername password: - KAMAL_REGISTRY_PASSWORD lang-yaml
And finally, update the secrets file to ensure Kamal has access to your registry.
Kamal is now fully set up with your own container registry. I've read some complaints that Kamal requires hosting Docker images somewhere, but considering how easy it is to do, I don't think that's a big deal.
Caveats and keeping things free
The only real limitation with this approach is that Cloudflare limits the request body size. With a free plan, you can't exceed 100MB. There's a simple workaround available, but it won't work with Kamal out of the box. In any case, the 100MB limit is per layer, not per image. For most web applications, this won't ever be a problem.
Finally, to make sure you do not accumulate images and stay within the free plan, you should periodically clean up your registry. If you like automation, a potential solution could be to add a Kamal post-deploy hook. Here's the file I use:
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" # Get the directory where the script is located SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Load environment variables from .env set -a source "$SCRIPT_DIR/.env.registry" set +a echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fetching credentials from 1Password..." # Get credentials from 1Password USERNAME=$(op read "op://$PASSWORD_VAULT/$ITEM_NAME/$USERNAME_FIELD") PASSWORD=$(op read "op://$PASSWORD_VAULT/$ITEM_NAME/$PASSWORD_FIELD") if [ -z "$USERNAME" ] || [ -z "$PASSWORD" ]; then echo "Failed to retrieve credentials from 1Password" exit 1 fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting registry cleanup..." # Run registry cleanup docker run --rm \ anoxis/registry-cli \ -r "$REGISTRY_URL" \ -l "$USERNAME:$PASSWORD" \ -i velism \ --delete \ --keep-tags-like latest status=$? if [ $status -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Cleanup completed successfully." else echo "[$(date '+%Y-%m-%d %H:%M:%S')] Cleanup failed with exit status: $status" exit $status fi lang-bash
You're now fully setup to stay under the 5GB limit :)
💬 Comments