Skip to content

Production Deployment

Step-by-step guide for deploying Repod on a production Linux server.


System requirements

Operating system

  • Debian 11/12 or Ubuntu 22.04/24.04 LTS (recommended for APT edition)
  • AlmaLinux 9 or RHEL 9 (recommended for RPM edition)
  • Root or sudo access

Required software

Software Minimum version Check command
Docker Engine 24.0 docker --version
Docker Compose 2.20 (plugin) docker compose version
Git 2.x git --version
OpenSSL 1.1+ openssl version

Use the v2 Compose plugin

Use docker compose (v2 plugin), not the legacy docker-compose command.

Install Docker (if not present)

sudo apt-get remove -y docker docker-engine docker.io containerd runc
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER
newgrp docker
sudo dnf remove -y docker docker-engine docker.io

sudo dnf install -y dnf-plugins-core
sudo dnf config-manager \
  --add-repo https://download.docker.com/linux/rhel/docker-ce.repo

sudo dnf install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker

Hardware requirements

Resource Minimum Recommended
CPU 2 vCPU 4 vCPU
RAM 2 GB 4 GB
Disk 20 GB 100 GB+

Disk space must accommodate all package binaries under repos/pool/. Provision according to your expected package volume.


Network ports

Port Service Exposure
80 APT repository (Nginx) LAN or public (apt clients)
3003 Web interface Internal or VPN
8000 Backend API Reverse proxy only β€” never expose directly
Port Service Exposure
80 RPM repository (Nginx) LAN or public (dnf/zypper clients)
3003 Web interface Internal or VPN
8000 Backend API Reverse proxy only β€” never expose directly

Step 1 β€” Clone the repository

sudo mkdir -p /opt/repod
sudo chown $USER:$USER /opt/repod
cd /opt/repod

# APT edition
git clone https://github.com/getautoflow/repod.git .

# RPM edition
git clone https://github.com/getautoflow/repod-rpm.git .

Verify the project structure:

ls /opt/repod
# Expected: backend/ frontend/ repos/ docker-compose.yaml .env.example backend.env.example

Step 2 β€” Configure environment variables

.env (ports and build-time frontend URLs)

cp .env.example .env
nano .env

Typical content β€” replace repo.example.com with your actual domain or IP:

.env
# Bind to loopback when a reverse proxy handles external traffic
BIND_HOST=127.0.0.1

# Public URLs embedded into the frontend bundle at build time
REACT_APP_API_URL=https://repo.example.com
REACT_APP_REPO_URL=http://repo.example.com

# Port mapping
BACKEND_PORT=8000
FRONTEND_PORT=3003
APT_PORT=80

backend.env (secrets and runtime config)

cp backend.env.example backend.env

Generate a JWT secret key:

openssl rand -hex 32

Generate the bcrypt hash for the admin password:

python3 -c "from passlib.hash import bcrypt; print(bcrypt.hash('YourPassword!'))"

Dollar-sign escaping

In backend.env, every $ in a bcrypt hash must be doubled to $$. Example: $2b$12$abc… β†’ $$2b$$12$$abc…

backend.env
# ── Security ──────────────────────────────────────────────────────────────────
JWT_SECRET_KEY=<output of openssl rand -hex 32>
JWT_EXPIRE_MINUTES=60
ADMIN_USERNAME=admin
ADMIN_PASSWORD_HASH=$$2b$$12$$<rest of bcrypt hash>

# ── Environment ───────────────────────────────────────────────────────────────
ENV=production
APP_VERSION=v1.2.0

# ── Reverse proxy ─────────────────────────────────────────────────────────────
TRUSTED_PROXIES=127.0.0.1
CORS_ORIGINS=https://repo.example.com

Secure the files:

chmod 600 /opt/repod/.env
chmod 600 /opt/repod/backend.env

Step 3 β€” Create the data volume structure

mkdir -p /opt/repod/repos/{audit,auth,clamav-db,conf,db,dists,gnupg,grype-db,\
imports,logs,manifests,package-index,pool,security,staging/incoming,staging/quarantine}
Directory Contents
audit/ Append-only JSONL audit logs (one file per day)
auth/ Users SQLite database
clamav-db/ ClamAV signature database (~800 MB)
conf/ reprepro distribution configuration
db/ reprepro internal database
dists/ APT distribution trees (served by Nginx)
gnupg/ GPG keyring shared between backend and APT Nginx
grype-db/ Grype CVE database cache
imports/ Working directory for imported packages
logs/ Nginx download logs (parsed for statistics)
manifests/ Per-package JSON manifests and central index.json
pool/ .deb package binaries
security/ CVE decisions, CISA KEV and EPSS caches
staging/ Upload landing zone and quarantine
mkdir -p /opt/repod/repos/{audit,auth,clamav-db,gnupg,grype-db,\
imports,logs,manifests,package-index,security,staging/incoming,staging/quarantine}

# RPM distribution directories
for dist in almalinux8 almalinux9 rocky8 rocky9 centos-stream9 fedora opensuse-leap-15.6; do
  mkdir -p /opt/repod/repos/$dist/x86_64
done
Directory Contents
audit/ Append-only JSONL audit logs
auth/ Users SQLite database
clamav-db/ ClamAV signature database
gnupg/ GPG keyring
grype-db/ Grype CVE database cache
almalinux9/x86_64/ RPM packages + repodata/ index

Step 4 β€” Build and start the services

cd /opt/repod
docker compose up -d --build

Docker builds and starts three containers:

Container Role Port
frontend-ui React web interface + Nginx 3003
backend-api FastAPI, security pipeline 8000
depot-apt APT repository Nginx 80
Container Role Port
frontend-ui React web interface + Nginx 3003
backend-api FastAPI, security pipeline 8000
depot-rpm RPM repository Nginx 80

Verify all containers are running:

docker compose ps

Monitor startup logs:

docker compose logs -f backend-api
# Wait for: INFO:     Application startup complete.

ClamAV startup time

ClamAV loads ~800 MB of signatures on first start. The health endpoint may report "clamav": false for up to 2 minutes. This is normal.


Step 5 β€” Configure the GPG signing key

The repository must be signed for APT/RPM clients to verify packages.

  1. Open http://YOUR_HOST:3003 in a browser
  2. Log in with the admin credentials from backend.env
  3. Go to Settings β†’ GPG
  4. Click Generate GPG Key
  5. Fill in the real name and email address, then click Generate
# APT edition
docker exec backend-api gpg --homedir /repos/gnupg \
  --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: Repod Repository
Name-Email: [email protected]
Expire-Date: 2y
%commit
EOF

# Initialize distributions after key generation
TOKEN=$(curl -s -X POST http://localhost:8000/auth/token \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"YourPassword!"}' | jq -r .access_token)

curl -X POST http://localhost:8000/api/v1/distributions/init \
  -H "Authorization: Bearer $TOKEN"

Step 6 β€” Verify the installation

# 1. All containers running
docker compose ps

# 2. API liveness probe
curl -s http://localhost:8000/health/live
# Expected: {"status":"ok"}

# 3. API full health check (includes ClamAV and Grype status)
TOKEN=$(curl -s -X POST http://localhost:8000/auth/token \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"YourPassword!"}' | jq -r .access_token)
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/health | jq .

# 4. Web interface reachable
curl -s -o /dev/null -w "%{http_code}" http://localhost:3003
# Expected: 200

# 5. Repository server reachable
curl -s -o /dev/null -w "%{http_code}" http://localhost:80
# Expected: 200

# 6. Swagger UI disabled in production
curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/docs
# Expected: 404

Step 7 β€” Configure the firewall

sudo ufw enable
sudo ufw allow 22/tcp          # SSH β€” do not lock yourself out
sudo ufw allow 3003/tcp        # Web interface
sudo ufw allow 80/tcp          # Repository (apt/dnf/zypper clients)
# Port 8000 β€” do NOT open publicly; use a reverse proxy
sudo ufw status numbered
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-port=3003/tcp   # Web interface
sudo firewall-cmd --permanent --add-port=80/tcp     # Repository
# Port 8000 β€” do NOT open; use a reverse proxy
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Never expose port 8000 directly

The backend API (port 8000) must be accessible only through a TLS reverse proxy. Direct public exposure transmits credentials and tokens in plaintext. See Reverse proxy β†’


Step 8 β€” Enable systemd auto-start

cat > /etc/systemd/system/repod.service << 'EOF'
[Unit]
Description=Repod Package Repository Manager
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/repod
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=300

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable repod
sudo systemctl start repod

Post-deployment checklist

Step Command / File Status
JWT secret key configured JWT_SECRET_KEY in backend.env
Admin password hash set ADMIN_PASSWORD_HASH in backend.env
BIND_HOST restricted BIND_HOST=127.0.0.1 in .env
CORS origins set CORS_ORIGINS=https://… in backend.env
ENV=production ENV=production in backend.env
GPG key generated Settings β†’ GPG in the web UI
Firewall configured Port 8000 not exposed publicly
Reverse proxy with TLS See reverse proxy guide
Automated backups See backup guide
Systemd service enabled systemctl enable repod

Next steps