Skip to content

Production Deployment Guide

This guide covers deploying NORA in production environments with HTTPS/TLS support using reverse proxies.

Caddy provides automatic HTTPS with Let’s Encrypt and simple configuration.

Caddyfile:

nora.example.com {
reverse_proxy localhost:4000 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
}

With custom certificate:

{
auto_https disable_redirects
}
nora.example.com:443 {
tls /etc/ssl/certs/nora.crt /etc/ssl/private/nora.key
reverse_proxy localhost:4000 {
header_up Host {host}
header_up X-Real-IP {remote}
}
log {
output file /var/log/caddy/nora-access.log
}
}

Start Caddy:

Terminal window
caddy run --config /etc/caddy/Caddyfile

nginx.conf:

upstream nora_backend {
server localhost:4000;
}
server {
listen 443 ssl http2;
server_name nora.example.com;
ssl_certificate /etc/ssl/certs/nora.crt;
ssl_certificate_key /etc/ssl/private/nora.key;
# SSL hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Proxy settings
location / {
proxy_pass http://nora_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts for large image uploads
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# Metrics endpoint
location /metrics {
proxy_pass http://localhost:4000/metrics;
allow 10.0.0.0/8; # Restrict to internal network
deny all;
}
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name nora.example.com;
return 301 https://$host$request_uri;
}

docker-compose.yml with Traefik:

version: '3.8'
services:
nora:
image: ghcr.io/getnora-io/nora:latest
restart: unless-stopped
volumes:
- nora-data:/data
environment:
NORA_STORAGE_PATH: /data
labels:
- "traefik.enable=true"
- "traefik.http.routers.nora.rule=Host(`nora.example.com`)"
- "traefik.http.routers.nora.entrypoints=websecure"
- "traefik.http.routers.nora.tls.certresolver=letsencrypt"
- "traefik.http.services.nora.loadbalancer.server.port=4000"
traefik:
image: traefik:v2.10
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik-certs:/letsencrypt
command:
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
volumes:
nora-data:
traefik-certs:

Hardened production stack:

services:
nora:
image: ghcr.io/getnora-io/nora:0.9.0
container_name: nora
restart: unless-stopped
read_only: true
tmpfs:
- /tmp:size=256M
ports:
- "127.0.0.1:4000:4000"
volumes:
- nora-data:/data
- ./config.toml:/etc/nora/config.toml:ro
environment:
NORA_HOST: "0.0.0.0"
NORA_PORT: "4000"
NORA_STORAGE_PATH: /data
NORA_CONFIG_PATH: /etc/nora/config.toml
NORA_PUBLIC_URL: ${NORA_PUBLIC_URL}
NORA_AUTH_ENABLED: "true"
RUST_LOG: "info"
deploy:
resources:
limits:
memory: 512M
cpus: "2"
reservations:
memory: 128M
cpus: "0.25"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/health"]
interval: 15s
timeout: 5s
start_period: 10s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
security_opt:
- no-new-privileges:true
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
nora:
condition: service_healthy
volumes:
nora-data:
caddy-data:
caddy-config:

Key hardening points:

  • read_only: true — filesystem is immutable except for mounted volumes
  • no-new-privileges — prevents privilege escalation
  • tmpfs for /tmp — no persistent temp files on disk
  • Resource limits prevent runaway memory usage
  • Healthcheck with start_period gives time for startup
  • Log rotation with max-size and max-file

Start:

Terminal window
NORA_PUBLIC_URL=https://registry.example.com docker compose up -d

Caddy handles this automatically. Just use hostname in Caddyfile:

nora.example.com {
reverse_proxy localhost:4000
}

1. Generate certificate request:

Terminal window
openssl req -new -newkey rsa:2048 -nodes \
-keyout nora.key \
-out nora.csr \
-subj "/CN=nora.example.com/O=MyOrg/C=US"

2. Sign with your CA and install:

Terminal window
# Copy signed certificate and key
cp nora.crt /etc/ssl/certs/
cp nora.key /etc/ssl/private/
chmod 600 /etc/ssl/private/nora.key
# Add CA certificate to system trust store
cp ca.crt /usr/local/share/ca-certificates/
update-ca-certificates

3. Configure reverse proxy with custom cert (see examples above)


On client machines, add CA certificate:

Terminal window
# Linux
sudo cp ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
sudo systemctl restart docker
# macOS
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain ca.crt

Verify access:

Terminal window
docker login nora.example.com
docker pull nora.example.com/myapp:latest

Add CA to all nodes:

Terminal window
# On each node
sudo cp ca.crt /usr/local/share/ca-certificates/nora-ca.crt
sudo update-ca-certificates
sudo systemctl restart containerd

Update image references in manifests:

containers:
- name: myapp
image: nora.example.com/myapp:latest

Create /etc/systemd/system/nora.service:

[Unit]
Description=NORA Artifact Registry
Documentation=https://getnora.dev
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=nora
Group=nora
ExecStart=/usr/local/bin/nora serve
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
# Environment
Environment=RUST_LOG=info
Environment=NORA_HOST=0.0.0.0
Environment=NORA_PORT=4000
Environment=NORA_STORAGE_PATH=/var/lib/nora
Environment=NORA_CONFIG_PATH=/etc/nora/config.toml
EnvironmentFile=-/etc/nora/nora.env
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/nora
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
# Resource limits
LimitNOFILE=65535
LimitNPROC=4096
[Install]
WantedBy=multi-user.target

Install and start:

Terminal window
# Create service user
useradd --system --no-create-home --shell /sbin/nologin nora
mkdir -p /var/lib/nora /etc/nora
chown nora:nora /var/lib/nora
# Install binary
cp target/release/nora /usr/local/bin/
chmod 755 /usr/local/bin/nora
# Enable and start
systemctl daemon-reload
systemctl enable --now nora

NORA supports hot-reloading the curation policy without restarting the server. Send SIGHUP to reload config.toml and apply updated curation rules instantly:

Terminal window
# systemd
systemctl reload nora
# Docker
docker kill --signal=HUP nora
# Manual
kill -HUP $(pidof nora)
SettingHot-reloadedRequires restart
Curation mode (off/audit/enforce)Yes
Curation allowlist/blocklistYes
Curation bypass tokenYes
Internal namespacesYes
Min release ageYes
Server host/portYes
Storage settingsYes
Auth providers (OIDC)Yes
Docker upstreamsYes

If the reloaded config is invalid (TOML parse error, missing required fields), NORA logs a warning and keeps the previous configuration. The server never crashes due to a bad reload:

WARN Curation policy reload failed, keeping previous config error="TOML parse error at line 12"
  1. Edit /etc/nora/config.toml (e.g., add a package to blocklist)
  2. Reload: systemctl reload nora
  3. Verify: journalctl -u nora --since "1 min ago" | grep reload

Health check endpoint:

Terminal window
curl https://nora.example.com/health
# Expected: {"status":"healthy"}

Metrics endpoint (Prometheus):

Terminal window
curl http://localhost:4000/metrics

See Monitoring Guide for Prometheus/Grafana setup.


For heavy CI/CD environments, increase rate limits:

Terminal window
NORA_RATE_LIMIT_UPLOAD_RPS=2000 # Default: 200
NORA_RATE_LIMIT_UPLOAD_BURST=5000 # Default: 500
NORA_RATE_LIMIT_GENERAL_RPS=1000 # Default: 100
NORA_RATE_LIMIT_GENERAL_BURST=2000 # Default: 200

Monitor /metrics for rate limit indicators to tune appropriately.

Local storage:

  • Use fast SSD for /data
  • Regular cleanup of old tags
  • Monitor disk usage

S3-compatible storage:

Terminal window
NORA_STORAGE_MODE=s3
NORA_STORAGE_S3_URL=https://s3.eu-central-1.amazonaws.com
NORA_STORAGE_BUCKET=nora-registry
NORA_STORAGE_S3_REGION=eu-central-1

See S3 Storage for full setup with MinIO, RustFS, or AWS S3.


  1. Always use HTTPS in production - Docker/containerd require secure registries
  2. Restrict metrics endpoint - Use firewall or reverse proxy rules
  3. Enable authentication - See Authentication
  4. Keep CA certificates updated on all client machines
  5. Monitor for unauthorized access via logs and metrics
  6. Regular backups of /data directory

Problem: “x509: certificate signed by unknown authority”

Solution: Install CA certificate on client machines (see Client Configuration above)

Problem: Rate limit errors

Solution: Increase rate limits via environment variables and restart NORA

Problem: Slow uploads

Solution: Check reverse proxy timeouts, increase if needed