Skip to content

vincentvdk/Orbit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Zot Registry Logo

Zot Registry on Hetzner Cloud

Automated deployment of a zot OCI registry on Hetzner Cloud with object storage backend.

Overview

This repository contains automation to deploy a self-configuring zot container registry on Hetzner Cloud infrastructure. The setup includes:

  • Zot container running via Podman
  • Systemd service for automatic startup and management
  • Podman auto-update for automatic container updates
  • Hetzner Object Storage as the storage backend (S3-compatible)
  • Caddy reverse proxy for public access
  • Automatic HTTPS with Let's Encrypt (zero-config SSL/TLS)
  • Cloud-init based bootstrap for zero-touch deployment
  • Git-based configuration sync with age encryption for secrets

Repository Structure

.
├── config/                          # Configuration files
│   ├── Caddyfile                    # Caddy reverse proxy config
│   ├── zot-config.json              # Zot registry configuration (template)
│   └── zot.env.example              # Environment configuration template
├── scripts/                         # Shell scripts
│   ├── setup.sh                     # Interactive deployment wizard
│   ├── zot-sync-config.sh           # Git-based config sync script
│   └── age-helper.sh                # Age encryption helper
├── systemd/                         # Systemd unit files
│   ├── caddy.service                # Caddy service
│   ├── zot.service                  # Zot registry service
│   ├── zot-config-sync.service      # Config sync service
│   └── zot-config-sync.timer        # Config sync timer
├── cloud-init.yaml                  # Base cloud-init template
├── cloud-init-custom.yaml           # Generated custom cloud-init
└── README.md                        # This file

Architecture

Internet → Caddy (80/443) → Zot Container (5000) → Hetzner Object Storage

The registry is exposed publicly through Caddy, which acts as a reverse proxy with automatic HTTPS. Authentication is handled via htpasswd, and all image data is stored in Hetzner Object Storage.

Prerequisites

SSH Key

You'll need an SSH public key to access the server. If you don't have one:

# Generate a new ED25519 key (recommended, secure)
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519_zot -C "zot-registry-admin"

# Breakdown:
#   -t ed25519    Modern, secure algorithm
#   -a 100        100 KDF rounds (slows brute force)
#   -f ~/.ssh/... Custom output filename
#   -C "..."      Comment to identify key

# Or RSA 4096-bit if ED25519 is not supported
ssh-keygen -t rsa -b 4096 -a 100 -f ~/.ssh/id_rsa_zot -C "zot-registry-admin"

# Your keys will be at:
# Private: ~/.ssh/id_ed25519_zot
# Public:  ~/.ssh/id_ed25519_zot.pub (use this with scripts/setup.sh)

Hetzner Cloud Account

  1. Create a project in Hetzner Cloud Console
  2. Generate an API token (optional, for CLI deployment)

Hetzner Object Storage

  1. Create an Object Storage bucket:

    • Go to Storage → Object Storage in Hetzner Cloud Console
    • Choose your region - available regions:
      • fsn1 - Falkenstein, Germany
      • nbg1 - Nuremberg, Germany
      • hel1 - Helsinki, Finland
    • Create a new bucket (e.g., my-registry-storage)
    • Note: Deploying your server in the same region is recommended for optimal performance and lower latency
  2. Generate S3 credentials:

    • Click on your bucket
    • Go to "S3 Keys" section
    • Create new credentials
    • Save the Access Key ID and Secret Access Key

Domain Name (Required)

HTTPS is required for the web UI authentication to work properly.

  • Register a domain name (e.g., cr.yourdomain.com)
  • Point an A record to your server's IP address
  • Caddy will automatically obtain and manage SSL certificates from Let's Encrypt

Quick Start

Option 1: Deploy via Hetzner Cloud Console

  1. Configure DNS (do this first):

    • Create an A record pointing your domain (e.g., cr.yourdomain.com) to your future server IP
    • Or prepare to update DNS immediately after server creation
  2. Prepare your configuration:

    • Run ./scripts/setup.sh to generate cloud-init-custom.yaml with your domain, SSH key, and credentials
    • The script will prompt you for your domain name and automatically configure HTTPS
    • Or manually edit cloud-init.yaml and replace :80 with your domain in the Caddyfile section
  3. Create a new server:

    • Go to Hetzner Cloud Console
    • Click "Add Server"
    • Select location (recommended: same region as Object Storage bucket for optimal performance - fsn1, nbg1, or hel1)
    • Choose Debian 13 (Trixie)
    • Select server type (CX23 recommended, 2 vCPU / 4GB RAM)
  4. Configure Cloud-Init:

    • Scroll to "Cloud config" section
    • Paste contents of cloud-init-custom.yaml (generated by scripts/setup.sh)
  5. Create the server:

    • Click "Create & Buy now"
    • Note the server IP address
    • Update your DNS A record if you haven't already
    • Wait 2-3 minutes for the server to boot and configure
  6. Access your registry:

    • Wait for DNS propagation: dig +short cr.yourdomain.com
    • Open https://cr.yourdomain.com in your browser
    • Caddy will automatically obtain an SSL certificate (may take 30-60 seconds)
    • Login with: admin / changeme
    • Change the default password immediately!

Option 2: Deploy via Hetzner CLI

# Install hcloud CLI
brew install hcloud  # macOS
# or
snap install hcloud  # Linux

# Authenticate
hcloud context create my-project

# Generate customized cloud-init with your domain and credentials
scripts/setup.sh

# Create server with cloud-init
# Recommended: Use same location as Object Storage bucket for optimal performance
hcloud server create \
  --name zot-registry \
  --type cx23 \
  --image debian-13 \
  --location fsn1 \
  --user-data-from-file cloud-init-custom.yaml

# Note the server IP and update your DNS A record
# Access your registry at https://your-domain.com once DNS propagates

Post-Deployment Configuration

1. Update Environment Configuration

If you didn't modify the cloud-init file before deployment:

ssh root@YOUR_SERVER_IP

# Edit environment configuration
nano /etc/zot/zot.env

# Update with your actual values:
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
BUCKET_NAME=my-registry-storage
BUCKET_REGION=eu-central
BUCKET_ENDPOINT=fsn1.your-objectstorage.com

# Restart service (config.json is auto-generated from template with these values)
systemctl restart zot.service

2. Change Default Password

# Generate new password
htpasswd -Bbn newuser newpassword > /etc/zot/htpasswd

# Restart service
systemctl restart zot.service

3. Adding users

It is adviced to encrypt this file using age. See documentation below on how to do this.

htpasswd -bB htpasswd newuser newpassword

4. Set Up Automatic HTTPS (Recommended)

Caddy automatically obtains and renews SSL/TLS certificates from Let's Encrypt!

# Edit Caddyfile
nano /etc/caddy/Caddyfile

# Replace :80 with your domain name:
# Change:  :80 {
# To:      your-registry.example.com {

# Or uncomment the example section and modify it

# Restart Caddy
systemctl restart caddy

# That's it! Caddy automatically:
# - Obtains Let's Encrypt certificate
# - Configures HTTPS
# - Redirects HTTP to HTTPS
# - Renews certificates before expiration

Check certificate status:

# View Caddy logs
journalctl -u caddy -f

# Check if HTTPS is working
curl https://your-registry.example.com/v2/

5. Enable Git-Based Configuration Sync (Optional)

The instance includes a pull-based configuration management system that automatically syncs configs from a git repository every 5 minutes. Sensitive files can be encrypted with age for secure storage in git.

Quick Start with Helper Script:

This repository includes scripts/age-helper.sh to simplify age encryption:

# Generate key pair (on local machine or server)
scripts/age-helper.sh generate

# Encrypt a file
scripts/age-helper.sh encrypt zot.env age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

# Decrypt a file (for testing)
scripts/age-helper.sh decrypt zot.env.age /path/to/age-key.txt

Manual Setup:

  1. On the server - Generate an age encryption key:
ssh root@YOUR_SERVER_IP

# Generate age key pair
age-keygen -o /etc/zot/age-key.txt

# Secure the private key
chmod 600 /etc/zot/age-key.txt

# Display the public key (save this for encrypting files)

grep "# public key:" /etc/zot/age-key.txt
# Example output: # public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
  1. On your local machine - Create a git repository for your configuration files:
# Install age on your local machine
# macOS: brew install age
# Linux: apt install age / dnf install age
# Windows: Download from https://github.com/FiloSottile/age/releases

mkdir zot-config
cd zot-config
git init

# Add non-sensitive configuration files (plaintext)
cp /path/to/Caddyfile .
cp /path/to/zot-config.json .

# Encrypt sensitive files with age (use the public key from step 1)
AGE_PUBLIC_KEY="age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"

# Encrypt environment configuration
age -r $AGE_PUBLIC_KEY -o zot.env.age /path/to/zot.env

# Encrypt htpasswd (optional)
age -r $AGE_PUBLIC_KEY -o htpasswd.age /path/to/htpasswd

# Add files to git
git add Caddyfile zot-config.json zot.env.age htpasswd.age

# Create .gitignore to prevent committing plaintext secrets
cat > .gitignore << EOF
zot.env
htpasswd
*.key
*.pem
EOF

git add .gitignore
git commit -m "Initial configuration with encrypted secrets"
git remote add origin https://github.com/yourusername/zot-config.git
git push -u origin main
  1. Set up git authentication on the server:

The server needs authentication to pull from your repository. Choose one method:

Option A: SSH Deploy Key (Recommended for private repos)

ssh root@YOUR_SERVER_IP

# Generate SSH key for git sync (no passphrase for automated pulling)
ssh-keygen -t ed25519 -f /root/.ssh/zot-config-deploy -N "" -C "zot-config-sync"

# Display the public key
cat /root/.ssh/zot-config-deploy.pub
# Copy this output

Now add this as a deploy key to your repository:

  • GitHub: Settings → Deploy keys → Add deploy key
  • GitLab: Settings → Repository → Deploy keys → Add key
  • Gitea/Forgejo: Settings → Deploy Keys → Add Deploy Key

Configure git to use this key:

# Create SSH config for the deploy key
cat > /root/.ssh/config << EOF
Host github.com
  IdentityFile /root/.ssh/zot-config-deploy
  StrictHostKeyChecking accept-new

Host gitlab.com
  IdentityFile /root/.ssh/zot-config-deploy
  StrictHostKeyChecking accept-new
EOF

chmod 600 /root/.ssh/config

# Edit the config sync environment file
nano /etc/zot/zot-config-sync.env

# Use SSH URL (not HTTPS):
CONFIG_REPO_URL=ssh://git@github.com/yourusername/zot-config.git
CONFIG_BRANCH=main

Option B: HTTPS with Personal Access Token (Alternative)

Create a personal access token in your git provider:

  • GitHub: Settings → Developer settings → Personal access tokens → Generate new token
    • Scope: repo (or just public_repo for public repos)
  • GitLab: User Settings → Access Tokens → Add new token
    • Scope: read_repository
ssh root@YOUR_SERVER_IP

# Edit the config sync environment file
nano /etc/zot/zot-config-sync.env

# Use token in URL:
CONFIG_REPO_URL=https://YOUR_TOKEN@github.com/yourusername/zot-config.git
CONFIG_BRANCH=main

# Or for GitHub with username:
CONFIG_REPO_URL=https://yourusername:YOUR_TOKEN@github.com/yourusername/zot-config.git
CONFIG_BRANCH=main

Option C: Public Repository (Not recommended for configs)

ssh root@YOUR_SERVER_IP

nano /etc/zot/zot-config-sync.env

# Use HTTPS URL (no auth needed for public repos):
CONFIG_REPO_URL=https://github.com/yourusername/zot-config.git
CONFIG_BRANCH=main

⚠️ Warning: Only use public repositories if ALL sensitive files are encrypted with age!


  1. Enable sync and verify:
# Restart the timer to apply changes
systemctl restart zot-config-sync.timer

# Check timer status
systemctl status zot-config-sync.timer

# Test the connection by manually triggering sync
systemctl start zot-config-sync.service

# View sync logs (look for "Configuration sync completed successfully")
journalctl -u zot-config-sync -n 50

# For live monitoring:
journalctl -u zot-config-sync -f

Common authentication issues:

  • Permission denied (publickey): Deploy key not added to repository or SSH config incorrect
  • Authentication failed: Wrong token or token doesn't have correct permissions
  • Could not resolve host: Check network connectivity and DNS
  • Repository not found: Check repository URL and access permissions

Usage Workflow:

# On your local machine:
cd zot-config

# Update non-sensitive files (plaintext)
nano Caddyfile
git add Caddyfile
git commit -m "Update reverse proxy settings"
git push origin main

# Update sensitive files (encrypted)
# 1. Edit the plaintext file
nano zot.env

# 2. Re-encrypt with age (use same public key)
AGE_PUBLIC_KEY="age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
age -r $AGE_PUBLIC_KEY -o zot.env.age zot.env

# 3. Commit and push the encrypted version
git add zot.env.age
git commit -m "Update environment configuration"
git push origin main

# The instance will automatically:
# 1. Pull changes within 5 minutes
# 2. Detect which files changed (.age or plaintext)
# 3. Decrypt .age files using the server's private key
# 4. Apply configs to /etc/caddy or /etc/zot
# 5. Reload/restart affected services

Monitored Files:

  • Caddyfile/etc/caddy/Caddyfile (validates before applying, reloads Caddy)
  • zot-config.json/etc/zot/config.json.template (template for env var substitution, restarts zot service)
  • zot.env or zot.env.age/etc/zot/zot.env (decrypts if .age, contains all config including bucket settings, restarts zot service)
  • htpasswd or htpasswd.age/etc/zot/htpasswd (decrypts if .age, restarts zot service)

Security Notes:

  • age encryption is built-in - use .age extension for encrypted files (recommended for zot.env and htpasswd)
  • The server's private key is stored at /etc/zot/age-key.txt (protected with 600 permissions)
  • Only the server can decrypt .age files - they're safe to commit to git
  • Use a private repository for configuration storage, or public with encrypted secrets
  • For public repos, use deploy keys with read-only access
  • The sync script validates Caddyfile syntax before applying changes
  • Never commit plaintext zot.env - always use .age encrypted versions

Usage

Push an Image

# Log in to your registry
podman login your-domain.com
# or
docker login your-domain.com

# Tag an image
podman tag myimage:latest your-domain.com/myimage:latest

# Push the image
podman push your-domain.com/myimage:latest

Pull an Image

podman pull your-domain.com/myimage:latest

Management

Check Service Status

systemctl status zot.service

View Logs

# Service logs
journalctl -u zot.service -f

# Application logs
tail -f /var/log/zot/zot.log

# Audit logs
tail -f /var/log/zot/audit.log

Restart Service

systemctl restart zot.service

Podman Auto-Update

The container is configured to automatically check for updates daily at midnight using podman's auto-update feature.

# Check auto-update timer status
systemctl status podman-auto-update.timer

# View last auto-update run
systemctl status podman-auto-update.service

# Manually check for updates (dry-run)
podman auto-update --dry-run

# Manually trigger update
podman auto-update

# View auto-update logs
journalctl -u podman-auto-update.service

The container has the io.containers.autoupdate=registry label, which means podman will:

  1. Check the container registry for image updates
  2. Pull the new image if available
  3. Restart the container with the new image
  4. Rollback to the previous version if the restart fails

Configuration Sync Management

If you enabled git-based configuration sync, you can manage it with these commands:

# Check sync timer status
systemctl status zot-config-sync.timer

# View sync logs (live)
journalctl -u zot-config-sync -f

# View recent sync history
journalctl -u zot-config-sync -n 50

# Manually trigger sync (useful for testing)
systemctl start zot-config-sync.service

# Disable automatic sync
systemctl stop zot-config-sync.timer
systemctl disable zot-config-sync.timer

# Re-enable automatic sync
systemctl enable zot-config-sync.timer
systemctl start zot-config-sync.timer

# Change sync repository
nano /etc/zot/zot-config-sync.env
systemctl restart zot-config-sync.timer

Sync behavior:

  • Runs every 5 minutes automatically
  • Only applies changes if git repository has updates
  • Validates Caddyfile syntax before applying
  • Automatically reloads/restarts affected services
  • Logs all actions to journald

Update Caddy

To update Caddy to the latest version:

# Download latest binary
curl -L "https://github.com/caddyserver/caddy/releases/latest/download/caddy_linux_amd64.tar.gz" -o /tmp/caddy.tar.gz

# Extract and replace
tar -xzf /tmp/caddy.tar.gz -C /tmp
mv /tmp/caddy /usr/local/bin/caddy
chmod +x /usr/local/bin/caddy
rm /tmp/caddy.tar.gz

# Restart Caddy
systemctl restart caddy

# Verify version
/usr/local/bin/caddy version

Or use the built-in upgrade command (requires existing Caddy installation):

caddy upgrade
systemctl restart caddy

Configuration Files

All configuration templates are located in the config/ directory, and systemd units in systemd/.

config/zot-config.json

Template configuration file for zot with S3 storage backend using environment variable placeholders. Key sections:

  • storage.storageDriver: S3 backend configuration (uses ${BUCKET_NAME}, ${BUCKET_REGION}, ${BUCKET_ENDPOINT} from zot.env)
  • http.auth: Authentication settings (htpasswd)
  • http.accessControl: Repository access policies
  • log: Logging configuration

This file is deployed as /etc/zot/config.json.template and processed with envsubst at service start to generate the final /etc/zot/config.json.

config/zot.env.example

Environment configuration template containing all deployment-specific settings:

  • AWS S3 credentials: Access key and secret key for Object Storage
  • Bucket configuration: Bucket name, region, and endpoint
  • Variables are substituted into config.json at runtime
  • Copy to zot.env and customize for your deployment
  • Encrypt with age before committing to git

systemd/zot.service

Systemd service unit file that:

  • Manages the zot Podman container lifecycle
  • Automatically restarts on failure
  • Uses Type=notify for proper systemd integration
  • Runs envsubst to generate config.json from template with environment variables
  • Configures container with auto-update label
  • Loads all configuration from /etc/zot/zot.env
  • Includes SELinux labels (Z) for volume mounts

config/Caddyfile

Single source of truth for Caddy configuration.

This file:

  • Configures reverse proxy to zot container
  • Handles automatic HTTPS with Let's Encrypt
  • Sets proper headers for container registry
  • Configures unlimited request buffers for large uploads
  • Sets appropriate timeouts for image operations
  • Enables access logging

Important: This file is deployed via cloud-init's write_files section. Edit this file and regenerate cloud-init with scripts/setup.sh to apply changes.

systemd/caddy.service

Systemd service unit for Caddy:

  • Runs Caddy as caddy user/group
  • Type=notify for proper systemd integration
  • Automatic reload support
  • Security hardening (ProtectSystem, PrivateTmp)
  • CAP_NET_BIND_SERVICE capability for binding to privileged ports

cloud-init.yaml

Bootstrap script that:

  • Uses write_files to deploy Caddyfile from repository
  • Downloads Caddy and age binaries from official GitHub releases
  • Installs Podman and utilities
  • Creates Caddy user and directory structure
  • Configures zot and systemd services
  • Enables podman auto-update timer
  • Enables git-based configuration sync timer
  • Sets up firewall rules
  • Starts all services

Note: The Caddyfile content comes from the separate config/Caddyfile file via write_files section.

scripts/zot-sync-config.sh

Git-based configuration sync script that:

  • Pulls configuration updates from a git repository every 5 minutes
  • Detects which files have changed
  • Decrypts age-encrypted files (.age extension) using server's private key
  • Validates Caddyfile syntax before applying
  • Copies updated configs to appropriate locations
  • Automatically reloads/restarts affected services
  • Logs all sync operations to journald

Configuration:

  • Set CONFIG_REPO_URL in /etc/zot/zot-config-sync.env
  • Age private key stored at /etc/zot/age-key.txt (auto-generated or manual)
  • Supports both plaintext and .age encrypted files (encrypted preferred for secrets)

systemd/zot-config-sync.service

Systemd service unit for configuration sync:

  • Type=oneshot for one-time execution per trigger
  • Loads git repository URL from environment file
  • Runs the sync script with proper permissions
  • Security hardening with ProtectSystem and ReadWritePaths

systemd/zot-config-sync.timer

Systemd timer for periodic config sync:

  • Triggers every 5 minutes (configurable)
  • Runs 1 minute after boot
  • Persistent across reboots
  • Enables pull-based GitOps workflow

Security Considerations

  1. Change default passwords immediately after deployment
  2. Use SSL/TLS for production deployments
  3. Restrict firewall rules to necessary ports only
  4. Secure S3 credentials - never commit them to git
  5. Automatic updates - podman auto-update keeps zot container current
  6. System updates - keep OS and packages updated with unattended-upgrades
  7. Access control - configure appropriate user permissions in config.json
  8. Network security - use Hetzner Cloud Firewalls for additional protection
  9. Caddy and age binaries - downloaded directly from official GitHub releases (no third-party repos)
  10. Minimal dependencies - fewer packages means smaller attack surface
  11. Git configuration sync - use private repos or deploy keys; age encryption built-in for secrets (.age files)

Troubleshooting

Service won't start

# Check service status
systemctl status zot.service

# Check Podman logs
podman logs zot

# Check container status
podman ps -a

# Verify S3 credentials and bucket config
cat /etc/zot/zot.env

# Test S3 connectivity
podman run --rm \
  -e AWS_ACCESS_KEY_ID=your_key \
  -e AWS_SECRET_ACCESS_KEY=your_secret \
  amazon/aws-cli \
  s3 ls --endpoint-url=https://your-bucket.fsn1.your-objectstorage.com

Can't connect to registry

# Check if Caddy is running
systemctl status caddy

# Check Caddy logs
journalctl -u caddy -n 50

# Check if zot container is running
podman ps | grep zot

# Test local connectivity to zot
curl http://localhost:5000/v2/

# Test through Caddy
curl http://localhost:80/v2/

# Check firewall
ufw status

# Validate Caddyfile syntax
caddy validate --config /etc/caddy/Caddyfile

Auto-update not working

# Check timer is active
systemctl status podman-auto-update.timer

# Check last run
journalctl -u podman-auto-update.service -n 50

# Verify container label
podman inspect zot | grep autoupdate

# Manually trigger update
podman auto-update

HTTPS/Certificate issues

# Check Caddy logs for certificate errors
journalctl -u caddy -n 100 | grep -i certificate

# Verify domain points to your server
dig +short your-domain.com

# Check if ports are accessible
curl -v http://your-domain.com
curl -v https://your-domain.com

# Force certificate renewal
caddy reload --config /etc/caddy/Caddyfile

Common issues:

  • DNS not pointing to server IP (wait for DNS propagation)
  • Port 80/443 blocked by firewall
  • Domain validation failing (ensure ports are open)
  • Rate limits from Let's Encrypt (use staging environment for testing)

S3 connection errors

  • Verify bucket name and region are correct
  • Check that S3 credentials have proper permissions
  • Ensure bucket endpoint URL matches your region
  • Test credentials with aws-cli or s3cmd

Configuration sync not working

# Check if timer is running
systemctl status zot-config-sync.timer

# Check sync service logs
journalctl -u zot-config-sync -n 100

# Verify git repository is configured
cat /etc/zot/zot-config-sync.env

# Test git connectivity
cd /opt/zot-config
git fetch origin

# Manually trigger sync to see errors
systemctl start zot-config-sync.service
journalctl -u zot-config-sync -f

Common issues:

  • CONFIG_REPO_URL not set in /etc/zot/zot-config-sync.env
  • Git repository requires authentication (use deploy keys or HTTPS with tokens)
  • Network connectivity issues (check firewall, DNS)
  • Repository is empty or missing expected config files
  • File permissions preventing config file updates
  • Caddyfile syntax errors (validation will fail and skip update)
  • Age decryption failures (wrong key, corrupted .age file, age not installed)
  • Missing age key at /etc/zot/age-key.txt

Age decryption troubleshooting:

# Verify age is installed
age --version

# Check if age key exists
ls -la /etc/zot/age-key.txt

# View the public key
grep "# public key:" /etc/zot/age-key.txt

# Test decryption manually
age -d -i /etc/zot/age-key.txt /opt/zot-config/zot.env.age

# Re-encrypt file if needed (on local machine)
AGE_PUBLIC_KEY="age1..." # Get from server
age -r $AGE_PUBLIC_KEY -o zot.env.age zot.env

# Verify encrypted file format
file /opt/zot-config/zot.env.age
# Should show: ASCII text (age-encrypted data)

Setting up authentication for private repos:

For HTTPS with token:

# Edit the environment file
nano /etc/zot/zot-config-sync.env

# Use token in URL
CONFIG_REPO_URL=https://YOUR_TOKEN@github.com/yourusername/zot-config.git

For SSH deploy keys:

# Generate SSH key on server
ssh-keygen -t ed25519 -f /root/.ssh/zot-config-deploy -N ""

# Add public key to your git repository as a deploy key
cat /root/.ssh/zot-config-deploy.pub

# Configure git to use the key
cat > /root/.ssh/config << EOF
Host github.com
  IdentityFile /root/.ssh/zot-config-deploy
  StrictHostKeyChecking accept-new
EOF

# Use SSH URL in config
nano /etc/zot/zot-config-sync.env
CONFIG_REPO_URL=ssh://git@github.com/yourusername/zot-config.git

Cost Estimation

Example monthly costs for Hetzner:

  • CX23 server (2 vCPU, 4GB RAM): ~€6.39/month
  • Object Storage: €0.0049/GB/month + €0.01/GB transfer
  • Traffic: 20TB included with server

Total: ~€7-12/month for small to medium usage

Advanced Configuration

Custom Storage Backend

The default configuration uses Hetzner Object Storage:

{
  "storage": {
    "storageDriver": {
      "name": "s3",
      "region": "eu-central",
      "bucket": "my-registry-storage",
      "regionendpoint": "fsn1.your-objectstorage.com"
    }
  }
}

Available Hetzner regions:

  • fsn1.your-objectstorage.com - Falkenstein, Germany
  • nbg1.your-objectstorage.com - Nuremberg, Germany
  • hel1.your-objectstorage.com - Helsinki, Finland

For other S3-compatible storage (e.g., AWS S3):

{
  "storage": {
    "storageDriver": {
      "name": "s3",
      "region": "us-east-1",
      "bucket": "my-bucket",
      "regionendpoint": "s3.amazonaws.com"
    }
  }
}

High Availability Setup

For production deployments:

  1. Use multiple zot instances behind a load balancer
  2. Share the same S3 bucket across instances
  3. Use Hetzner Load Balancer for distribution
  4. Configure health checks on /v2/ endpoint

Monitoring

Add monitoring with Prometheus:

{
  "extensions": {
    "metrics": {
      "enable": true,
      "prometheus": {
        "path": "/metrics"
      }
    }
  }
}

Resources

License

This deployment automation is provided as-is for use with the zot registry project.

Support

For issues with:

About

Automated deployment of a Zot OCI registry on Hetzner Cloud with object storage backend.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors

Languages