Illustrative private-registry studio with a seated male engineer, laptop and HTTPS-to-storage DEMO board.

WIUNIX ACADEMY

Private Docker registry with Nginx HTTPS and native authentication

Deploy a digest-pinned OCI registry with loopback-only publishing, bcrypt, streamed uploads, protected client credentials and consistent storage maintenance.

Intermediate60 min readUpdated
Make this guide yours

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Ubuntu 26.04 LTSReviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment8 editable values

THE OUTCOME

What will you have at the end?

Push and pull private OCI images over trusted HTTPS while retaining data and authentication.

01Managed clientacademy-publisher → HTTPS registry.example.net:443
02Nginx → Distribution127.0.0.1:5000 → academy-registry (UID 23153)
03Persistent filesystem/srv/academy-registry/data · 1024 MiB · 900 s
Supported operating systems and versions

OS paths documented; real isolated evidence and untested operational acceptance are distinguished below.

Before you begin

  • Fresh dedicated VM and existing rootful Docker Engine ≥28; selected OS remains a documented path.
  • Trusted certificate/chain/key, DNS, TCP443 and an initialized client credential helper.

A private registry with a clear trust boundary

Build a single-node OCI image registry on a fresh dedicated Linux VM. Nginx terminates trusted HTTPS for registry.example.net; Distribution is published only at host loopback port 5000. Persistent data lives below /srv/academy-registry. This is an intermediate operations exercise, about 60 minutes plus certificate and backup preparation.

Native bcrypt authentication gives every enrolled user broad registry access. It is not repository-level RBAC, a vulnerability scanner, image signing or a highly available service. Choose a token authorization service before admitting mutually untrusted teams. A basic-auth password is protected by TLS; HTTPS must work before credentials are sent.

Set public examples before copying

Open the configuration form, edit the public values, press Apply, then copy. Pending or invalid edits disable copy. Defaults are examples, not evidence of installation. Never enter passwords, HTTP signing secrets, private keys or tokens in this form.

Values and custom progress stay in sessionStorage for this tab, language, article and OS environment. Default progress uses localStorage. Reset restores reviewed examples. Changing a value does not create DNS, reserve a port, change a firewall or execute a command.

Prepare the VM, clients and certificate

Use a fresh dedicated VM with sudo, enough disk/inodes for retained layers and backups, a working signed apt source, and an already installed rootful Docker Engine version 28 or newer. Install Docker using the supported owning procedure for your OS; this tutorial does not replace that installation. Do not mix distro and Docker CE packages. Loopback-publishing bugs in older Docker versions are a reason to stop, not add an insecure registry exception.

Reserve a DNS hostname whose A/AAAA records reach this VM, public TCP443 and the intended client network. Have a current full certificate chain for registry.example.net, its matching private key and a renewal procedure. Public issuance/ACME is outside this recipe; use the linked TLS guide. For a private CA, distribute the CA through managed trust on every client and Docker daemon before login.

Linux test clients need Docker and an initialized pass credential helper with a usable GPG key. Other platforms may use their native credential helper, but that path is not executed here. An uninitialized helper must fail rather than falling back to a base64 password in JSON.

01

LET’S BUILD

Check engine, names, UID and ports

Run on the fresh registry VM. Confirm rootful mode from SecurityOptions; stop if rootless is listed. The chosen name academy-registry must be unused, numeric UID/GID 23153 must not belong to a host account/group, and TCP443/5000 must be free. Reserve this numeric identity in your inventory; no host login account is created. The checks detect current conflicts but do not reserve ports against concurrent provisioning.

bash

Registry VM

set -eu
sudo docker version --format '{{.Server.Version}}'
sudo docker info --format '{{.SecurityOptions}}'
sudo docker container inspect 'academy-registry' >/dev/null 2>&1 && exit 1
if getent passwd '23153' >/dev/null; then exit 1; fi
if getent group '23153' >/dev/null; then exit 1; fi
sudo python3 - <<'PY'
import socket, subprocess
v=subprocess.check_output(['docker','version','--format','{{.Server.Version}}'],text=True).strip()
assert int(v.split('.')[0]) >= 28, 'Docker Engine >=28 required'
for port in (443, 5000):
    with socket.socket() as s:
        s.bind(('0.0.0.0',port))
print('Engine version and port availability: OK')
PY
How do I know it worked?

Engine ≥28, unused identity/name and available ports; no existing service changed.

02

LET’S BUILD

Install only the host-side utilities

Run the selected OS command on the fresh VM. It checks the codename before apt changes. Nginx may start after package installation; this recipe assumes no existing public service. Distribution itself comes from the separately pinned image. Review package candidates and security updates rather than inventing a universal version pin.

bash

Registry VM

set -eu
. /etc/os-release
test "$VERSION_CODENAME" = 'resolute'
sudo apt-get update
apt-cache policy nginx apache2-utils python3
sudo apt-get install --no-install-recommends nginx apache2-utils python3 openssl ca-certificates curl nano
nginx -v
dpkg-query -W nginx apache2-utils python3 openssl
How do I know it worked?

Codename matches and utility versions are listed.

03

LET’S BUILD

Reserve storage and open a protected password editor

Run on the VM. The absolute path /srv/academy-registry must be a new child below an existing real /srv directory. Every existing path component is checked for symlinks; existing data is never adopted or recursively chowned. The numeric registry process owns only its data directory. Root retains auth/config ownership; the container GID can read them.

In the root-only editor enter one randomly generated password, a single line of 20–72 printable ASCII bytes without spaces. Obtain it from your approved password manager; do not put it in a shell argument, browser form, screenshot or command history. Save and close. The plaintext file is temporary and root-only.

bash

Registry VM

sudo python3 - <<'PY'
import os
from pathlib import Path
p=Path('/srv/academy-registry')
assert p.is_absolute() and len(p.parts)>=3 and p.parts[1]=='srv'
for part in [p,*p.parents]:
    assert not part.is_symlink(), 'Symlink path rejected'
assert p.parent.is_dir() and not p.exists(), 'Fresh dedicated path required'
os.mkdir(p,0o750)
os.chown(p,0,23153)
for name in ('data','auth','config','tls','recovery'):
    d=p/name; os.mkdir(d,0o700)
    if name=='data': os.chown(d,23153,23153)
    elif name in ('auth','config'):
        os.chown(d,0,23153); os.chmod(d,0o750)
f=p/'auth/password-input'
fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
os.close(fd)
print('Dedicated directories reserved; protected password editor file is empty')
PY
sudo nano '/srv/academy-registry/auth/password-input'
How do I know it worked?

New protected directories exist; password-input is root-owned 0600.

Explore all tutorials ↗