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¶
- System Requirements
- Getting the Project
- Configuration
- First Start
- GPG Configuration
- Changing the Admin Password
- Verifying the Installation
- Firewall Configuration
- Automated Backups
- Updating the Application
- 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 legacydocker-composecommand.
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
.debpackages inrepos/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:
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)¶
Edit .env with the actual public URL of your server:
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_URLandREACT_APP_API_URLmust match the URLs that are actually reachable by end-user browsers.
3.2 backend.env File (secrets and authentication)¶
Generate the JWT Secret Key¶
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:
Typical backend.env Content¶
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_KEYis required in production. The application will refuse to start if the default value is detected.
3.3 Secure the Configuration Files¶
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¶
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¶
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:
4.4 Check the API Health Endpoint¶
Expected response:
5. GPG Configuration¶
The APT repository must be signed with a GPG key so that apt clients accept the packages.
Option A: Via the Web Interface (recommended)¶
- Open
https://repo.example.com:3003in a browser - Log in with the admin credentials configured in
backend.env - Navigate to Settings > GPG
- Click Generate GPG Key
- Fill in the real name and email address
- 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: repod@example.com
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 repod@example.com > /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¶
- Log in with the password configured in
backend.env - Navigate to Profile > Change Password
- Enter the current password, then the new password (and its confirmation)
- 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¶
With UFW (recommended on Ubuntu)¶
# 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.mdfor 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¶
The script backs up the repos/ directory and configuration files.
Configure Retention¶
Scheduled Backups with Cron¶
Add the following line for a daily backup at 3:00 AM:
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¶
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.
Common causes:
JWT_SECRET_KEYis not set or uses the default value inbackend.env- Fix: generate a key with
openssl rand -hex 32and restart $characters not doubled to$$in the bcrypt hash inbackend.env- Fix: edit
backend.envand 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