Self-hosted APT repository manager with built-in antivirus scanning, GPG signing, and a full web UI.
Free forever. No cloud dependency. Deploy in 5 minutes.
- What's included
- Requirements
- Quick start
- Configuration
- Distributions (multi-codename)
- GPG signing
- Upload a package
- Configure APT clients
- Import from an external APT repo
- REST API reference
- Ports & networking
- Upgrade to Enterprise
- Troubleshooting
- Contributing
- License
| Feature | Community | Enterprise |
|---|---|---|
| APT repository hosting (reprepro) | ✅ | ✅ |
| Web UI (React dashboard) | ✅ | ✅ |
| Package upload — REST API + drag & drop | ✅ | ✅ |
| Antivirus scan on every upload | ✅ | ✅ |
| GPG auto-signing | ✅ | ✅ |
| Multiple distributions (jammy / noble / focal / bookworm) | ✅ | ✅ |
| Local user management | ✅ | ✅ |
| Import from external APT mirror | ✅ | ✅ |
| Health & service monitoring | ✅ | ✅ |
| CVE analysis + CVSS scoring + CISO review queue | 🔒 | ✅ |
| Immutable audit trail (JSON export) | 🔒 | ✅ |
| RBAC — 5 roles + per-distribution scoping | 🔒 | ✅ |
| LDAP / Active Directory integration | 🔒 | ✅ |
| SBOM export (SPDX · CycloneDX) | 🔒 | ✅ |
| Download analytics | 🔒 | ✅ |
| Email & webhook notifications | 🔒 | ✅ |
| NIS2 Article 21 compliance mode | 🔒 | ✅ |
| Priority support & SLA | 🔒 | ✅ |
| Dependency | Minimum version |
|---|---|
| Docker | 24+ |
| Docker Compose | v2.20+ (plugin, not standalone) |
| RAM | 1 GB free |
| Disk | 2 GB free (+ package storage) |
| OS | Linux x86-64 (amd64) or arm64 |
Note — Internet access is required on first start to download the ClamAV signature database (~300 MB).
After that, Repod works fully air-gapped.
# 1. Clone
git clone https://github.com/getautoflow/repod-community.git
cd repod-community
# 2. Copy and edit the configuration files
cp .env.example .env
cp backend.env.example backend.env
# 3. Generate a JWT secret and set a strong admin password (see Configuration below)
# At minimum, change JWT_SECRET_KEY in backend.env
# 4. Start
docker compose up -d
# 5. Wait ~30 seconds for the antivirus engine to initialise, then open:
# http://localhost:3103 → Web UI (admin / changeme)
# http://localhost:8100 → REST API
# http://localhost:8180 → APT repo (add to sources.list)First login — default credentials are
admin / changeme.
Change the password immediately in Settings → Security or via the API.
# Ports exposed on the host
APT_PORT=8180 # APT repo (nginx)
BACKEND_PORT=8100 # FastAPI backend
FRONTEND_PORT=3103 # React web UI
# URL the frontend JavaScript will use to reach the API
REACT_APP_API_URL=http://localhost:8100
# URL used in APT sources.list instructions shown in the UI
REACT_APP_REPO_URL=http://localhost:8180If you run multiple instances or already occupy port 80, change the ports here — no rebuild needed.
| Variable | Required | Description |
|---|---|---|
JWT_SECRET_KEY |
✅ | Random secret for JWT signing. Generate with python3 -c "import secrets; print(secrets.token_hex(32))" |
JWT_EXPIRE_MINUTES |
Token lifetime in minutes (default: 60) |
|
ADMIN_USERNAME |
Initial admin username (default: admin) |
|
ADMIN_PASSWORD_HASH |
✅ | bcrypt hash of the admin password — escape every $ as $$ |
CORS_ORIGINS |
Comma-separated allowed origins for the API | |
AUTH_RATELIMIT_PER_MINUTE |
Login attempts per minute per IP (default: 10) |
python3 -c "
from passlib.hash import bcrypt
import getpass
pw = getpass.getpass('Password: ')
h = bcrypt.hash(pw)
# Escape $ for Docker Compose env_file
print(h.replace('\$', '\$\$'))
"Copy the output into ADMIN_PASSWORD_HASH= in backend.env.
Repod supports four distributions out of the box:
| Codename | OS | Notes |
|---|---|---|
jammy |
Ubuntu 22.04 LTS | |
noble |
Ubuntu 24.04 LTS | |
focal |
Ubuntu 20.04 LTS | |
bookworm |
Debian 12 | Initialized at first start |
After the first docker compose up -d, the bookworm distribution is automatically initialized by the apt-repo container. To initialize the remaining distributions (jammy, noble, focal), click "Init distributions" in the web UI under Distributions, or call the API:
curl -s -X POST http://localhost:8100/distributions/init \
-H "Authorization: Bearer $TOKEN"This writes the reprepro configuration and runs reprepro export for all codenames.
Repod generates a GPG key pair on first start inside the apt-repo container. The public key is exported to:
http://localhost:8180/repos/depot.gpg
# Download and add the key
curl -fsSL http://your-repod-host/repos/depot.gpg | sudo gpg --dearmor -o /usr/share/keyrings/repod.gpg
# Then reference it in your sources.list entry (see next section)- Remove
repos/.gnupg/andrepos/gnupg/on the host. - Restart the
apt-repocontainer — a new key is generated automatically. - Re-export the public key to all clients.
Go to Importer in the sidebar → drag & drop your .deb file → select a target distribution → click Import.
Every uploaded file is automatically scanned by ClamAV before being added to the repository.
# 1. Get a token
TOKEN=$(curl -s -X POST http://localhost:8100/auth/token \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"changeme"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# 2. Upload a .deb (JSON response)
curl -s -X POST http://localhost:8100/upload/ \
-H "Authorization: Bearer $TOKEN" \
-F "file=@./mypackage_1.0_amd64.deb" \
-F "distribution=jammy"
# 2b. Upload with live progress (SSE stream)
curl -s -X POST http://localhost:8100/upload/stream \
-H "Authorization: Bearer $TOKEN" \
-F "file=@./mypackage_1.0_amd64.deb" \
-F "distribution=jammy"amd64, arm64, i386
# /etc/apt/sources.list.d/repod.list
deb [arch=amd64 signed-by=/usr/share/keyrings/repod.gpg] http://your-repod-host/repos jammy mainReplace jammy with your target distribution codename.
sudo apt update
sudo apt install your-packageGo to Importer → Import depuis un dépôt externe in the web UI, or use the API.
# Search available packages in indexed sources
curl -s "http://localhost:8100/import/search?q=nginx" \
-H "Authorization: Bearer $TOKEN"
# Resolve dependencies for a package
curl -s "http://localhost:8100/import/resolve/curl" \
-H "Authorization: Bearer $TOKEN"curl -s -X POST http://localhost:8100/import/fetch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"package": "curl", "distribution": "jammy"}'curl -s -X POST http://localhost:8100/import/batch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"packages": ["curl", "wget", "jq"], "distribution": "jammy"}'Both import endpoints return a Server-Sent Events stream with real-time progress. Imported packages pass through the same antivirus scan as uploaded packages before being added to the pool.
Interactive documentation is available at:
http://localhost:8100/docs (Swagger UI — development mode only)
http://localhost:8100/redoc (ReDoc)
| Method | Path | Description |
|---|---|---|
POST |
/auth/token |
Login — returns JWT ({"username":…,"password":…}) |
GET |
/auth/me |
Current user info |
GET |
/auth/users |
List users (admin only) |
POST |
/auth/users |
Create user (admin only) |
PATCH |
/auth/users/{username} |
Update user role (admin only) |
DELETE |
/auth/users/{username} |
Delete user (admin only) |
POST |
/auth/change-password |
Change own password |
GET |
/packages/ |
List all packages |
POST |
/upload/ |
Upload a .deb — JSON response |
POST |
/upload/stream |
Upload a .deb — SSE live progress |
DELETE |
/artifacts/{name} |
Remove a package (all versions, maintainer+) |
DELETE |
/artifacts/{name}/{version} |
Remove a specific version (maintainer+) |
GET |
/distributions/ |
List distributions + package counts |
GET |
/distributions/{codename}/packages |
Packages in a distribution |
POST |
/distributions/promote |
Promote package to another distribution |
POST |
/distributions/init |
Initialize / re-initialize reprepro |
GET |
/import/search?q= |
Search indexed package sources |
GET |
/import/resolve/{name} |
Resolve dependencies online |
POST |
/import/fetch |
Import a single package (SSE stream) |
POST |
/import/batch |
Import up to 50 packages (SSE stream) |
POST |
/import/sync |
Re-sync all package index sources |
GET |
/import/sync-status |
Sync status of indexed sources |
GET |
/dashboard/stats |
Dashboard stats |
GET |
/dashboard/history |
Upload history (last N days) |
GET |
/health |
Full health check (no auth) |
GET |
/health/live |
Liveness probe (no auth) |
GET |
/health/ready |
Readiness probe (no auth) |
GET |
/security/clamav/status |
Antivirus engine status & DB version |
POST |
/security/clamav/update |
Force antivirus DB update (admin only) |
Endpoints that return HTTP 402 are Enterprise-only features.
| Service | Container port | Default host port | Purpose |
|---|---|---|---|
apt-repo (nginx) |
80 | 8180 | APT repository served to apt clients |
backend-api (FastAPI) |
8000 | 8100 | REST API + business logic |
frontend-ui (nginx/React) |
80 | 3103 | Web dashboard |
All three containers communicate over the internal Docker bridge. Only the ports listed above are exposed to the host.
Edit .env to use different ports:
APT_PORT=9180
BACKEND_PORT=9100
FRONTEND_PORT=3203
REACT_APP_API_URL=http://your-host:9100
REACT_APP_REPO_URL=http://your-host:9180The Community Edition is designed to grow with you. When you need:
- CVE analysis + CVSS scoring on every upload
- CISO approval workflow before promotion to production
- Immutable audit trail for NIS2 / ISO 27001
- RBAC with per-distribution scoping
- LDAP / Active Directory
- SBOM export (SPDX · CycloneDX)
- Email & webhook alerts
- Priority support and SLA
👉 Request a demo
📧 contact@getautoflow.dev
Migration from Community to Enterprise is in-place — same Docker Compose stack, same data, same ports. No data migration needed.
Normal on first boot — ClamAV downloads its signature database (~300 MB). Subsequent starts use the cached database in repos/clamav-db/.
docker logs backend-api --follow | grep -i clamThe backend-api container must be running with the repos/conf, repos/dists, and repos/db volumes. Check:
docker inspect backend-api | python3 -c "
import sys, json
mounts = json.load(sys.stdin)[0]['Mounts']
for m in mounts:
print(m['Source'], '->', m['Destination'])
"If the volumes are missing, re-create the container:
docker compose up -d backendThe backend runs as appuser (UID 1000). If you pulled repos/ from a backup as root, fix permissions:
sudo chown -R 1000:1000 repos/conf repos/dists repos/db repos/pool repos/manifestsIf you changed ADMIN_PASSWORD_HASH in backend.env, the running container still has the old value. Restart:
docker compose up -d backendIf the hash uses bare $ instead of $$, Docker Compose will silently strip the value. Re-generate the hash and escape every $ as $$.
ClamAV daemon may not be ready yet. Check:
curl http://localhost:8100/security/clamav/statusIf daemon_running is false, wait 30–60 seconds and retry.
The GPG public key is not trusted on the client. Run:
curl -fsSL http://your-repod-host/repos/depot.gpg | sudo gpg --dearmor -o /usr/share/keyrings/repod.gpgThen make sure your sources.list entry includes signed-by=/usr/share/keyrings/repod.gpg.
Issues and pull requests are welcome.
# Dev mode (hot-reload, backend code mounted as volume)
docker compose -f docker-compose.yaml -f docker-compose.dev.yml upPlease open an issue before submitting a large pull request.
Copyright © 2026 NGANDO ARMEL — Getautoflow.
Community Edition source code is released under the MIT License — see LICENSE.
Enterprise Edition source code is proprietary.