Harbor — Building a Private Container Repository

Paul Reyes
5 min readAug 27, 2020

I was looking for an opensource container registry implementation which I could use to host my images on-prem and I stumbled upon Harbor.

Harbor is a CNCF Graduated open source registry project that secures artifacts with policies and role-based access control, ensures images are scanned and free from vulnerabilities, and signs images as trusted.

Tried the quick installation steps and my registry was up and running within minutes. Harbor available as Docker containers, it can easily be deployed on any system supporting Docker. There’s even a Helm Chart if you want to deploy it on a Kubernetes Cluster.

Let’s see if we can login to this one.

developer@docker-host:~# docker login https://harbor.mydomain.com
Username: developer
Password:
WARNING! Your password will be stored unencrypted in /home/developer/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
developer@docker-host:~#

Sweet that works!

Now let’s try to build a sample app and upload it to our new repo. Here I created a sample Python Flask application. Nothing fancy.

from flask import Flask, redirect, url_for, session, request, jsonify
import os
app = Flask(__name__)
app.debug = True
@app.route(‘/’)
def index():
return “Hello World”
if __name__ == ‘__main__’:
app.run(host=’0.0.0.0',port=8080)

And let’s create a Dockerfile for this

from python:2.7.10COPY requirements.txt /app/requirements.txt
COPY main.py /app/main.py
RUN pip install -r /app/requirements.txtCMD ["python","/app/main.py"]

Let’s build, tag and push this to our new repo

developer@docker-host:~# docker build .
Sending build context to Docker daemon 4.096kB
Step 1/5 : from python:2.7.10
2.7.10: Pulling from library/python
Image docker.io/library/python:2.7.10 uses outdated schema1 manifest format. Please upgrade to a schema2 image for better future compatibility. More information at https://docs.docker.com/registry/spec/deprecated-schema-v1/
d4bce7fd68df: Pull complete
a3ed95caeb02: Pull complete
816152842605: Pull complete
5dcab2c7e430: Pull complete
dc54ada22a60: Pull complete
b7b0de78f891: Pull complete
88363ed594cb: Pull complete
f8c4a940a0da: Pull complete
dd19554ab82c: Pull complete
Digest: sha256:a11ab9a16bf6853c9c1134fd78ca1abe11e55ab4fe30eb5dffd688a58e7b5899
Status: Downloaded newer image for python:2.7.10
- -> 4442f7b981c4
Step 2/5 : COPY requirements.txt /app/requirements.txt
- -> 9e65b40d3cd4
Step 3/5 : COPY main.py /app/main.py
- -> de9ceae4cf74
Step 4/5 : RUN pip install -r /app/requirements.txt
- -> Running in c6326a490fa7
Collecting flask (from -r /app/requirements.txt (line 1))
Downloading https://files.pythonhosted.org/packages/f2/28/2a03252dfb9ebf377f40fba6a7841b47083260bf8bd8e737b0c6952df83f/Flask-1.1.2-py2.py3-none-any.whl (94kB)
Collecting Jinja2>=2.10.1 (from flask->-r /app/requirements.txt (line 1))
Downloading https://files.pythonhosted.org/packages/30/9e/f663a2aa66a09d838042ae1a2c5659828bb9b41ea3a6efa20a20fd92b121/Jinja2-2.11.2-py2.py3-none-any.whl (125kB)
Collecting click>=5.1 (from flask->-r /app/requirements.txt (line 1))
Downloading https://files.pythonhosted.org/packages/d2/3d/fa76db83bf75c4f8d338c2fd15c8d33fdd7ad23a9b5e57eb6c5de26b430e/click-7.1.2-py2.py3-none-any.whl (82kB)
Collecting Werkzeug>=0.15 (from flask->-r /app/requirements.txt (line 1))
Downloading https://files.pythonhosted.org/packages/cc/94/5f7079a0e00bd6863ef8f1da638721e9da21e5bacee597595b318f71d62e/Werkzeug-1.0.1-py2.py3-none-any.whl (298kB)
Collecting itsdangerous>=0.24 (from flask->-r /app/requirements.txt (line 1))
Downloading https://files.pythonhosted.org/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl
Collecting MarkupSafe>=0.23 (from Jinja2>=2.10.1->flask->-r /app/requirements.txt (line 1))
Downloading https://files.pythonhosted.org/packages/b9/2e/64db92e53b86efccfaea71321f597fa2e1b2bd3853d8ce658568f7a13094/MarkupSafe-1.1.1.tar.gz
Building wheels for collected packages: MarkupSafe
Running setup.py bdist_wheel for MarkupSafe
Stored in directory: /developer/home/.cache/pip/wheels/f2/aa/04/0edf07a1b8a5f5f1aed7580fffb69ce8972edc16a505916a77
Successfully built MarkupSafe
Installing collected packages: MarkupSafe, Jinja2, click, Werkzeug, itsdangerous, flask
Successfully installed Jinja2–2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 click-7.1.2 flask-1.1.2 itsdangerous-1.1.0
You are using pip version 7.1.2, however version 20.2.2 is available.
You should consider upgrading via the 'pip install - upgrade pip' command.
Removing intermediate container c6326a490fa7
- -> 961ad4c4dc60
Step 5/5 : CMD ["python","/app/main.py"]
- -> Running in 60b1b2554c0c
Removing intermediate container 60b1b2554c0c
- -> 2facd6c9fed5
Successfully built 2facd6c9fed5
developer@docker-host:~# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 2facd6c9fed5 13 minutes ago 681MB
python 2.7.10 4442f7b981c4 4 years ago 675MB
developer@docker-host:~# docker tag 2facd6c9fed harbor.mydomain.com/fusion/python-base:latest
developer@docker-host:~# docker push harbor.mydomain.com/fusion/python-base:latest
The push refers to repository [harbor.mydomain.com/fusion/python-base]
362a691995e9: Pushed
bebfe2dca211: Pushed
d6fc7ecd3437: Pushed
5f70bf18a086: Pushed
7ce007c412b5: Pushed
641946be2935: Pushed
3c62eeb65f64: Pushed
a21d673437af: Pushed
d6d84a7ea9f1: Pushed
c32291971339: Pushed
0d78baeff42f: Pushed
12e469267d21: Pushed
latest: digest: sha256:fdbd644635f80f2dae7432837e18aca94635a37857804c02ce565a448b885e3b size: 3663

Let’s take a quick look if it’s in our Harbor repo

We can now push images to our repo, let’s try to deploy this application on our local Kubernetes cluster and test pulling images from this repository.

First thing is we need to create a Secret resource which we could use for our imagePullSecrets

developer@docker-host:~# kubectl create secret docker-registry harbor - docker-server=harbor.mydomain.com - docker-username=developer - docker-password=******* - docker-email=developer@mydomain.comdeveloper@docker-host:~# kubectl describe secret harbor
Name: harbor
Namespace: default
Labels: <none>
Annotations: <none>
Type: kubernetes.io/dockerconfigjsonData
====
.dockerconfigjson: 162 bytes
developer@docker-host:~#

Nice! Let’s now create a Pod to test the above

developer@docker-host:~# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: python-base-app
spec:
containers:
- name: python-base-app
image: harbor.mydomain.com/fusion/python-base@sha256:fdbd644635f80f2dae7432837e18aca94635a37857804c02ce565a448b885e3b
imagePullSecrets:
- name: harbor

developer@docker-host:~# kubectl apply -f pod.yaml
pod/python-base-app created

Let’s check our pod

developer@docker-host:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
python-base-app 1/1 Running 0 6s 10.244.2.203 k8s-node2 <none> <none>
developer@docker-host:~# kubectl describe pod python-base-ap
Name: python-base-app
Namespace: default
Priority: 0
Node: k8s-node2/192.168.0.158
Start Time: Fri, 21 Aug 2020 12:46:19 +0800
Labels: <none>
Annotations: cni.projectcalico.org/podIP: 10.244.2.210/32
Status: Running
IP: 10.244.2.210
IPs: <none>
Containers:
python-base-app:
Container ID: docker://c2efa74dc59fc7051358b044c78af475dece59cb2046e119d090989043168f11
Image: harbor.mydomain.com/fusion/python-base@sha256:fdbd644635f80f2dae7432837e18aca94635a37857804c02ce565a448b885e3b
Image ID: docker-pullable://harbor.mydomain.com/fusion/python-base@sha256:fdbd644635f80f2dae7432837e18aca94635a37857804c02ce565a448b885e3b
Port: <none>
Host Port: <none>
State: Running
Started: Sun, 23 Aug 2020 14:59:23 +0800
Last State: Terminated
Reason: Error
Exit Code: 255
Started: Fri, 21 Aug 2020 12:46:21 +0800
Finished: Sun, 23 Aug 2020 14:56:26 +0800
Ready: True
Restart Count: 1
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-99fc5 (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
default-token-99fc5:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-99fc5
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events: <none>
developer@docker-host:~#

Nice!

Harbor isn’t just a repository. It has Security and vulnerability analysis and Content Signing and validation features to ensure images are scanned and free from vulnerabilities.

What I really like is the RBAC and Logging features which definitely helps in managing and securing the repo.

We’ll look into the Vulnerability Scanning feature in the future.

--

--

Paul Reyes

{ “Cloud Engineer”:☁️, “DevOps”:🤖, “Automation”: ⚙️, “Shutterbug”: 📷}