Configure a Private Container Registry for Podman

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
quay.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:

text
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 storage

Name resolution for the TLS phase:

text
127.0.0.1  registry.example.test

Add 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:

text
quay.io/libpod/registry:2.8.2

That avoids Docker Hub rate limits and matches what Podman upstream exercises in its own test suite.

Pull the pinned version:

bash
podman pull quay.io/libpod/registry:2.8.2

Sample output:

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
0030ba3d620c647159c935ee778991c68ef3e51a274703753b0bc530104ef5e5

The image ID at the end confirms the pull landed in local storage.


Start a simple HTTP registry (lab only)

IMPORTANT
The next section runs a registry without TLS. Use it only on an isolated lab host to learn the push path — not on a network others can reach.

Run the registry listening on port 5000:

bash
podman run -d --name local-registry -p 5000:5000 quay.io/libpod/registry:2.8.2

Podman prints the container ID when the detach succeeds. Confirm the Registry API responds over plain HTTP:

bash
curl http://127.0.0.1:5000/v2/

Sample output:

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:

bash
sudo tee /etc/containers/registries.conf.d/local-registry.conf << 'EOF'
[[registry]]
location = "localhost:5000"
insecure = true
EOF

insecure = 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:

bash
podman pull quay.io/podman/hello

Tag it for the insecure registry namespace:

bash
podman tag quay.io/podman/hello localhost:5000/demo/image:v1

Push to the HTTP registry:

bash
podman push localhost:5000/demo/image:v1

Sample output:

output
Getting image source signatures
Copying blob sha256:2114fc8b70586b9325dde6fd26066d9951414dcdfb3995f41d51d1995cf3bd9d
Copying config sha256:5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0
Writing manifest to image destination

The 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:

bash
podman rm -f local-registry

Create host directories for certificates, authentication, and optional persistent storage:

bash
sudo mkdir -p /srv/podman-registry/certs /srv/podman-registry/auth /srv/podman-registry/data

Add the TLS hostname to /etc/hosts on the lab host:

bash
grep -q 'registry.example.test' /etc/hosts || echo '127.0.0.1 registry.example.test' | sudo tee -a /etc/hosts

Modern 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:

bash
cd /srv/podman-registry/certs

Create a lab certificate authority:

bash
openssl genrsa -out CA.key 4096

Issue the CA certificate:

bash
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:

bash
openssl genrsa -out domain.key 2048

Create the CSR with registry.example.test as the common name:

bash
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:

bash
printf '%s\n' '[req]' 'distinguished_name=req' '[v3_req]' 'subjectAltName=DNS:registry.example.test' | sudo tee /srv/podman-registry/certs/san.cnf

Sign the server certificate with the CA:

bash
openssl x509 -req -days 365 -in domain.csr -CA CA.crt -CAkey CA.key -CAcreateserial -out domain.crt -extfile san.cnf -extensions v3_req

Confirm the SAN is present before you start the registry:

bash
openssl x509 -in domain.crt -noout -subject -issuer -ext subjectAltName

Sample output:

output
subject=CN=registry.example.test
issuer=CN=GoLinuxCloud Registry Lab CA
X509v3 Subject Alternative Name:
    DNS:registry.example.test

If 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:

bash
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.2

The -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:

bash
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:

bash
podman push quay.io/podman/hello registry.example.test:5000/demo/hello:v1

That 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:

bash
podman push --tls-verify=false \
  quay.io/podman/hello \
  registry.example.test:5000/demo/hello:v1

If 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:

text
/etc/containers/certs.d/registry.example.test:5000/ca.crt

Install the lab CA:

bash
sudo mkdir -p /etc/containers/certs.d/registry.example.test:5000

Copy the lab CA into that directory as ca.crt:

bash
sudo cp /srv/podman-registry/certs/CA.crt /etc/containers/certs.d/registry.example.test:5000/ca.crt

This 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:

bash
read -r -s REGISTRY_PASSWORD && printf '%s' "$REGISTRY_PASSWORD" | htpasswd -Bni labuser | sudo tee /srv/podman-registry/auth/htpasswd > /dev/null

Recreate the registry container with authentication environment variables:

bash
podman rm -f tls-registry

Start the registry again with TLS and htpasswd enabled:

bash
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.2

Anonymous access to the API should now fail:

bash
curl --cacert /srv/podman-registry/certs/CA.crt https://registry.example.test:5000/v2/

Sample output:

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:

bash
printf '%s' "$REGISTRY_PASSWORD" | podman login --username labuser --password-stdin registry.example.test:5000

Sample output:

output
Login Succeeded!

Push without a separate local tag by naming source and destination on one command — details in Push images to a registry:

bash
podman push quay.io/podman/hello registry.example.test:5000/demo/hello:v1

Sample output:

output
Getting image source signatures
Copying blob sha256:2114fc8b70586b9325dde6fd26066d9951414dcdfb3995f41d51d1995cf3bd9d
Copying config sha256:5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0
Writing manifest to image destination

TLS 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:

bash
podman tag quay.io/podman/hello registry.example.test:5000/demo/hello:v1

Remove the local copy of that reference:

bash
podman rmi registry.example.test:5000/demo/hello:v1

Pull from the private registry:

bash
podman pull registry.example.test:5000/demo/hello:v1

Sample output:

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
5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0

Run the pulled image:

bash
podman run --rm registry.example.test:5000/demo/hello:v1

Sample output:

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:

bash
curl --cacert /srv/podman-registry/certs/CA.crt -u "${REGISTRY_USER}:${REGISTRY_PASSWORD}" https://registry.example.test:5000/v2/_catalog

Set REGISTRY_USER=labuser and reuse the same password variable from htpasswd setup. Sample output:

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:

text
~/.config/containers/certs.d/registry.example.test:5000/ca.crt

Copy 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:

bash
podman rm -f tls-registry

Removing 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.conf if you no longer need the HTTP insecure stanza
  • /etc/containers/certs.d/registry.example.test:5000/
  • The registry.example.test line from /etc/hosts

References


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.


Frequently Asked Questions

1. Which registry container image should I use with Podman?

quay.io/libpod/registry matches the image Podman upstream tests against and avoids Docker Hub pull-rate limits. Pin a version tag such as 2.8.2 so upgrades do not change behavior mid-lab.

2. Where does Podman look for a private registry CA certificate?

Install the CA as ca.crt under /etc/containers/certs.d/HOST:PORT/ for system-wide trust, or under ~/.config/containers/certs.d/HOST:PORT/ for a single rootless user. The directory name must match the registry endpoint including a non-default port.

3. What is the difference between insecure in registries.conf and installing a CA in certs.d?

certs.d adds trust for a specific registry while TLS verification stays on — Podman checks the server certificate against your CA. insecure=true in registries.conf allows plain HTTP or skips verification for that registry scope and is appropriate only for controlled labs.

4. Why does podman push fail with http server gave HTTP response to HTTPS client?

Podman is speaking HTTPS to a registry that only serves HTTP. Either enable TLS on the registry and trust its CA, or mark that exact host:port as insecure in registries.conf for lab use only.

5. Do I need podman login for a private registry with htpasswd?

Yes when REGISTRY_AUTH is enabled. Anonymous curl to /v2/ returns 401 until credentials are supplied. Run podman login for the registry host before push or pull.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)