Container registries act as central repositories which users can use to share their container images. This is often done through some kind of automated build system like Github Actions. This is to ensure developers are able to release the most stable build of their code in an automated fashion. One of the most common container registries is Docker Hub; however, some hyperscalers (such as Google Cloud and AWS) have their own container registry offerings.

In this tutorial, we will learn how to deploy your own container registry in Kubernetes using Harbor.

What is Harbor, and why use it?

Harbor is an open-source container registry that allows developers to store and distribute container images securely. It provides a controlled and organized environment, ensuring only trusted images are used in deployments. Harbor goes beyond basic registries by offering features like access control, vulnerability scanning, and replication, which are crucial for large-scale or security-sensitive projects.

Using Harbor means you have greater control over your container images, helping you meet compliance requirements and safeguard against potential security risks.


Before proceeding with this tutorial, ensure you have a basic understanding of Docker and Kubernetes. Additionally, you will need:

Deploying your container registry using Harbor

Preparing the Kubernetes cluster

We’ll begin by creating a Kubernetes cluster with the Nginx Ingress controller installed.

For simplicity, we will be doing it from the CLI:

civo k3s create --create-firewall --nodes 2 -m --save --switch --wait nginx-rate-test -r=Traefik -a=Nginx

This would launch a two-node cluster with Nginx ingress installed. This would also point your kube-context to the cluster we just launched.

Installing Harbor

With our cluster up and running, let’s install Harbor using Helm, open up your terminal emulator of choice, and follow along.

Export your DNS name:

export INGRESS_HOST=<your civo dns name> 

Install the helm release:

helm upgrade --install harbor harbor/harbor \\
    --namespace harbor \\
    --create-namespace \\
    --set expose.ingress.hosts.core=harbor.$INGRESS_HOST \\
    --set externalURL=http://harbor.$INGRESS_HOST \\

There’s quite a bit going on here, so here’s what the important flags do:

  • -create-namespace: This flag tells Helm to create the "harbor" namespace if it doesn't already exist.
  • -set expose.ingress.hosts.core=harbor.$INGRESS_HOST: This sets a specific configuration value for the Harbor installation. It's configuring the host that will be used for the Ingress resource, which is how external traffic will reach Harbor.
  • -set externalURL=http://harbor.$INGRESS_HOST: This sets the external URL for Harbor. This is the URL that users will use to access Harbor.
  • -wait: This flag makes Helm wait until all resources are in a ready state before considering the installation to be successful.

In a couple of minutes, you should be able to access your installation by visiting harbor.<yourcivodnsname>.com

You’d be greeted with a login screen. By default, the username is admin and the password is Harbor12345

Note: Remember to change these as soon as possible

Working with Harbor

With harbor installed, let's push an image to test it’s functionality.

We’ll start by creating a project, that would allow us to organize images. Within the dashboard, click on the new project and fill in the popup form shown below:

Working with Harbor

Next, we’ll need a docker image to push, within your editor of choice, create a Dockerfile and add the following directives:

FROM python:3.9-slim


RUN echo "print('Mooo!')" >

CMD ["python", ""]

As we don’t have TLS configured on our harbor domain, we’d have to go through a few extra steps. However, for production environments, you should configure TLS using something like CertManager.

If you use docker desktop, you can explicitly tell the docker daemon you are using an insecure registry in the Settings > Docker engine and add the following directive:

"insecure-registries": [""]

Configure TLS  with Harbor

On Linux distributions, you can achieve the same by editing the config file at /etc/docker/daemon.json. Restart docker, and the changes should take effect.

Build the docker image

In your terminal, navigate to the same directory as your Dockerfile and run the following command:

docker buildx build -t harbor.<yourcivodnsname>/civoexperiments/cowsay:v1 .

be sure to replace <yourcivodnsname> with the same DNS name you exported earlier.

Push the image

docker push harbor.<yourcivodnsname>/civoexperiments/cowsay:v1

Once your image is done uploading back to the dashboard, you should see the following:

Push the image with Harbor

Should you host your own registry?

Like many things, it largely depends. Running your own registry can be useful when trying to meet compliance requirements or a cost-saving, especially with providers like Civo. However, this also comes with an operational cost. Harbor has a few more tricks up its sleeve, such as Vulnerability Scanning and Replication which can help with Redundancy.

If you are considering running Harbor in production, you’d want to look into running it in high availability mode. This is outside the scope of this article but would enable you to run it in production with a bit more confidence.


In this tutorial, we explored self-hosting a container registry on Kubernetes with Harbor. We covered the importance of container registries, introduced Harbor as a secure solution, and outlined the steps to deploy it. We also discussed some points to consider while deciding if the self-hosted route is best for you.

Further Resources

If you want to continue learning more about Harbor, check out these resources:

For an added layer of security, consider signing your images, you can learn more about that here.

Harbor supports more than just container images, learn how to store helm charts here.