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.
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
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.
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'
Missing and deliberately wrong credentials are rejected; correct login works again.
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.
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/'
Expected401 returns; a fresh pull still fetches the same manifest and blobs.
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.
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/'
A specific failure is identified without printing credentials or changing trust/security controls.
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.
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'
Writers were stopped, a new0600 archive was created, and protected off-host retention is arranged.
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.
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'
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.