| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64quay.io/libpod/registry:2.8.2 |
| Applies to | Any Linux host with Podman installed |
| Privilege | Rootful examples for system-wide certs.d and registries.conf.d; rootless notes where user-scoped trust differs |
| Scope | Single-host private registry lab with quay.io/libpod/registry:2.8.2 — HTTP insecure path, HTTPS with SAN certificate, certs.d CA trust, htpasswd authentication, podman login, push, pull, run, and API checks. Does not cover registry HA, object-storage backends, full registries.conf reference, auth-file internals, or image signing. |
| Related guides | Pull images with podman pull Configure registries.conf |
A private registry gives you a repository you control for podman push and podman pull without depending on a public hub. This walkthrough runs the registry as a Podman container on one host, wires Podman to trust and authenticate to it, and proves the full loop with a real image. We start with a plain HTTP lab registry, then replace it with TLS, a custom CA, and htpasswd credentials — the shape most teams want before production.
Private registry lab architecture
On a single lab host the registry hostname resolves to loopback so every step stays local:
Podman client
│
│ HTTPS :5000 (or HTTP in the first lab phase)
▼
registry.example.test:5000
│
├── TLS certificate (HTTPS phase)
├── htpasswd authentication (HTTPS phase)
└── registry blob storageName resolution for the TLS phase:
127.0.0.1 registry.example.testAdd that line to /etc/hosts on the test machine. The HTTP-only phase uses localhost:5000 directly and does not need the extra hostname.
Pull the Podman project registry image
Use the registry image maintained for Podman testing rather than docker.io/library/registry:
quay.io/libpod/registry:2.8.2That avoids Docker Hub rate limits and matches what Podman upstream exercises in its own test suite.
Pull the pinned version:
podman pull quay.io/libpod/registry:2.8.2Sample output:
Trying to pull quay.io/libpod/registry:2.8.2...
Getting image source signatures
Copying blob sha256:7417fa3c6d923a722787ff60825c3c40f74621ab2e4abcc120e25b10a35e4811
Copying blob sha256:7264a8db6415046d36d16ba98b79778e18accee6ffa71850405994cffa9be7de
Copying blob sha256:88b450dec42ebc9659f10c6fd6d2326706fec4ab7aadf0c38973d930c23c546a
Copying blob sha256:c4d48a809fc2256f8aa0aeee47998488d64409855adba00a7cb3007ab9f3286e
Copying blob sha256:121f958bea53668d782691e643e9401ea21dd36c9d81078b51964d2e82b51376
Copying config sha256:0030ba3d620c647159c935ee778991c68ef3e51a274703753b0bc530104ef5e5
Writing manifest to image destination
0030ba3d620c647159c935ee778991c68ef3e51a274703753b0bc530104ef5e5The image ID at the end confirms the pull landed in local storage.
Start a simple HTTP registry (lab only)
Run the registry listening on port 5000:
podman run -d --name local-registry -p 5000:5000 quay.io/libpod/registry:2.8.2Podman prints the container ID when the detach succeeds. Confirm the Registry API responds over plain HTTP:
curl http://127.0.0.1:5000/v2/Sample output:
{}An empty JSON object is the normal unauthenticated response from a registry v2 root. Podman defaults to secure transport, so pushing to this HTTP endpoint fails until you mark the registry insecure or enable TLS later.
Mark the HTTP registry as insecure
Scope the exception to the exact host and port in a drop-in under registries.conf.d:
sudo tee /etc/containers/registries.conf.d/local-registry.conf << 'EOF'
[[registry]]
location = "localhost:5000"
insecure = true
EOFinsecure = true for that location allows plain HTTP and, for HTTPS registries in the same scope, can permit connections when the certificate is not trusted. Do not use a broad wildcard — one stanza per lab registry is enough.
Pull a small image to publish:
podman pull quay.io/podman/helloTag it for the insecure registry namespace:
podman tag quay.io/podman/hello localhost:5000/demo/image:v1Push to the HTTP registry:
podman push localhost:5000/demo/image:v1Sample output:
Getting image source signatures
Copying blob sha256:2114fc8b70586b9325dde6fd26066d9951414dcdfb3995f41d51d1995cf3bd9d
Copying config sha256:5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0
Writing manifest to image destinationThe layer copy lines confirm Podman reached the HTTP registry after the insecure stanza took effect. For the TLS path below, you will stop this container and rely on CA trust instead of insecure = true for the same hostname.
Stop the HTTP registry and prepare TLS paths
Remove the HTTP-only container before binding port 5000 again:
podman rm -f local-registryCreate host directories for certificates, authentication, and optional persistent storage:
sudo mkdir -p /srv/podman-registry/certs /srv/podman-registry/auth /srv/podman-registry/dataAdd the TLS hostname to /etc/hosts on the lab host:
grep -q 'registry.example.test' /etc/hosts || echo '127.0.0.1 registry.example.test' | sudo tee -a /etc/hostsModern TLS clients validate the name in the certificate against the hostname you connect to — a legacy common name alone is not enough. Generate a small CA and a server certificate whose Subject Alternative Name includes DNS:registry.example.test.
Generate a CA and server certificate with SAN
Work in the certs directory:
cd /srv/podman-registry/certsCreate a lab certificate authority:
openssl genrsa -out CA.key 4096Issue the CA certificate:
openssl req -new -x509 -days 365 -key CA.key -out CA.crt -subj "/CN=GoLinuxCloud Registry Lab CA"Generate the server key and certificate signing request:
openssl genrsa -out domain.key 2048Create the CSR with registry.example.test as the common name:
openssl req -new -key domain.key -out domain.csr -subj "/CN=registry.example.test"Add a SAN extension file so the signed certificate includes the registry hostname:
printf '%s\n' '[req]' 'distinguished_name=req' '[v3_req]' 'subjectAltName=DNS:registry.example.test' | sudo tee /srv/podman-registry/certs/san.cnfSign the server certificate with the CA:
openssl x509 -req -days 365 -in domain.csr -CA CA.crt -CAkey CA.key -CAcreateserial -out domain.crt -extfile san.cnf -extensions v3_reqConfirm the SAN is present before you start the registry:
openssl x509 -in domain.crt -noout -subject -issuer -ext subjectAltNameSample output:
subject=CN=registry.example.test
issuer=CN=GoLinuxCloud Registry Lab CA
X509v3 Subject Alternative Name:
DNS:registry.example.testIf you connect by IP as well as DNS, add the IP to subjectAltName in san.cnf and re-sign.
Run the registry with TLS enabled
Mount the certificate files read-only and set the registry 2.x environment variables Podman expects:
podman run -d --name tls-registry -p 5000:5000 -v /srv/podman-registry/data:/var/lib/registry:Z -v /srv/podman-registry/certs:/certs:ro,Z -v /srv/podman-registry/auth:/auth:ro,Z -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key quay.io/libpod/registry:2.8.2The -v /srv/podman-registry/data:/var/lib/registry:Z bind mount keeps uploaded blobs on the host when you recreate the container later for htpasswd authentication.
Verify HTTPS at the API root using the CA file — before Podman is involved:
curl --cacert /srv/podman-registry/certs/CA.crt https://registry.example.test:5000/v2/At this stage the registry may still allow anonymous access; you should see {} or a similar success body. If curl fails here, fix TLS on the registry container before configuring Podman.
Prove TLS verification with --tls-verify=false
Podman has no CA installed for registry.example.test:5000 yet. A push without the flag should fail with an x509 error:
podman push quay.io/podman/hello registry.example.test:5000/demo/hello:v1That failure is expected until you install the CA in certs.d. To confirm the registry is reachable over HTTPS and only trust is missing, retry with:
podman push --tls-verify=false \
quay.io/podman/hello \
registry.example.test:5000/demo/hello:v1If this succeeds while the same push without --tls-verify=false fails with an x509 error, registry connectivity works and the remaining problem is certificate trust — not a production fix. The next section installs the CA in certs.d instead of keeping the flag.
Trust the registry CA in Podman
Podman reads registry-specific CAs from certs.d. The directory name must match the endpoint you use in image references, including the port:
/etc/containers/certs.d/registry.example.test:5000/ca.crtInstall the lab CA:
sudo mkdir -p /etc/containers/certs.d/registry.example.test:5000Copy the lab CA into that directory as ca.crt:
sudo cp /srv/podman-registry/certs/CA.crt /etc/containers/certs.d/registry.example.test:5000/ca.crtThis is narrower than adding the CA to the operating system-wide trust store — only Podman registry clients that honor certs.d trust this CA for that host and port. After this step, podman push and podman pull should verify TLS without --tls-verify=false.
Enable htpasswd authentication on the registry
Create credentials with htpasswd from the httpd-tools package (or your distribution equivalent). Pipe the password so it does not sit in shell history:
read -r -s REGISTRY_PASSWORD && printf '%s' "$REGISTRY_PASSWORD" | htpasswd -Bni labuser | sudo tee /srv/podman-registry/auth/htpasswd > /dev/nullRecreate the registry container with authentication environment variables:
podman rm -f tls-registryStart the registry again with TLS and htpasswd enabled:
podman run -d --name tls-registry -p 5000:5000 -v /srv/podman-registry/data:/var/lib/registry:Z -v /srv/podman-registry/certs:/certs:ro,Z -v /srv/podman-registry/auth:/auth:ro,Z -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key -e REGISTRY_AUTH=htpasswd -e REGISTRY_AUTH_HTPASSWD_REALM='Registry Realm' -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd quay.io/libpod/registry:2.8.2Anonymous access to the API should now fail:
curl --cacert /srv/podman-registry/certs/CA.crt https://registry.example.test:5000/v2/Sample output:
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}HTTP status 401 confirms authentication is enforced.
Log in and push to the private registry
Store credentials for Podman the same way you would for any registry — see Log in to a container registry for auth file locations and rootless vs rootful stores:
printf '%s' "$REGISTRY_PASSWORD" | podman login --username labuser --password-stdin registry.example.test:5000Sample output:
Login Succeeded!Push without a separate local tag by naming source and destination on one command — details in Push images to a registry:
podman push quay.io/podman/hello registry.example.test:5000/demo/hello:v1Sample output:
Getting image source signatures
Copying blob sha256:2114fc8b70586b9325dde6fd26066d9951414dcdfb3995f41d51d1995cf3bd9d
Copying config sha256:5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0
Writing manifest to image destinationTLS verification and registry authentication both succeeded — no insecure stanza and no --tls-verify=false.
Pull the image back and run it
Tag the remote name locally so you can remove it and prove pull works:
podman tag quay.io/podman/hello registry.example.test:5000/demo/hello:v1Remove the local copy of that reference:
podman rmi registry.example.test:5000/demo/hello:v1Pull from the private registry:
podman pull registry.example.test:5000/demo/hello:v1Sample output:
Trying to pull registry.example.test:5000/demo/hello:v1...
Getting image source signatures
Copying blob sha256:ce980a8f5545faa3125a489aad32c00d6cf13d80a302308c3963b524085657af
Copying config sha256:5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0
Writing manifest to image destination
5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0Run the pulled image:
podman run --rm registry.example.test:5000/demo/hello:v1Sample output:
!... Hello Podman World ...!That run completes the publish → store → retrieve loop on your own registry.
Verify the registry API catalog
List repositories with curl and credentials from the environment — not from a literal password on the command line:
curl --cacert /srv/podman-registry/certs/CA.crt -u "${REGISTRY_USER}:${REGISTRY_PASSWORD}" https://registry.example.test:5000/v2/_catalogSet REGISTRY_USER=labuser and reuse the same password variable from htpasswd setup. Sample output:
{"repositories":["demo/hello"]}The catalog shows the repository path you pushed. A successful Distribution registry catalog response contains a repositories array. Some registry products or reverse-proxy configurations may restrict or disable catalog access, so failure of _catalog alone does not prove that a push failed.
Rootless Podman and registry CA trust
System-wide trust under /etc/containers/certs.d/ applies to all users on the host — the simplest approach on a shared lab server. Rootless Podman can also read user-scoped CAs:
~/.config/containers/certs.d/registry.example.test:5000/ca.crtCopy the same ca.crt content there when only one unprivileged user needs the private registry and you cannot install system files. Login credentials remain per-user as described in the login guide — rootless podman login does not share root's auth.json.
Compare certs.d CA trust and insecure=true
CA in certs.d |
insecure=true in registries.conf |
|
|---|---|---|
| TLS verification | Stays enabled | Bypassed or HTTP allowed for that scope |
| Identity check | Server cert must chain to your CA | Registry treated as insecure transport |
| Typical use | Production and final lab TLS setup | Controlled HTTP lab or legacy exception |
| Configuration | HOST:PORT/ca.crt under certs.d |
[[registry]] stanza with location |
Prefer certs.d once the registry serves HTTPS with a certificate you control. Reserve insecure = true for the short HTTP learning path on localhost:5000.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
http: server gave HTTP response to HTTPS client |
Podman uses HTTPS; registry speaks HTTP only | Enable TLS on the registry, or mark that exact host:port insecure for lab only |
x509: certificate signed by unknown authority |
CA not installed for that endpoint | Copy CA to /etc/containers/certs.d/HOST:PORT/ca.crt — see Fix x509 certificate signed by unknown authority |
x509: certificate is valid for ..., not ... |
Hostname or SAN mismatch | Reissue certificate with DNS:registry.example.test (and IP SAN if needed) |
authentication required or unauthorized |
Missing or wrong login | podman login registry.example.test:5000 with a user from htpasswd |
Push works only with --tls-verify=false |
CA trust not wired for Podman | Install ca.crt under certs.d; do not keep the flag as the permanent fix |
| Login or pull times out behind a proxy | Corporate proxy intercepts local registry | Set NO_PROXY for registry.example.test, 127.0.0.1, and localhost |
Clean up the lab registry
Stop and remove the registry container:
podman rm -f tls-registryRemoving the container does not delete bind-mounted certificate, auth, or data directories on the host. Keep /srv/podman-registry/data if you mounted it for persistent blobs; delete it only when you intentionally discard stored images. Remove lab-only system changes when finished:
/etc/containers/registries.conf.d/local-registry.confif you no longer need the HTTP insecure stanza/etc/containers/certs.d/registry.example.test:5000/- The
registry.example.testline from/etc/hosts
References
- Podman registry configuration (certs.d) — per-registry CA layout
- Distribution registry configuration — TLS and
REGISTRY_AUTHenvironment variables - podman-login(1) — credential storage for registry operations
- Red Hat documentation — Building, running, and managing containers — RHEL container workflows including private registries
Summary
You can run a private registry beside Podman with quay.io/libpod/registry:2.8.2, prove the API with curl, and publish images once transport and auth match what Podman expects. The HTTP path on localhost:5000 plus an insecure = true stanza shows why plain registries need explicit configuration; the HTTPS path with a SAN certificate, certs.d CA trust, and htpasswd is the pattern to carry forward.
After podman login, push and pull use the same commands as any other registry — only the hostname, port, and trust files change. Capturing the remote digest, retries, and manifest push stay in the push guide; mirrors and full registries.conf behavior stay in the registries.conf article.
When something fails, read the error literally: HTTP vs HTTPS mismatch, unknown CA, SAN mismatch, and missing login each have distinct fixes. Install the CA under the exact HOST:PORT you reference in image names, log in before push, and drop lab insecure stanzas once TLS trust works.

