{"id":11573,"date":"2025-08-10T14:29:38","date_gmt":"2025-08-10T14:29:38","guid":{"rendered":"https:\/\/putridparrot.com\/blog\/?p=11573"},"modified":"2025-08-10T14:29:38","modified_gmt":"2025-08-10T14:29:38","slug":"creating-a-local-container-registry","status":"publish","type":"post","link":"https:\/\/putridparrot.com\/blog\/creating-a-local-container-registry\/","title":{"rendered":"Creating a local container registry"},"content":{"rendered":"<p>I feel love I&#8217;ve written a post on this before, but it doesn&#8217;t hurt to keep things upto date.<\/p>\n<p>I set up a Kubernetes instance along with container registry etc. within Azure, but if all you want to do is run things locally and at zero cost (other than you usual cost of running a computer), you might want to set up a local container registry.<\/p>\n<p>I&#8217;m doing this on Windows for this post, but I expect it&#8217;s pretty much the same on Linux and Mac, but in my case I am also running Docker Desktop.<\/p>\n<p><strong>docker-compose<\/strong><\/p>\n<p>We&#8217;re going to stand up the registry using docker-compose, but if you&#8217;d like to just run the registry from the simple docker run command, you can use this<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ndocker run -d -p 5000:5000 --name registry registry:2\r\n<\/pre>\n<p>However you&#8217;ll probably want a volume set-up, along with a web UI, so let&#8217;s put all that together into the <em>docker-compose.yml<\/em> file below<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nversion: &#039;3.8&#039;\r\n\r\nservices:\r\n  registry:\r\n    image: registry:2\r\n    container_name: container-registry\r\n    ports:\r\n      - &quot;5000:5000&quot;\r\n    environment:\r\n      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: \/var\/lib\/registry\r\n      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: &#039;&#x5B;&quot;http:\/\/localhost:8080&quot;]&#039;\r\n      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: &#039;&#x5B;&quot;GET&quot;, &quot;HEAD&quot;, &quot;DELETE&quot;]&#039;\r\n      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials: &#039;&#x5B;&quot;true&quot;]&#039;\r\n    volumes:\r\n      - registry-data:\/var\/lib\/registry\r\n\r\n  registry-ui:\r\n    image: joxit\/docker-registry-ui:latest\r\n    container_name: registry-ui\r\n    ports:\r\n      - &quot;8080:80&quot;\r\n    environment:\r\n      - REGISTRY_TITLE=Private Docker Registry\r\n      - REGISTRY_URL=http:\/\/localhost:5000\r\n      - DELETE_IMAGES=true\r\n    depends_on:\r\n      - registry\r\n\r\nvolumes:\r\n  registry-data:\r\n<\/pre>\n<p><em>Note: if you&#8217;re going to be running your services on port 8080, you might wish to change the UI here to use port 8081, for example.<\/em><\/p>\n<p>In the above I name my container <em>container-registry<\/em> as I already have a <em>registry<\/em> container running on my machine, the REGISTRY_HTTP_HEADERS_Access were required as I was getting CORS like issues. Finally we run up the <em<>joxit\/docker-registry-ui<\/em> which gives us a web UI to our registry. <\/p>\n<p>To run everything just type<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ndocker-compose up\r\n<\/pre>\n<p>and use ctrl+c to bring this down when in interactive mode or run<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ndocker-compose down\r\n<\/pre>\n<p>If you want to clear up the volume (i.e. remove it) use <\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ndocker-compose down -v\r\n<\/pre>\n<p>Ofcourse you can also use curl etc. to interact with the registry itself, for example to list the repositories<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ncurl http:\/\/localhost:500\/v2\/_catalog\r\n<\/pre>\n<p><strong>Tag and push<\/strong><\/p>\n<p>We&#8217;re obviously going to need to push images to the registry and this is done by first, tagging them (as usual) then pushing, so for example<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ndocker tag putridparrot.echo_service:v1 localhost:5000\/putridparrot.echo_service:v1\r\n<\/pre>\n<p>which tags the image I already created for a simple &#8220;echo service&#8221;.<\/p>\n<p>Next we push the tagged image to the registry using<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ndocker push localhost:5000\/putridparrot.echo_service:v1\r\n<\/pre>\n<p>If you&#8217;re running the web UI you should be able to see the repository with your new tagged image.<\/p>\n<p><strong>Pull<\/strong><\/p>\n<p>Obviously we&#8217;ll want to be able to pull an image from the registry either to run locally or to deploy within a Kubernetes cluster etc. <\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\ndocker pull localhost:5000\/putridparrot.echo_service:v1\r\n<\/pre>\n<p><strong>Deployment<\/strong><\/p>\n<p>If some coming posts I will be writing an echo service in multiple languages, so let&#8217;s assume this echo_services is one of those. We&#8217;re going to want to run things locally so we might have a deployments.yaml with the following deployment and service<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\napiVersion: apps\/v1\r\nkind: Deployment\r\nmetadata:\r\n  name: echo\r\n  namespace: dev\r\n  labels:\r\n    app: echo\r\nspec:\r\n  replicas: 1\r\n  selector:\r\n    matchLabels:\r\n      app: echo\r\n  template:\r\n    metadata:\r\n      labels:\r\n        app: echo\r\n    spec:\r\n      containers:\r\n      - name: echo\r\n        image: localhost:5000\/putridparrot.echo_service:v1\r\n        ports:\r\n        - containerPort: 8080\r\n        resources:\r\n          requests:\r\n            memory: &quot;100Mi&quot;\r\n            cpu: &quot;100m&quot;\r\n          limits:\r\n            memory: &quot;200Mi&quot;\r\n            cpu: &quot;200m&quot;\r\n        livenessProbe:\r\n          httpGet:\r\n            path: \/livez\r\n            port: 8080\r\n          initialDelaySeconds: 30\r\n          periodSeconds: 10\r\n        readinessProbe:\r\n          httpGet:\r\n            path: \/readyz\r\n            port: 8080\r\n          initialDelaySeconds: 5\r\n          periodSeconds: 5\r\n\r\n---\r\napiVersion: v1\r\nkind: Service\r\nmetadata:\r\n  name: echo-service\r\n  namespace: dev\r\n  labels:\r\n    app: echo\r\nspec:\r\n  type: ClusterIP\r\n  selector:\r\n    app: echo \r\n  ports:\r\n  - name: http\r\n    port: 80\r\n    targetPort: 8080\r\n    protocol: TCP\r\n<\/pre>\n<p>Now we can use port forwarding in place on an ingress service if we&#8217;d like, for testing, like this<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nkubectl port-forward svc\/echo-service 8080:8080 -n dev\r\n<\/pre>\n<p>and now use <em>http:\/\/localhost:8080\/echo?text=Putridparrot<\/em><\/p>\n<p>Other options to get this deployment running with ingress require <em>hosts<\/em> file changes or we can add a load balancer, for example<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\napiVersion: v1\r\nkind: Service\r\nmetadata:\r\n  name: echo-service\r\nspec:\r\n  type: LoadBalancer\r\n  selector:\r\n    app: echo\r\n  ports:\r\n    - protocol: TCP\r\n      port: 8080\r\n      targetPort: 8080\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>I feel love I&#8217;ve written a post on this before, but it doesn&#8217;t hurt to keep things upto date. I set up a Kubernetes instance along with container registry etc. within Azure, but if all you want to do is run things locally and at zero cost (other than you usual cost of running a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[102],"tags":[],"class_list":["post-11573","post","type-post","status-publish","format-standard","hentry","category-docker"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/11573","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/comments?post=11573"}],"version-history":[{"count":5,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/11573\/revisions"}],"predecessor-version":[{"id":11581,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/11573\/revisions\/11581"}],"wp:attachment":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/media?parent=11573"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/categories?post=11573"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/tags?post=11573"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}