← Start of tutorial

WIUNIX ACADEMY

Private Docker registry with Nginx HTTPS and native authentication

Page 4 of 5. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

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

Ubuntu 24.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
14

LET’S BUILD

Check denial before trusting the registry

Run on a test client. First use the unauthenticated curl check, then the helper-backed Docker login with a deliberately wrong password typed only at the private prompt. A valid TLS connection must return401 for missing/wrong credentials. Re-enter the correct password after the denial test. Do not record prompt values, Authorization headers or helper output. Native htpasswd is broad access: a successful login is not a repository permission test.

bash

Linux client

curl --silent --show-error --output /dev/null --write-out '%{http_code}
' 'https://registry.example.net/v2/'
docker --config "$HOME/.config/academy-registry-client" login 'registry.example.net' --username 'academy-publisher'
How do I know it worked?

Missing and deliberately wrong credentials are rejected; correct login works again.

15

LET’S BUILD

Prove persistence through a controlled restart

On the VM, stop new writers and allow in-flight uploads to finish before restarting this one container. Then repeat the independent clean-client pull and compare the recorded manifest digest. The 401 health result alone proves neither storage persistence nor complete image availability. Restart persistence is not a backup restore rehearsal or reboot test.

bash

Registry VM

set -eu
sudo docker restart --time 90 'academy-registry'
curl --silent --show-error --output /dev/null --write-out '%{http_code}\n' 'https://registry.example.net/v2/'
How do I know it worked?

Expected401 returns; a fresh pull still fetches the same manifest and blobs.

16

LET’S BUILD

Diagnose upload and authentication failures

Run on the VM; share only sanitized status, never full config/auth files. A413 points first to 1024 MiB per-request limits; include every upstream load balancer in that check. A stalled streamed push calls for checking HTTP/1.1, request buffering, 900-second idle intervals, disk/inodes and write permissions. A502 calls for listener/container status. A401 with correct credentials calls for bcrypt format, selected username and restart after a controlled rotation.

If an upload Location points to HTTP, an internal hostname or another port, stop: inspect the fixed external host and forwarded scheme, not client insecure-registry settings. TLS errors call for SAN/chain/time/managed CA trust checks. The following panel prints no credentials. Nginx access logging is off; keep error logs protected because request context may still be sensitive.

bash

Registry VM

sudo docker inspect 'academy-registry' --format '{{.State.Status}} {{.State.ExitCode}}'
sudo stat -c '%a %u:%g %n' '/srv/academy-registry/data' '/srv/academy-registry/auth/htpasswd' '/srv/academy-registry/config/config.yml'
sudo df -h '/srv/academy-registry/data'
sudo df -i '/srv/academy-registry/data'
sudo nginx -t
curl --silent --show-error --dump-header - --output /dev/null 'https://registry.example.net/v2/'
How do I know it worked?

A specific failure is identified without printing credentials or changing trust/security controls.

17

LET’S BUILD

Take a consistent stopped-writer backup

Schedule an outage, stop all CI pushes, let uploads finish and stop the registry. With every writer stopped, snapshot data plus config/auth/TLS at one consistency point. The archive below is a root-only local staging artifact containing secrets; encrypt and transfer it off-host using your existing approved backup system. A local copy on the same disk is not disaster recovery. On failure leave writers stopped until the failure is understood; do not blindly resume in a trap.

For a future multi-instance or object-store design, stopping one container is insufficient. Coordinate all writers, storage snapshots, database/token-service dependencies and version compatibility. Rehearse restoration into a separate isolated VM with the same pinned version, protected credentials and independent trust/hostname; validate manifests, every referenced blob and a clean-client pull before admitting writers. That restore is a required operational acceptance exercise, not claimed as executed here.

bash

Registry VM

set -eu
sudo docker stop --time 90 'academy-registry'
sudo test "$(sudo docker inspect --format '{{.State.Running}}' 'academy-registry')" = false
sudo python3 - <<'PY'
import os, subprocess
from pathlib import Path
root=Path('/srv/academy-registry')
dest=root/'recovery'/'consistent-backup.tar'
fd=os.open(dest,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:
    subprocess.run(['tar','--numeric-owner','-C',str(root),'-cf','-','data','config','auth','tls'],stdout=out,check=True)
print('Stopped-writer snapshot created; transfer encrypted off-host before resuming')
PY
sudo docker start 'academy-registry'
How do I know it worked?

Writers were stopped, a new0600 archive was created, and protected off-host retention is arranged.

18

LET’S BUILD

Understand deletion before a GC dry run

Deleting a tag or manifest is different from reclaiming blobs. The current service has API deletion disabled. Enabling deletion later requires an authorization/retention decision and version-matched API semantics; it is not needed for push/pull. Never delete storage files manually. Garbage collection marks referenced objects then sweeps unreferenced objects; concurrent uploads can lose layers.

The panel performs only a dry run while the service is fully stopped, with read-only data and the exact pinned image. Even dry-run output can disclose repository names, so keep it private. After a reviewed backup and isolated restore rehearsal, destructive GC needs a separate deliberate maintenance action; omit --delete-untagged unless you explicitly accept its tag/digest and multi-platform consequences. Alternatively readonly maintenance requires restart of every writer into readonly mode and observed write denial before GC; do not merely edit YAML while it is running.

bash

Registry VM

set -eu
sudo docker stop --time 90 'academy-registry'
sudo test "$(sudo docker inspect --format '{{.State.Running}}' 'academy-registry')" = false
sudo docker run --rm --network none --user '23153:23153' \
  --read-only --cap-drop ALL --security-opt no-new-privileges --log-driver none \
  --mount 'type=bind,src=/srv/academy-registry/data,dst=/var/lib/registry,readonly' \
  --mount 'type=bind,src=/srv/academy-registry/auth,dst=/auth,readonly' \
  --mount 'type=bind,src=/srv/academy-registry/config,dst=/etc/distribution,readonly' \
  --entrypoint /bin/registry 'docker.io/library/registry:3.1.2@sha256:ddf754342cfc8acc51a56d5d0ab6af06826461864460636d8bd5c546dab2a7b8' \
  garbage-collect --dry-run /etc/distribution/config.yml
sudo docker start 'academy-registry'
How do I know it worked?

Dry run lists mark/sweep candidates without deletion; the original registry starts again.

Recover without weakening authentication

If the first Nginx reload fails, move only the new site file to the protected recovery directory, validate Nginx again and reload its previous valid configuration. Keep the loopback registry private. If a registry update fails, stop writers and preserve the failed container/config and storage snapshot before using the previous reviewed image with compatible data. Never downgrade across a storage-format boundary without upstream guidance.

For credential rotation, edit a protected candidate bcrypt file, preserve the previous root-only copy, atomically replace it and restart this registry after uploads drain. Test both the old-password denial and new-password acceptance without recording either password. Rollback restores a known-good authenticated configuration; removing auth, publishing backend HTTP or disabling TLS trust is not a recovery method.

Explore all tutorials ↗