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.
Make this guide yoursYOUR SYSTEM. YOUR STEPS.
Where are you working?
Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.
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.
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.
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.
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
Engine ≥28, unused identity/name and available ports; no existing service changed.
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.
Registry VM
set -eu
. /etc/os-release
test "$VERSION_CODENAME" = 'stonking'
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
Codename matches and utility versions are listed.
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.
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'
New protected directories exist; password-input is root-owned 0600.