Skip to content

Production Installation Guide — APT Repo Manager

This guide covers the complete installation of APT Repo Manager in a production environment on a Linux server (Debian/Ubuntu). Follow each step in order.


Table of Contents

  1. System Requirements
  2. Getting the Project
  3. Configuration
  4. First Start
  5. GPG Configuration
  6. Changing the Admin Password
  7. Verifying the Installation
  8. Firewall Configuration
  9. Automated Backups
  10. Updating the Application
  11. Troubleshooting Common Issues

1. System Requirements

Operating System

  • Debian 11/12 or Ubuntu 22.04/24.04 LTS (recommended)
  • 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

Important: Use the Docker Compose v2 plugin (docker compose) rather than the legacy docker-compose command.

Installing Docker (if not present)

# Remove any old versions
sudo apt-get remove -y docker docker-engine docker.io containerd runc

# Install dependencies
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# Add the official Docker repository
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

# Add current user to the docker group
sudo usermod -aG docker $USER
newgrp docker

Minimum Hardware Resources

Resource Minimum Recommended
CPU 2 vCPU 4 vCPU
RAM 2 GB 4 GB
Disk 20 GB 100 GB+
Network 100 Mb/s 1 Gb/s

Disk space must be sufficient to store .deb packages in repos/pool/. Provision accordingly based on your expected package volume.

Required Network Ports

Port Service Exposure
80 APT repository Public (apt clients)
3003 Web UI Public or VPN
8000 Backend API Reverse proxy with TLS

2. Getting the Project

Clone the Repository

# Recommended installation directory
sudo mkdir -p /opt/repod
sudo chown $USER:$USER /opt/repod

cd /opt/repod
git clone https://github.com/your-organisation/repod.git .

If deploying from an archive rather than Git:

tar -xzf repod-v2.0.0.tar.gz -C /opt/repod --strip-components=1

Verify the Project Structure

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

3. Configuration

3.1 .env File (frontend variables and ports)

cd /opt/repod
cp .env.example .env

Edit .env with the actual public URL of your server:

nano .env

Typical content — replace repo.example.com with your domain or IP address:

PUBLIC_URL=https://repo.example.com:3003
REACT_APP_API_URL=https://repo.example.com:8000
REACT_APP_REPO_URL=https://repo.example.com:80
FRONTEND_PORT=3003
APP_VERSION=v2.0.0

PUBLIC_URL and REACT_APP_API_URL must match the URLs that are actually reachable by end-user browsers.

3.2 backend.env File (secrets and authentication)

cp backend.env.example backend.env

Generate the JWT Secret Key

openssl rand -hex 32

Copy the output — it will be used as JWT_SECRET_KEY.

Generate the bcrypt Hash for the Admin Password

docker run --rm python:3.10-slim python3 -c \
  "from passlib.context import CryptContext; \
   print(CryptContext(schemes=['bcrypt']).hash('YourPassword!'))"

Replace YourPassword! with a password that meets the policy: - Minimum 8 characters - At least one uppercase letter - At least one digit or special character

Important: In backend.env, every $ character in a bcrypt hash must be doubled to $$. Example:

$2b$12$abc...  →  $$2b$$12$$abc...

Typical backend.env Content

nano backend.env
JWT_SECRET_KEY=<value generated by openssl rand -hex 32>
JWT_EXPIRE_MINUTES=60
ADMIN_USERNAME=admin
ADMIN_PASSWORD_HASH=$$2b$$12$$<rest of bcrypt hash>
CORS_ORIGINS=https://repo.example.com:3003
AUTH_RATELIMIT_PER_MINUTE=10

JWT_SECRET_KEY is required in production. The application will refuse to start if the default value is detected.

3.3 Secure the Configuration Files

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

3.4 Create the Volume Directory Structure

mkdir -p /opt/repod/repos/{audit,auth,clamav-db,gnupg,grype-db,imports,logs,manifests,package-index,pool,security,staging/incoming,staging/quarantine}

Directory descriptions:

Directory Contents
audit/ Audit logs in JSONL format (append-only)
auth/ Users SQLite database + reset tokens
clamav-db/ ClamAV antivirus definitions
gnupg/ GPG keyring (shared between services)
grype-db/ Grype CVE database cache
imports/ Temporary import cache
logs/ Nginx download logs
manifests/ Package JSON manifests
package-index/ APT SQLite index
pool/ .deb package files
security/ CVE decisions and API tokens (JSON)
staging/ Staging area (uploads and quarantine)

4. First Start

4.1 Build and Start the Services

cd /opt/repod
docker compose -f docker-compose.yaml up -d

Docker will download, build, and start three containers:

Container Role Port
frontend-ui React web interface 3003
backend-api FastAPI + business logic 8000
depot-apt APT Nginx server 80

4.2 Verify that All Containers are Running

docker compose ps

All services must show the state Up or running.

4.3 Monitor Logs on Startup

# Backend API logs
docker logs backend-api -f

# Frontend logs
docker logs frontend-ui -f

# APT server logs
docker logs depot-apt -f

Wait until the backend displays something similar to:

INFO:     Application startup complete.

4.4 Check the API Health Endpoint

curl http://localhost:8000/health/live

Expected response:

{"status": "ok"}


5. GPG Configuration

The APT repository must be signed with a GPG key so that apt clients accept the packages.

  1. Open https://repo.example.com:3003 in a browser
  2. Log in with the admin credentials configured in backend.env
  3. Navigate to Settings > GPG
  4. Click Generate GPG Key
  5. Fill in the real name and email address
  6. Click Generate

Option B: Via the Command Line

docker exec depot-apt gpg --homedir /repos/gnupg --no-default-keyring \
  --keyring /repos/gnupg/pubring.kbx --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: repod APT Repository
Name-Email: [email protected]
Expire-Date: 2y
%commit
EOF

Adjust Name-Real and Name-Email to match your organisation.

Export the GPG Public Key

The public key must be distributed to clients so they can verify package signatures:

# Export the public key from the container
docker exec depot-apt gpg --homedir /repos/gnupg \
  --armor --export [email protected] > /opt/repod/repos/pubkey.asc

# Verify the export
cat /opt/repod/repos/pubkey.asc

APT Client Configuration

Client machines must import the key and configure the repository:

# On each client machine
curl -fsSL https://repo.example.com/pubkey.asc | sudo gpg --dearmor \
  -o /etc/apt/keyrings/repod.gpg

echo "deb [signed-by=/etc/apt/keyrings/repod.gpg] \
  http://repo.example.com/ stable main" \
  | sudo tee /etc/apt/sources.list.d/repod.list

sudo apt-get update

6. Changing the Admin Password

It is strongly recommended to change the admin password immediately after the first login via the web interface.

Via the Web Interface

  1. Log in with the password configured in backend.env
  2. Navigate to Profile > Change Password
  3. Enter the current password, then the new password (and its confirmation)
  4. Click Save

Via the API

# Obtain a JWT token
TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"OldPassword!"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

# Change the password
curl -s -X POST http://localhost:8000/auth/change-password \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"old_password":"OldPassword!","new_password":"NewPassword!"}'

7. Verifying the Installation

Use this checklist after the initial installation.

Verification Checklist

# 1. All containers are running
docker compose ps

# 2. API health check
curl -s http://localhost:8000/health/live | python3 -m json.tool

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

# 4. APT server is reachable
curl -s -o /dev/null -w "%{http_code}" http://localhost:80
# Expected: 200 or 301

# 5. Swagger UI is disabled in production (must return 404)
curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/docs
# Expected: 404

# 6. Admin login works
curl -s -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"YourPassword!"}'
# Expected: JSON response containing access_token

# 7. ClamAV is operational
docker exec backend-api curl -s http://localhost:3310
# Expected: PONG response or ClamAV signature

# 8. GPG key is configured
docker exec depot-apt gpg --homedir /repos/gnupg --list-keys
# Expected: at least one key listed

Verify APT Repository Signing

# Check that the Release file is signed
curl -s http://localhost/dists/stable/InRelease | head -5
# Expected: PGP SIGNED MESSAGE block

Verify the Audit Trail

# Admin actions should be recorded
ls -la /opt/repod/repos/audit/
cat /opt/repod/repos/audit/$(date +%Y-%m-%d).jsonl | head -20

Audited events include: LOGIN, USER_CREATE, USER_DELETE, SETTINGS_CHANGE, PASSWORD_RESET.


8. Firewall Configuration

# Enable UFW if not already active
sudo ufw enable

# Allow SSH (do not lock yourself out!)
sudo ufw allow 22/tcp

# APT Repo Manager web interface
sudo ufw allow 3003/tcp

# APT repository (apt clients)
sudo ufw allow 80/tcp

# Port 8000 (backend API): DO NOT expose directly
# Use a TLS reverse proxy — see docs/REVERSE_PROXY.md

# Review the rules
sudo ufw status numbered

Security note: Port 8000 (backend API) must not be exposed directly to the internet. Place it behind a reverse proxy (Nginx, Caddy, Traefik) with TLS termination. See docs/REVERSE_PROXY.md for the TLS configuration.

With iptables

# Allow inbound traffic on port 3003 (UI)
sudo iptables -A INPUT -p tcp --dport 3003 -j ACCEPT

# Allow inbound traffic on port 80 (APT)
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT

# Persist the rules
sudo apt-get install -y iptables-persistent
sudo netfilter-persistent save

9. Automated Backups

Manual Backup

cd /opt/repod
./backup.sh

The script backs up the repos/ directory and configuration files.

Configure Retention

# Keep backups for 30 days
BACKUP_RETENTION_DAYS=30 ./backup.sh

Scheduled Backups with Cron

# Edit root's crontab
sudo crontab -e

Add the following line for a daily backup at 3:00 AM:

0 3 * * * cd /opt/repod && ./backup.sh >> /var/log/repod-backup.log 2>&1

Verify Backups

# List existing backups
ls -lh /opt/repod/backups/

# Review the backup log
tail -50 /var/log/repod-backup.log

Backing Up Secrets

The following files must be backed up separately in a secure vault:

# Critical files to back up off-server
/opt/repod/.env
/opt/repod/backend.env
/opt/repod/repos/gnupg/     # GPG private key
/opt/repod/repos/auth/      # User database

Warning: Losing the GPG private key (repos/gnupg/) makes it impossible to sign future APT repository releases. Back it up securely and offline.


10. Updating the Application

Standard Update Procedure

cd /opt/repod

# 1. Back up before updating
./backup.sh

# 2. Fetch the new version
git fetch origin
git pull origin main

# 3. Stop the services
docker compose -f docker-compose.yaml down

# 4. Rebuild images with the new version
docker compose -f docker-compose.yaml build --no-cache

# 5. Restart the services
docker compose -f docker-compose.yaml up -d

# 6. Verify the startup
docker compose ps
curl http://localhost:8000/health/live

Update Docker Image Only (without rebuilding)

cd /opt/repod

./backup.sh

docker compose -f docker-compose.yaml pull
docker compose -f docker-compose.yaml up -d

Check the Deployed Version

curl http://localhost:8000/health/live
# or via the interface: Settings > About

Rollback if Something Goes Wrong

cd /opt/repod

# Return to the previous version in git
git log --oneline -10
git checkout <previous-commit>

# Restart with the previous version
docker compose -f docker-compose.yaml down
docker compose -f docker-compose.yaml up -d --build

11. Troubleshooting Common Issues

The backend-api Container Does Not Start

Symptom: docker compose ps shows Exited for backend-api.

# View the error logs
docker logs backend-api --tail 50

Common causes:

  • JWT_SECRET_KEY is not set or uses the default value in backend.env
  • Fix: generate a key with openssl rand -hex 32 and restart
  • $ characters not doubled to $$ in the bcrypt hash in backend.env
  • Fix: edit backend.env and double all $ in the hash
  • Syntax error in backend.env
  • Fix: ensure there are no spaces around = signs

The Web Interface Shows an API Connection Error

Symptom: The frontend loads but shows an error when trying to log in.

# Check that the backend responds
curl http://localhost:8000/health/live

# Check for CORS errors
docker logs backend-api --tail 20 | grep -i cors

Common cause: CORS_ORIGINS in backend.env does not exactly match PUBLIC_URL in .env (protocol, port, and domain must all match).

APT Clients Reject the Repository (Signature Error)

Symptom: apt update fails with The following signatures couldn't be verified.

# Check that the GPG key is configured
docker exec depot-apt gpg --homedir /repos/gnupg --list-keys

# Check that the Release file is signed
curl http://localhost/dists/stable/InRelease | head -3

Fix: If no key is listed, generate a GPG key (see section 5) and then republish the repository via the web interface.

ClamAV Does Not Update

Symptom: Warnings in the logs about outdated ClamAV definitions.

# Manually update the definitions
docker exec backend-api freshclam

# Check the clamav-db volume
ls -la /opt/repod/repos/clamav-db/

Disk Space Is Insufficient

# Check available space
df -h /opt/repod/

# List the largest packages
du -sh /opt/repod/repos/pool/* | sort -rh | head -20

# Review quarantined packages
ls /opt/repod/repos/staging/quarantine/
# rm -f /opt/repod/repos/staging/quarantine/*  # remove manually after reviewing

Access Denied on Port 3003 or 80

# Check firewall rules
sudo ufw status

# Check that Docker is listening on the expected ports
ss -tlnp | grep -E '3003|8000|:80'

Reset the Admin Password

If the admin password is lost:

# Generate a new bcrypt hash
docker run --rm python:3.10-slim python3 -c \
  "from passlib.context import CryptContext; \
   print(CryptContext(schemes=['bcrypt']).hash('NewPassword!'))"

# Update backend.env with the new hash (double all $ characters)
nano /opt/repod/backend.env

# Restart the backend to apply the change
docker compose restart backend-api

View All Logs in Real Time

# All services at once
docker compose logs -f

# A single service
docker logs backend-api -f
docker logs frontend-ui -f
docker logs depot-apt -f

Appendix: Quick Reference Commands

# Start in production
docker compose -f docker-compose.yaml up -d

# Stop all services
docker compose -f docker-compose.yaml down

# Restart a specific service
docker compose restart backend-api

# View service status
docker compose ps

# Health check
curl http://localhost:8000/health/live

# Backup
cd /opt/repod && ./backup.sh

# View logs
docker logs backend-api -f
docker logs frontend-ui -f
docker logs depot-apt -f

# Open a shell inside a container
docker exec -it backend-api bash
docker exec -it depot-apt bash

This guide applies to APT Repo Manager v2.0.0 — Last updated: May 2026