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;
}
# API endpoint
location /api/ {
proxy_pass http://localhost:4000/;
}
# 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:

Full production stack with Caddy:

version: '3.8'
services:
nora:
image: ghcr.io/getnora-io/nora:latest
container_name: nora
restart: unless-stopped
ports:
- "127.0.0.1:4000:4000" # NORA (Registry + API + UI)
volumes:
- nora-data:/data
environment:
# Storage
NORA_STORAGE_PATH: /data
# Rate Limits (tuned for production)
NORA_RATE_LIMIT_UPLOAD_RPS: 2000
NORA_RATE_LIMIT_UPLOAD_BURST: 5000
NORA_RATE_LIMIT_GENERAL_RPS: 1000
NORA_RATE_LIMIT_GENERAL_BURST: 2000
# Logging
NORA_LOG_LEVEL: info
NORA_LOG_FORMAT: json
# Metrics
NORA_METRICS_ENABLED: "true"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/health"]
interval: 30s
timeout: 10s
retries: 3
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./certs:/etc/ssl/certs:ro
- ./private:/etc/ssl/private:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- nora
volumes:
nora-data:
driver: local
caddy-data:
caddy-config:

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 Container Registry
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=nora
Group=nora
WorkingDirectory=/opt/nora
ExecStart=/usr/local/bin/nora serve \
--storage-path /var/lib/nora \
--rate-limit-upload-rps 2000 \
--rate-limit-upload-burst 5000
Restart=on-failure
RestartSec=10
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/nora
[Install]
WantedBy=multi-user.target

Enable and start:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable nora
sudo systemctl start nora
sudo systemctl status nora

Health check endpoint:

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

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: 500
NORA_RATE_LIMIT_GENERAL_BURST=2000 # Default: 1000

Monitor /metrics for nora_rate_limit_hits_total 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_TYPE=s3
NORA_S3_BUCKET=nora-registry
NORA_S3_REGION=us-east-1
NORA_S3_ENDPOINT=https://s3.amazonaws.com

  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 Configuration/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