You are currently viewing Private Docker Registry HTTPS: Secure Setup with Nginx and Basic Auth

Private Docker Registry HTTPS: Secure Setup with Nginx and Basic Auth

  • Post author:
  • Post category:Tutorials
  • Post comments:0 Comments
  • Reading time:10 mins read

Table of Contents

  1. Overview
  2. Prerequisites
  3. Quick Architecture
  4. Variables
  5. Step 1 – Install / Setup
  6. Step 2 – Base Configuration
  7. Step 3 – Enable Service
  8. Step 4 – Firewall & NAT
  9. Step 5 – Add-Ons / Plugins
  10. Step 6 – Client Files / Access
  11. Advanced Add-Ons
  12. Security
  13. Backup & Restore
  14. Troubleshooting
  15. Next Steps

Overview

This guide walks you through a clean, reproducible setup for a Private Docker Registry HTTPS environment. You’ll place Nginx in front for TLS on port 443, protect the registry with basic authentication, and store images on local disk or S3. Everything is copy‑paste friendly and uses per‑OS boxes so you can follow on any major Linux distribution. Reference: Docker Registry documentation.

Prerequisites

One Linux VM or server with a public DNS name for the registry, ports 80/443 available, Docker Engine installed, and shell access with sudo. A second machine with Docker CLI helps to test push/pull.

Quick Architecture

Private Docker Registry HTTPS
Short path: Clients → Nginx → Docker Registry → Storage.

Variables

Before you copy: These variables are here so you can customize the guide to your environment.
Change them to any values you need (domain names, email, directories, ports, usernames). After saving the file and running
source /root/vars.sh, all commands in this guide will read from these variables automatically—so you only need to set them once.

  • REGISTRY_DOMAIN – your public FQDN (e.g., registry.example.com)
  • EMAIL – mailbox used by Certbot/ACME for renewal notices
  • REGISTRY_DATA – on‑disk storage path for image layers
  • REGISTRY_PORT – local port where the container listens (behind Nginx)
  • NGINX_SITE – path to Nginx site config used in this guide
  • AUTH_DIR – directory holding htpasswd credentials
  • AUTH_USER – the username for basic auth (pick your own)

Load a few variables so the rest of the guide stays consistent.

All Linux

sudo install -d -m 700 /root
cat <<'EOF' | sudo tee /root/vars.sh
export REGISTRY_DOMAIN="registry.example.com"
export EMAIL="[email protected]"
export REGISTRY_DATA="/var/lib/registry"
export REGISTRY_PORT="5000"
export NGINX_SITE="/etc/nginx/sites-available/registry.conf"
export AUTH_DIR="/etc/docker/registry/auth"
export AUTH_USER="registryuser"
EOF
source /root/vars.sh
echo "$REGISTRY_DOMAIN $REGISTRY_PORT"  # Check

Step 1 – Install / Setup

Install the registry service and Nginx. You will run the official Docker Registry container and place Nginx in front for TLS offload.

On Debian / Ubuntu

sudo apt update
sudo apt -y install docker.io nginx apache2-utils
docker --version | head -n 1  # Check
nginx -v 2>&1 | head -n 1    # Check

On RHEL / Rocky / Alma / CentOS Stream / Fedora

sudo dnf -y install docker nginx httpd-tools
docker --version | head -n 1  # Check
nginx -v 2>&1 | head -n 1    # Check

On Arch / Manjaro

sudo pacman -Syu --noconfirm docker nginx apache-tools
docker --version | head -n 1  # Check
nginx -v 2>&1 | head -n 1    # Check

On openSUSE / SLE

sudo zypper refresh
sudo zypper install -y docker nginx apache2-utils
docker --version | head -n 1  # Check
nginx -v 2>&1 | head -n 1    # Check

Step 2 – Base Configuration

Create directories, add basic auth, run the registry container, and publish it on localhost:5000. Nginx will proxy it on 443 with HTTPS.

All Linux

# Create data and auth folders
sudo mkdir -p "$REGISTRY_DATA" "$AUTH_DIR"
sudo chown -R root:root "$REGISTRY_DATA" "$AUTH_DIR"
sudo chmod 700 "$AUTH_DIR"

# Create an htpasswd file (you'll be prompted for a password)
sudo htpasswd -Bc "$AUTH_DIR/htpasswd" "$AUTH_USER"

# Run the Docker Registry bound to localhost only
sudo docker run -d --name registry --restart unless-stopped   -p 127.0.0.1:$REGISTRY_PORT:5000   -v "$REGISTRY_DATA":/var/lib/registry   -e REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry   registry:2

# Minimal health check
curl -s http://127.0.0.1:$REGISTRY_PORT/v2/ | jq . 2>/dev/null || echo "OK if empty"  # Check

Now configure Nginx as a TLS reverse proxy for your domain. This example uses a modern TLS config and forwards auth to the registry.

On Debian / Ubuntu

sudo tee "$NGINX_SITE" >/dev/null <<'NGINX'
map $http_upgrade $connection_upgrade {{ default = "close"; "~websocket" = "upgrade"; }}

server {
  listen 80;
  server_name $REGISTRY_DOMAIN;
  location /.well-known/acme-challenge/ { root /var/www/_letsencrypt; }
  location / { return 301 https://$host$request_uri; }
}
server {
  listen 443 ssl http2;
  server_name $REGISTRY_DOMAIN;

  # TLS (provide your certs)
  ssl_certificate     /etc/letsencrypt/live/$REGISTRY_DOMAIN/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/$REGISTRY_DOMAIN/privkey.pem;

  client_max_body_size 1g;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto https;
  proxy_set_header X-Real-IP $remote_addr;

  # Basic auth
  auth_basic "Private Registry";
  auth_basic_user_file $AUTH_DIR/htpasswd;

  location /v2/ { proxy_pass http://127.0.0.1:$REGISTRY_PORT; }
}
NGINX
sudo ln -sf "$NGINX_SITE" /etc/nginx/sites-enabled/registry.conf
sudo nginx -t && sudo systemctl reload nginx  # Check

On RHEL / Rocky / Alma / CentOS Stream / Fedora / Arch / openSUSE

# Single-file nginx.conf style (common on these distros)
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%s)
sudo tee /etc/nginx/conf.d/registry.conf >/dev/null <<'NGINX'
server {
  listen 80;
  server_name $REGISTRY_DOMAIN;
  location /.well-known/acme-challenge/ { root /var/www/_letsencrypt; }
  location / { return 301 https://$host$request_uri; }
}
server {
  listen 443 ssl http2;
  server_name $REGISTRY_DOMAIN;
  ssl_certificate     /etc/letsencrypt/live/$REGISTRY_DOMAIN/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/$REGISTRY_DOMAIN/privkey.pem;

  client_max_body_size 1g;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto https;
  proxy_set_header X-Real-IP $remote_addr;

  auth_basic "Private Registry";
  auth_basic_user_file $AUTH_DIR/htpasswd;

  location /v2/ { proxy_pass http://127.0.0.1:$REGISTRY_PORT; }
}
NGINX
sudo nginx -t && sudo systemctl reload nginx  # Check

Step 3 – Enable Service

Make sure Docker and Nginx start automatically and are healthy.

All Linux

sudo systemctl enable --now docker nginx
sudo docker ps --format "table {{.Names}}	{{.Status}}	{{.Ports}}" | grep -E '^registry' || echo "registry not started?"
sudo ss -tlnp | grep -E ':(80|443)'  # Check

Step 4 – Firewall & NAT

Open TCP 80 and 443 so clients can reach the proxy. If your distro uses a different firewall, use the matching commands.

On Debian / Ubuntu (UFW)

sudo ufw allow 80,443/tcp
sudo ufw reload
sudo ufw status verbose | grep -E '80|443'  # Check

On RHEL / Rocky / Alma / CentOS Stream / Fedora (firewalld)

sudo firewall-cmd --add-service=http --add-service=https --permanent
sudo firewall-cmd --reload
sudo firewall-cmd --list-all | grep services  # Check

On Arch / Manjaro / openSUSE (nftables)

sudo nft add rule inet filter input tcp dport {80,443} accept
sudo nft list ruleset | sed -n '1,120p'  # Check

Step 5 – Add-Ons / Plugins

You can add S3 or other remote storage, enable Content Trust, or integrate with OAuth/SSO. For a quick win, filesystem storage on fast SSDs is often enough.

Step 6 – Client Files / Access

Log in from a client and push a test image. Replace the domain with your registry FQDN.

All Linux

docker login https://$REGISTRY_DOMAIN
docker pull alpine:3
docker tag alpine:3 $REGISTRY_DOMAIN/demo/alpine:3
docker push $REGISTRY_DOMAIN/demo/alpine:3
docker pull $REGISTRY_DOMAIN/demo/alpine:3  # Check

Advanced Add-Ons

Below are optional but highly recommended enhancements. They are safe defaults designed to work on most Linux servers and CI systems.

Docker Compose Variant

Run the registry and Nginx behind a dedicated bridge network. Certificates are read from /etc/letsencrypt/live/$REGISTRY_DOMAIN/.

version: "3.9"
services:
  registry:
    image: registry:2
    container_name: registry
    restart: unless-stopped
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
    volumes:
      - /var/lib/registry:/var/lib/registry
      - /etc/docker/registry/auth:/auth:ro
    networks: [regnet]
    expose:
      - "5000"
  nginx:
    image: nginx:stable
    container_name: reg-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - /etc/nginx/conf.d:/etc/nginx/conf.d
      - /etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on: [registry]
    networks: [regnet]
networks:
  regnet:
    driver: bridge

Check: docker compose ps should show both services healthy and ss -tlnp should list 80/443.

Certbot Automation

Automate certificate issuance and renewal using webroot ACME. Use the OS-specific block below so you can copy/paste safely.

Debian / Ubuntu

sudo install -d -m 755 /var/www/_letsencrypt
sudo apt -y install certbot
sudo certbot certonly --webroot -w /var/www/_letsencrypt -d "$REGISTRY_DOMAIN" --email "$EMAIL" --agree-tos --no-eff-email
sudo openssl x509 -in /etc/letsencrypt/live/$REGISTRY_DOMAIN/fullchain.pem -noout -dates  # Check
sudo certbot renew --dry-run  # Check

RHEL / Rocky / Alma / CentOS Stream / Fedora

sudo install -d -m 755 /var/www/_letsencrypt
sudo dnf -y install certbot
sudo certbot certonly --webroot -w /var/www/_letsencrypt -d "$REGISTRY_DOMAIN" --email "$EMAIL" --agree-tos --no-eff-email
sudo openssl x509 -in /etc/letsencrypt/live/$REGISTRY_DOMAIN/fullchain.pem -noout -dates  # Check
sudo certbot renew --dry-run  # Check

Arch / Manjaro

sudo install -d -m 755 /var/www/_letsencrypt
sudo pacman -Syu --noconfirm certbot
sudo certbot certonly --webroot -w /var/www/_letsencrypt -d "$REGISTRY_DOMAIN" --email "$EMAIL" --agree-tos --no-eff-email
sudo openssl x509 -in /etc/letsencrypt/live/$REGISTRY_DOMAIN/fullchain.pem -noout -dates  # Check
sudo certbot renew --dry-run  # Check

openSUSE / SLE

sudo install -d -m 755 /var/www/_letsencrypt
sudo zypper install -y certbot
sudo certbot certonly --webroot -w /var/www/_letsencrypt -d "$REGISTRY_DOMAIN" --email "$EMAIL" --agree-tos --no-eff-email
sudo openssl x509 -in /etc/letsencrypt/live/$REGISTRY_DOMAIN/fullchain.pem -noout -dates  # Check
sudo certbot renew --dry-run  # Check

Object Storage Backend (S3/GCS compatible)

Switch to object storage for durability and easier scaling. Example with AWS S3:

sudo docker stop registry
sudo docker rm registry || true
sudo docker run -d --name registry --restart unless-stopped   -p 127.0.0.1:$REGISTRY_PORT:5000   -e REGISTRY_STORAGE=s3   -e REGISTRY_STORAGE_S3_REGION=eu-central-1   -e REGISTRY_STORAGE_S3_BUCKET=my-registry-bucket   -e REGISTRY_STORAGE_S3_ACCESSKEY=<AWS_ACCESS_KEY_ID>   -e REGISTRY_STORAGE_S3_SECRETKEY=<AWS_SECRET_ACCESS_KEY>   -e REGISTRY_STORAGE_S3_ENCRYPT=true   -e REGISTRY_STORAGE_S3_SECURE=true   -e REGISTRY_STORAGE_S3_ROOTDIRECTORY=/docker/registry   registry:2
curl -s https://$REGISTRY_DOMAIN/v2/ | head -n 1  # Check (401 expected due to auth)

Garbage Collection

Reclaim space after deletes by running offline garbage collection on a schedule.

cat <<'EOF' | sudo tee /usr/local/sbin/registry-gc.sh
#!/usr/bin/env bash
set -euo pipefail
sudo docker stop registry
sudo docker run --rm -v "$REGISTRY_DATA":/var/lib/registry registry:2 bin/registry garbage-collect /etc/docker/registry/config.yml || true
sudo docker start registry
EOF
sudo chmod +x /usr/local/sbin/registry-gc.sh
echo "0 3 * * sun root /usr/local/sbin/registry-gc.sh" | sudo tee /etc/cron.d/registry-gc
sudo systemctl restart cron 2>/dev/null || sudo systemctl restart crond  # Check

CI/CD Push Example (GitHub Actions)

Push images to your registry on tag creation.

name: Push image
on:
  push:
    tags: ["v*"]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Login
        run: echo "${{ '{' }}secrets.REGISTRY_PASSWORD{{ '}' }}" | docker login https://$REGISTRY_DOMAIN -u "$AUTH_USER" --password-stdin
      - name: Build & Push
        run: |
          docker build -t $REGISTRY_DOMAIN/demo/app:${{ '{' }}github.ref_name{{ '}' }} .
          docker push $REGISTRY_DOMAIN/demo/app:${{ '{' }}github.ref_name{{ '}' }}

Extra Hardening

  • Restrict access to /v2/ by IP allowlists in Nginx or a WAF.
  • Prefer TLS 1.3 (if clients support it), enable OCSP stapling, and use modern ciphers.
  • Set client_max_body_size thoughtfully and monitor disk/network usage.

Security

Tighten TLS and access. Ensure only HTTPS is exposed, keep the registry bound to localhost, and rotate credentials regularly.

All Linux

# Ensure the container only listens on localhost
sudo docker inspect -f '{{range .NetworkSettings.Ports}}{{.}}{{end}}' registry

# Minimal strong TLS tweaks (example ciphers if you manage nginx.conf)
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';

# Permissions for auth folder
sudo chown -R root:root "$AUTH_DIR" && sudo chmod -R 700 "$AUTH_DIR"
test -f "$AUTH_DIR/htpasswd" && echo "auth file present"  # Check

Backup & Restore

Back up the auth file and the registry data directory. Store off the server.

Backup

sudo tar -C / -czf /root/registry-backup.tgz "$AUTH_DIR" "${REGISTRY_DATA}"
ls -lh /root/registry-backup.tgz  # Check

Restore

sudo systemctl stop nginx
sudo docker stop registry
sudo tar -C / -xzf /root/registry-backup.tgz
sudo docker start registry
sudo systemctl start nginx
sudo docker ps | grep -E '^registry\b'  # Check

Troubleshooting

Service not running (systemd unit names)

sudo systemctl status --no-pager docker
sudo systemctl status --no-pager nginx
sudo journalctl -u docker -u nginx --since "20 min ago" | tail -n 200

Port blocked (80/443 not reachable)

ss -tulpen | grep -E ':(80|443)\b' || echo "not listening"
sudo ufw status verbose 2>/dev/null || sudo firewall-cmd --list-all 2>/dev/null || sudo nft list ruleset 2>/dev/null | sed -n '1,120p'

Auth failures (wrong password or missing file)

sudo ls -l "$AUTH_DIR/htpasswd"
sudo htpasswd -B "$AUTH_DIR/htpasswd" "$AUTH_USER"
sudo nginx -t && sudo systemctl reload nginx  # Check

Image push too large (client_max_body_size)

# Ensure this appears inside the 443 server block
client_max_body_size 1g;  # Check

Wrong upstream port (registry not bound to 127.0.0.1)

sudo docker ps --format "table {{.Names}}	{{.Ports}}"
# If exposed externally, remove and re-run with -p 127.0.0.1:$REGISTRY_PORT:5000

Next Steps

Consider adding object storage (S3/GCS), enabling garbage collection, and integrating with your CI/CD pipeline. Keep your TLS certificates renewed and monitor disk space. Reference again the official docs: Docker Registry documentation

Leave a Reply