Skip to content

TLS / HTTPS

NORA serves plain HTTP by design. TLS termination is handled by a reverse proxy.

This is a deliberate architectural decision:

  • Single responsibility — NORA manages artifacts, not certificates. Embedding TLS means bundling Let’s Encrypt clients, certificate renewal, ACME challenges, and custom CA support — all of which already exist in battle-tested tools.
  • Operational simplicity — One place for certificates (reverse proxy), not scattered across every service. When a cert expires, you fix it in one config.
  • Industry standard — Docker Hub, GitHub Container Registry, AWS ECR, Harbor, Nexus — none terminate TLS in the registry process. A reverse proxy in front is the universal pattern.
  • Zero-config on internal networks — On trusted networks (lab, CI/CD), NORA works out of the box without certificate management.
Client (docker push) → Reverse Proxy (HTTPS :443) → NORA (HTTP :4000)
Section titled “Caddy (recommended — auto Let’s Encrypt)”
registry.example.com {
reverse_proxy localhost:4000
}

That’s it. Caddy handles certificate issuance and renewal automatically.

server {
listen 443 ssl;
server_name registry.example.com;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
# Required for large image pushes
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
docker-compose.yml
services:
traefik:
image: traefik:v3
command:
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.tlschallenge=true
- --certificatesresolvers.le.acme.email=admin@example.com
ports:
- 443:443
nora:
image: ghcr.io/getnora-io/nora:latest
labels:
- traefik.http.routers.nora.rule=Host(\`registry.example.com\`)
- traefik.http.routers.nora.tls.certresolver=le
- traefik.http.services.nora.loadbalancer.server.port=4000

If you run NORA without TLS on a private network, configure Docker clients to trust it.

Edit /etc/docker/daemon.json on every Docker client that needs to push/pull:

{
insecure-registries: [192.168.1.100:4000]
}

Then restart Docker:

Terminal window
sudo systemctl restart docker

If you use a DNS name (e.g. nora.internal:4000), add that instead:

{
insecure-registries: [nora.internal:4000]
}

Warning: insecure-registries disables TLS certificate verification for that host. Use only on trusted networks where traffic between client and registry is not exposed.

For K8s nodes using containerd, edit /etc/containerd/config.toml:

[plugins.io.containerd.grpc.v1.cri.registry.configs.nora.internal:4000.tls]
insecure_skip_verify = true

Then restart containerd:

Terminal window
sudo systemctl restart containerd

If your organization uses a private CA (e.g., FreeIPA, internal PKI):

Terminal window
# Copy CA cert
sudo mkdir -p /etc/docker/certs.d/registry.example.com:4000/
sudo cp ca.crt /etc/docker/certs.d/registry.example.com:4000/ca.crt
# No Docker restart needed
Terminal window
sudo cp ca.crt /usr/local/share/ca-certificates/my-ca.crt
sudo update-ca-certificates
Terminal window
sudo cp ca.crt /etc/pki/ca-trust/source/anchors/my-ca.crt
sudo update-ca-trust