{"id":4961,"date":"2026-10-02T17:02:21","date_gmt":"2026-10-02T11:32:21","guid":{"rendered":"https:\/\/cpanelfree.com\/blog\/setting-up-a-self-hosted-docker-registry\/"},"modified":"2026-10-02T17:02:21","modified_gmt":"2026-10-02T11:32:21","slug":"setting-up-a-self-hosted-docker-registry","status":"publish","type":"post","link":"https:\/\/cpanelfree.com\/blog\/setting-up-a-self-hosted-docker-registry\/","title":{"rendered":"Setting Up a Self-Hosted Docker Registry"},"content":{"rendered":"<p>Enterprise continuous integration pipelines frequently stall when public container registries throttle traffic with aggressive rate limits, upstream network congestion, or egress transit bottlenecks. Engineering teams deploying staging clusters or private microservices on <a href=\"https:\/\/cpanelfree.com\">CpanelFree<\/a> often find that centralized cloud registries introduce unneeded latency and external dependencies into standard build-and-test loops. Setting up an autonomous, self-hosted Docker registry within your private network or dedicated infrastructure resolves these throughput bottlenecks while enforcing strict data governance and instantaneous image caching.<\/p>\n<p><!-- more --><\/p>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">What is a Self-Hosted Docker Registry and When Should You Deploy One?<\/h2>\n<div class=\"wp-block-group\" style=\"background:#f9f9f9;border-left:4px solid #001b41;padding:16px 20px;margin:20px 0 28px 0;border-radius:0 4px 4px 0\">\n<p style=\"font-size:15px;line-height:1.6;color:#333;margin:0\"><strong>Direct Answer:<\/strong> A self-hosted Docker registry is a privately managed deployment of the OCI Distribution Specification (Registry v2) that stores, indexes, and distributes container image layers inside your own network perimeter. It eliminates public rate limits, reduces layer pull times from minutes to milliseconds, prevents proprietary code leaks, and eliminates recurring outbound cloud egress bandwidth costs.<\/p>\n<\/div>\n<p>Modern container-driven software architectures rely on an Open Container Initiative (OCI) image distribution engine to broker artifacts between build runners, staging environments, and production clusters. When you run <code>docker push<\/code> or <code>docker pull<\/code>, the Docker daemon does not transmit a monolithic virtual machine image. Instead, it negotiates an HTTP\/2 REST API transaction that streams individual content-addressable filesystem layers (blobs) formatted as compressed tar archives, tracked by a JSON-formatted image manifest.<\/p>\n<p>Public container hubs enforce aggressive anonymous pull restrictions\u2014often capping anonymous clients at 100 pulls per six hours and authenticated free accounts at 200 pulls per six hours. In automated CI\/CD pipelines where test runners trigger hundreds of builds daily, reaching these thresholds causes immediate build failures. Running your own self-hosted Docker registry creates an isolated caching and storage sanctuary with zero arbitrary quotas, guaranteed LAN-speed distribution, and full operational sovereignty.<\/p>\n<blockquote class=\"wp-block-quote\" style=\"background:#f9f9f9;border-left:4px solid #001b41;padding:16px 20px;margin:24px 0\">\n<p><strong style=\"color:#001b41\">Architecture Note:<\/strong> The official Docker Distribution Registry (version 2.x) stores images by their cryptographic SHA256 digest. Layers shared across different containers\u2014such as shared base OS layers (Debian, Alpine) or runtime runtimes (Node.js, Python)\u2014are stored once on disk, saving substantial storage capacity while speeding up multi-stage deployment builds.<\/p>\n<\/blockquote>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Architectural Comparison: Public Registries vs. Self-Hosted Infrastructure<\/h2>\n<p>Evaluating whether to maintain your own container storage infrastructure versus outsourcing to third-party managed services requires assessing bandwidth latency, egress economics, and security isolation. The matrix below contrasts default public registries against a tuned self-hosted production deployment.<\/p>\n<figure class=\"wp-block-table is-style-regular\">\n<table style=\"width:100%;border-collapse:collapse;margin:24px 0;font-size:15px;text-align:left\">\n<thead style=\"background:#001b41;color:#ffffff\">\n<tr>\n<th style=\"padding:12px 16px;border-bottom:2px solid #001b41\">Feature \/ Metric<\/th>\n<th style=\"padding:12px 16px;border-bottom:2px solid #001b41\">Standard \/ Default (Public Hub)<\/th>\n<th style=\"padding:12px 16px;border-bottom:2px solid #001b41\">Tuned \/ Production (Self-Hosted)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Pull Latency (Local LAN \/ VPC)<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">350ms &ndash; 1800ms (WAN Dependent)<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7;color:#20B038;font-weight:600\">8ms &ndash; 35ms (Pure NVMe LAN)<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">API Rate Limiting<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Strict (100&ndash;200 pulls \/ 6 hrs)<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7;color:#20B038;font-weight:600\">Unlimited (Hardware Capacity Only)<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Storage Backend Flexibility<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Vendor Proprietary \/ Closed<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7;color:#20B038;font-weight:600\">Local NVMe, S3, MinIO, Ceph, GCS<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Network Egress Costs<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">$0.05 &ndash; $0.09 per GB transferred<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7;color:#20B038;font-weight:600\">$0.00 (Zero Internal Bandwidth Fees)<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Network Isolation<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Public Internet Endpoint Required<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7;color:#20B038;font-weight:600\">Full Air-Gap &amp; Private VPC Capability<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Authentication Integration<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">SaaS Identity \/ Vendor SSO Tier<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7;color:#20B038;font-weight:600\">htpasswd, LDAP, OAuth2, Mutual TLS<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Data Governance &amp; Compliance<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7\">Third-Party Multi-Tenant Cloud<\/td>\n<td style=\"padding:12px 16px;border-bottom:1px solid #e7e7e7;color:#20B038;font-weight:600\">100% Single-Tenant Data Ownership<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Host Kernel Tuning and System Prerequisites<\/h2>\n<p>A high-concurrency container registry handles hundreds of concurrent TCP connections during simultaneous CI runner pulls. Large layers (often 500MB to 2GB) require optimized socket buffer allocations, expanded connection backlogs, and raised file descriptor limits to prevent connection timeouts and packet retransmissions.<\/p>\n<p>Create a dedicated sysctl configuration file at <code>\/etc\/sysctl.d\/99-docker-registry.conf<\/code> to tune the Linux networking stack for high-throughput blob streaming:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># \/etc\/sysctl.d\/99-docker-registry.conf\n# Linux Kernel Optimization for High-Concurrency Container Registry\n\n# Increase maximum open file handles system-wide\nfs.file-max = 2097152\n\n# Enhance socket listen backlog for concurrent incoming pulls\nnet.core.somaxconn = 65535\nnet.ipv4.tcp_max_syn_backlog = 65535\n\n# Expand network interface backlog queue\nnet.core.netdev_max_backlog = 16384\n\n# Allocate larger TCP send and receive buffers for multi-megabyte layer transfers\nnet.core.rmem_max = 16777216\nnet.core.wmem_max = 16777216\nnet.ipv4.tcp_rmem = 4096 87380 16777216\nnet.ipv4.tcp_wmem = 4096 65536 16777216\n\n# Enable TCP BBR congestion control algorithm for low-latency transfers\nnet.core.default_qdisc = fq\nnet.ipv4.tcp_congestion_control = bbr\n\n# Enable TCP window scaling and fast open\nnet.ipv4.tcp_window_scaling = 1\nnet.ipv4.tcp_fastopen = 3\n\n# Reuse TIME_WAIT sockets for outgoing connections\nnet.ipv4.tcp_tw_reuse = 1\n\n# Expand ephemeral port range\nnet.ipv4.ip_local_port_range = 10240 65535<\/code><\/pre>\n<p>Apply the kernel parameters immediately without rebooting by executing:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>sudo sysctl --system<\/code><\/pre>\n<p>In addition to kernel tuning, set user-level file limits in <code>\/etc\/security\/limits.d\/99-registry.conf<\/code>:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># \/etc\/security\/limits.d\/99-registry.conf\n*    soft    nofile    1048576\n*    hard    nofile    1048576\n*    soft    nproc     65536\n*    hard    nproc     65536<\/code><\/pre>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Production Directory Structure and Authentication Setup<\/h2>\n<p>Production container registries should never expose the raw Go distribution daemon directly to untrusted networks. The robust enterprise architecture consists of the official <code>registry:2<\/code> container operating on an internal Docker bridge network, fronted by a hardened <strong>Nginx<\/strong> reverse proxy handling TLS termination, client body buffering, and HTTP Basic authentication.<\/p>\n<p>Initialize the isolated workspace directory structure on your host filesystem:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>sudo mkdir -p \/opt\/docker-registry\/{auth,certs,config,data,nginx}\ncd \/opt\/docker-registry<\/code><\/pre>\n<p>Generate a secure password hash file using <code>htpasswd<\/code> with bcrypt hashing (which provides substantial cryptographic resistance compared to obsolete MD5 or crypt hashes):<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># Install apache2-utils if htpasswd is not present\n# Debian\/Ubuntu: sudo apt-get install -y apache2-utils\n# RHEL\/AlmaLinux: sudo dnf install -y httpd-tools\n\nsudo htpasswd -B -b -c \/opt\/docker-registry\/auth\/htpasswd registryadmin \"YourSecureProductionPassPhrase2026!\"\nsudo chmod 600 \/opt\/docker-registry\/auth\/htpasswd<\/code><\/pre>\n<blockquote class=\"wp-block-quote\" style=\"background:#f9f9f9;border-left:4px solid #001b41;padding:16px 20px;margin:24px 0\">\n<p><strong style=\"color:#001b41\">Security Advisory:<\/strong> The Docker daemon strictly refuses to send plaintext credentials over unencrypted HTTP connections. While Docker supports an <code>insecure-registries<\/code> flag for local loopback testing, doing so in production exposes image layers and authentication headers to on-path interception. You must always terminate connections with valid TLS.<\/p>\n<\/blockquote>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Registry Engine Configuration (config.yml)<\/h2>\n<p>The Registry v2 configuration file controls storage backends, HTTP socket behavior, cache headers, and manifest deletion capabilities. By default, deletion of container manifests is disabled, which prevents disk reclamation routines from functioning. Enable deletion explicitly in the configuration.<\/p>\n<p>Create <code>\/opt\/docker-registry\/config\/config.yml<\/code>:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># \/opt\/docker-registry\/config\/config.yml\nversion: 0.1\nlog:\n  level: info\n  fields:\n    service: registry\n    environment: production\n\nstorage:\n  cache:\n    blobdescriptor: inmemory\n  filesystem:\n    rootdirectory: \/var\/lib\/registry\n    maxthreads: 100\n  maintenance:\n    uploadpurging:\n      enabled: true\n      age: 168h\n      interval: 24h\n      dryrun: false\n  delete:\n    enabled: true\n  redirect:\n    disable: true\n\nhttp:\n  addr: :5000\n  headers:\n    X-Content-Type-Options: [nosniff]\n    X-Frame-Options: [DENY]\n    X-XSS-Protection: [\"1; mode=block\"]\n    Strict-Transport-Security: [\"max-age=31536000; includeSubDomains\"]\n\nhealth:\n  storagedriver:\n    enabled: true\n    interval: 10s\n    threshold: 3<\/code><\/pre>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Hardened Nginx Reverse Proxy Configuration<\/h2>\n<p>When clients push Docker images, multi-gigabyte layers are streamed using HTTP <code>PUT<\/code> and <code>PATCH<\/code> requests. Default Nginx installations restrict client upload payloads to 1MB (via <code>client_max_body_size 1m;<\/code>) and attempt to buffer entire request bodies to temporary disk files before passing them upstream. This default behavior causes catastrophic <code>HTTP 413 Request Entity Too Large<\/code> failures during Docker pushes.<\/p>\n<p>Place the following production-grade configuration in <code>\/opt\/docker-registry\/nginx\/registry.conf<\/code>:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># \/opt\/docker-registry\/nginx\/registry.conf\nupstream docker_registry_backend {\n    server registry:5000;\n    keepalive 64;\n}\n\n# Redirect HTTP to HTTPS\nserver {\n    listen 80;\n    listen [::]:80;\n    server_name registry.yourdomain.com;\n    return 301 https:\/\/$host$request_uri;\n}\n\nserver {\n    listen 443 ssl http2;\n    listen [::]:443 ssl http2;\n    server_name registry.yourdomain.com;\n\n    # SSL \/ TLS Modern Cryptography\n    ssl_certificate \/etc\/nginx\/certs\/fullchain.pem;\n    ssl_certificate_key \/etc\/nginx\/certs\/privkey.pem;\n    ssl_protocols TLSv1.2 TLSv1.3;\n    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';\n    ssl_prefer_server_ciphers on;\n    ssl_session_cache shared:SSL:50m;\n    ssl_session_timeout 1d;\n    ssl_session_tickets off;\n\n    # Crucial Docker Registry directives:\n    # 1. Disable maximum upload limit for gigabyte image layers\n    client_max_body_size 0;\n\n    # 2. Disable request and response buffering for instant streaming\n    proxy_request_buffering off;\n    proxy_buffering off;\n\n    # Forwarding Headers required by Docker Registry V2 API\n    proxy_set_header Host $http_host;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n\n    # Timeouts for sustained high-capacity layer uploads\n    proxy_read_timeout 900s;\n    proxy_send_timeout 900s;\n    send_timeout 900s;\n\n    location \/ {\n        # Check Docker client user agent header or enforce auth\n        auth_basic \"Private Docker Registry\";\n        auth_basic_user_file \/etc\/nginx\/auth\/htpasswd;\n\n        proxy_pass http:\/\/docker_registry_backend;\n        proxy_http_version 1.1;\n        proxy_set_header Connection \"\";\n    }\n\n    # Lightweight Docker Ping Endpoint\n    location \/v2\/ {\n        auth_basic \"Private Docker Registry\";\n        auth_basic_user_file \/etc\/nginx\/auth\/htpasswd;\n\n        proxy_pass http:\/\/docker_registry_backend;\n        proxy_http_version 1.1;\n        proxy_set_header Connection \"\";\n        \n        # Docker client specifically looks for Docker-Distribution-Api-Version header\n        add_header 'Docker-Distribution-Api-Version' 'registry\/2.0' always;\n    }\n}<\/code><\/pre>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Production Docker Compose Stack Deployment<\/h2>\n<p>Now orchestrate both containers using Docker Compose. The configuration binds the registry container exclusively to an internal container bridge network, leaving the Nginx container as the sole exposed listener on ports 80 and 443.<\/p>\n<p>Create <code>\/opt\/docker-registry\/docker-compose.yml<\/code>:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># \/opt\/docker-registry\/docker-compose.yml\nversion: '3.8'\n\nservices:\n  registry:\n    image: registry:2.8.3\n    container_name: docker-registry-engine\n    restart: always\n    environment:\n      REGISTRY_STORAGE_DELETE_ENABLED: \"true\"\n    volumes:\n      - \/opt\/docker-registry\/config\/config.yml:\/etc\/docker\/registry\/config.yml:ro\n      - \/opt\/docker-registry\/data:\/var\/lib\/registry\n    networks:\n      - registry-internal\n    healthcheck:\n      test: [\"CMD\", \"wget\", \"-q\", \"--spider\", \"http:\/\/127.0.0.1:5000\/v2\/\"]\n      interval: 15s\n      timeout: 5s\n      retries: 3\n      start_period: 10s\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"50m\"\n        max-file: \"5\"\n\n  nginx:\n    image: nginx:1.27-alpine\n    container_name: docker-registry-proxy\n    restart: always\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - \/opt\/docker-registry\/nginx\/registry.conf:\/etc\/nginx\/conf.d\/default.conf:ro\n      - \/opt\/docker-registry\/auth:\/etc\/nginx\/auth:ro\n      - \/opt\/docker-registry\/certs:\/etc\/nginx\/certs:ro\n    depends_on:\n      registry:\n        condition: service_healthy\n    networks:\n      - registry-internal\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"50m\"\n        max-file: \"5\"\n\nnetworks:\n  registry-internal:\n    driver: bridge\n    ipam:\n      driver: default\n      config:\n        - subnet: 172.28.0.0\/16<\/code><\/pre>\n<p>Launch the service stack in detached mode:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>sudo docker compose up -d<\/code><\/pre>\n<p>Verify that both containers achieve healthy operational status:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>sudo docker compose ps<\/code><\/pre>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Systemd Service Unit for Autonomous Lifecycle Management<\/h2>\n<p>To guarantee that your Docker registry initializes automatically across host reboots and integrates with host-level log collectors, package the Compose stack inside a systemd management unit.<\/p>\n<p>Create <code>\/etc\/systemd\/system\/docker-registry.service<\/code>:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># \/etc\/systemd\/system\/docker-registry.service\n[Unit]\nDescription=Self-Hosted Docker Registry Stack\nAfter=docker.service network-online.target\nRequires=docker.service\nWants=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=yes\nWorkingDirectory=\/opt\/docker-registry\nExecStart=\/usr\/bin\/docker compose -f \/opt\/docker-registry\/docker-compose.yml up -d --remove-orphans\nExecStop=\/usr\/bin\/docker compose -f \/opt\/docker-registry\/docker-compose.yml down\nExecReload=\/usr\/bin\/docker compose -f \/opt\/docker-registry\/docker-compose.yml restart\nTimeoutStartSec=120\nTimeoutStopSec=60\n\n# Sandboxing and Security Restrictions\nProtectSystem=full\nProtectHome=read-only\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n<p>Enable and start the service with standard systemctl directives:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>sudo systemctl daemon-reload\nsudo systemctl enable docker-registry.service\nsudo systemctl start docker-registry.service\nsudo systemctl status docker-registry.service<\/code><\/pre>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Client Authentication and Image Lifecycle Verification<\/h2>\n<p>Once your registry is operational, verify end-to-end functionality from a remote development workstation or CI runner agent. First, authenticate against the registry endpoint using your generated credentials:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>docker login registry.yourdomain.com -u registryadmin<\/code><\/pre>\n<p>When prompted, enter your password. Upon successful authentication, Docker writes an encoded token to <code>~\/.docker\/config.json<\/code>. Next, pull a standard lightweight image, re-tag it with your custom registry prefix, and push it to your private infrastructure:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># Pull an upstream baseline image\ndocker pull alpine:3.20\n\n# Re-tag for your private registry namespace\ndocker tag alpine:3.20 registry.yourdomain.com\/infra\/alpine:3.20\n\n# Push layers to your self-hosted registry\ndocker push registry.yourdomain.com\/infra\/alpine:3.20<\/code><\/pre>\n<p>Verify that the image catalog index contains your uploaded repository using the OCI v2 REST API:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>curl -u registryadmin:\"YourSecureProductionPassPhrase2026!\"      https:\/\/registry.yourdomain.com\/v2\/_catalog<\/code><\/pre>\n<p>The server returns a JSON response listing the repositories stored in the backend:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>{\"repositories\":[\"infra\/alpine\"]}<\/code><\/pre>\n<p>To inspect specific tags associated with an image repository, query the tag list endpoint:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>curl -u registryadmin:\"YourSecureProductionPassPhrase2026!\"      https:\/\/registry.yourdomain.com\/v2\/infra\/alpine\/tags\/list<\/code><\/pre>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code>{\"name\":\"infra\/alpine\",\"tags\":[\"3.20\"]}<\/code><\/pre>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Storage Maintenance and Garbage Collection Procedures<\/h2>\n<p>One of the most misunderstood aspects of Docker Registry administration is storage reclamation. When you delete an image manifest via the REST API or UI tools, the Docker Registry marks the manifest reference as unlinked, but <strong>does not delete the underlying layer blobs<\/strong>. Blobs remain on disk to prevent race conditions during concurrent push operations.<\/p>\n<blockquote class=\"wp-block-quote\" style=\"background:#f9f9f9;border-left:4px solid #001b41;padding:16px 20px;margin:24px 0\">\n<p><strong style=\"color:#001b41\">Operational Warning:<\/strong> Never execute registry garbage collection while the registry is actively accepting write operations. Doing so can delete layers currently being pushed by CI runners, corrupting manifests. Always place the registry in read-only mode during garbage collection.<\/p>\n<\/blockquote>\n<p>To safely reclaim disk space occupied by orphaned layer blobs, execute the garbage collection utility through the container binary:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># Step 1: Run a dry run to inspect eligible orphaned blobs without deleting\nsudo docker exec -it docker-registry-engine bin\/registry garbage-collect --dry-run \/etc\/docker\/registry\/config.yml\n\n# Step 2: Execute actual garbage collection to purge unreferenced blobs\nsudo docker exec -it docker-registry-engine bin\/registry garbage-collect \/etc\/docker\/registry\/config.yml\n\n# Step 3: Remove empty upload staging directories\nsudo docker exec -it docker-registry-engine bin\/registry garbage-collect -m \/etc\/docker\/registry\/config.yml<\/code><\/pre>\n<p>Automate this routine weekly via an administrative cron job scheduled during off-peak deployment maintenance windows:<\/p>\n<pre class=\"wp-block-code\" style=\"background:#f3f3f3;color:#333;padding:16px;border-left:4px solid #001b41;font-family:monospace;font-size:13px\"><code># \/etc\/cron.d\/docker-registry-gc\n# Run registry garbage collection every Sunday at 03:00 UTC\n0 3 * * 0 root \/usr\/bin\/docker exec docker-registry-engine bin\/registry garbage-collect -m \/etc\/docker\/registry\/config.yml &gt; \/var\/log\/registry-gc.log 2&gt;&amp;1<\/code><\/pre>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Production Sizing and Infrastructure Optimization<\/h2>\n<p>When scaling beyond single-node prototypes to enterprise production workloads with dozens of concurrent CI runner nodes pushing multi-gigabyte layers, network I\/O and storage throughput become critical bottlenecks. Provisioning your registry on high-throughput, low-latency NVMe infrastructure such as <a href=\"https:\/\/merahost.org\" target=\"_blank\" rel=\"noopener\">MeraHost Enterprise Cloud<\/a> guarantees dedicated unmetered uplink speeds and predictable operational costs with zero unexpected renewal hikes.<\/p>\n<p>For distributed teams operating across multiple geographic regions, configure your local registry as a <strong>pull-through cache<\/strong> proxying upstream images. In this topology, developers point their Docker daemon to the private registry; if an image layer is missing locally, the registry fetches it once from the origin and serves all subsequent cluster requests directly from local NVMe cache at wire speed.<\/p>\n<h2 style=\"color:#001b41;font-size:22px;font-weight:700;margin-top:32px\">Frequently Asked Questions (FAQ)<\/h2>\n<details class=\"wp-block-group\" style=\"background:#f9f9f9;border:1px solid #e7e7e7;border-radius:4px;padding:14px;margin-bottom:12px\">\n<summary style=\"cursor:pointer;font-weight:600;color:#001b41\">Can I run a self-hosted Docker registry without SSL\/TLS certificates?<\/summary>\n<p style=\"margin-top:10px;color:#444\">While technically possible by adding your registry domain to the <code>insecure-registries<\/code> array in <code>\/etc\/docker\/daemon.json<\/code> on every client, this is strongly discouraged in production. Plain HTTP transmits basic authentication credentials in base64 without encryption and leaves container images vulnerable to man-in-the-middle tampering. Using free Let&#8217;s Encrypt certificates or internal corporate CA certificates with Nginx TLS termination eliminates this security vulnerability.<\/p>\n<\/details>\n<details class=\"wp-block-group\" style=\"background:#f9f9f9;border:1px solid #e7e7e7;border-radius:4px;padding:14px;margin-bottom:12px\">\n<summary style=\"cursor:pointer;font-weight:600;color:#001b41\">Why did my host disk space not decrease after deleting an image tag?<\/summary>\n<p style=\"margin-top:10px;color:#444\">Deleting a tag or manifest via the Registry API merely unlinks the tag pointer from the manifest digest. The actual filesystem layers (blobs) remain intact on disk to prevent corruption of images sharing identical layers. To reclaim physical disk space, you must execute the garbage collector tool: <code>docker exec -it docker-registry-engine bin\/registry garbage-collect \/etc\/docker\/registry\/config.yml<\/code> while the registry is in a quiescent state.<\/p>\n<\/details>\n<details class=\"wp-block-group\" style=\"background:#f9f9f9;border:1px solid #e7e7e7;border-radius:4px;padding:14px;margin-bottom:12px\">\n<summary style=\"cursor:pointer;font-weight:600;color:#001b41\">How do I resolve &#8220;HTTP 413 Request Entity Too Large&#8221; errors when pushing layers?<\/summary>\n<p style=\"margin-top:10px;color:#444\">This error occurs when the reverse proxy (Nginx) enforces default client upload payload limits (usually 1MB). To resolve this, configure <code>client_max_body_size 0;<\/code> inside your Nginx server block. Additionally, disable request body buffering with <code>proxy_request_buffering off;<\/code> and <code>proxy_buffering off;<\/code> so multi-gigabyte layers stream directly to the registry backend without filling proxy storage partitions.<\/p>\n<\/details>\n<details class=\"wp-block-group\" style=\"background:#f9f9f9;border:1px solid #e7e7e7;border-radius:4px;padding:14px;margin-bottom:12px\">\n<summary style=\"cursor:pointer;font-weight:600;color:#001b41\">How can I back up my self-hosted Docker registry?<\/summary>\n<p style=\"margin-top:10px;color:#444\">Backing up the registry requires archiving the persistent storage root (by default <code>\/opt\/docker-registry\/data<\/code>) alongside configuration files and authentication tables. Because the registry filesystem is strictly content-addressable and immutable once written, you can create consistent snapshots using LVM snapshots, ZFS send\/receive, or daily incremental rsync jobs to remote secondary storage without stopping read traffic.<\/p>\n<\/details>\n<div class=\"wp-block-group has-background\" style=\"background:#f9f9f9;border:1px solid #e7e7e7;border-radius:8px;padding:32px;margin:40px 0;text-align:center\">\n<h3 style=\"color:#001b41;margin-top:0;font-size:24px;font-weight:700\">Deploy Enterprise-Grade Production Infrastructure<\/h3>\n<p style=\"color:#444;font-size:16px;line-height:1.6;max-width:680px;margin:12px auto 24px auto\">Need guaranteed performance with zero price hikes? Host mission-critical workloads on <strong style=\"color:#001b41\">MeraHost<\/strong> with pure Enterprise NVMe, LiteSpeed Web Server, and Same Renewal Price, Always (starting at \u20b999\/mo).<\/p>\n<div class=\"wp-block-buttons\" style=\"display:flex;gap:16px;justify-content:center;flex-wrap:wrap\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link\" href=\"https:\/\/merahost.org\" style=\"background:#001b41;color:#ffffff;font-weight:700;padding:12px 28px;border-radius:4px;text-decoration:none;display:inline-block;font-size:15px\" target=\"_blank\" rel=\"noopener\">Explore MeraHost NVMe Cloud &rarr;<\/a><\/div>\n<div class=\"wp-block-button is-style-outline\"><a class=\"wp-block-button__link\" href=\"https:\/\/cpanelfree.com\" style=\"background:transparent;color:#001b41;font-weight:600;padding:12px 24px;border:2px solid #001b41;border-radius:4px;text-decoration:none;display:inline-block;font-size:15px\">Deploy Free Staging on CpanelFree<\/a><\/div>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Deploy a high-performance self-hosted Docker registry with TLS and htpasswd auth. Optimize CI\/CD image pulls, cut egress latency, and secure container assets.<\/p>\n","protected":false},"author":1,"featured_media":4960,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[215],"tags":[57,216,177,87,101],"class_list":["post-4961","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-containers","tag-almalinux","tag-containers","tag-databases-performance","tag-devops","tag-sysadmin"],"_links":{"self":[{"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/posts\/4961","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/comments?post=4961"}],"version-history":[{"count":0,"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/posts\/4961\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/media\/4960"}],"wp:attachment":[{"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/media?parent=4961"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/categories?post=4961"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cpanelfree.com\/blog\/wp-json\/wp\/v2\/tags?post=4961"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}