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
sudoaccess
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)¶
Typical content β replace repo.example.com with your actual domain or IP:
# 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)¶
Generate a JWT secret key:
Generate the bcrypt hash for the admin password:
Dollar-sign escaping
In backend.env, every $ in a bcrypt hash must be doubled to $$.
Example: $2b$12$abcβ¦ β $$2b$$12$$abcβ¦
# ββ 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:
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¶
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:
Monitor startup logs:
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.
- Open
http://YOUR_HOST:3003in a browser - Log in with the admin credentials from
backend.env - Go to Settings β GPG
- Click Generate GPG Key
- 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¶
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 |