Skip to content

Getting Started

Get Repod up and running and upload your first package in under 10 minutes.

Prerequisites

  • Docker 24+
  • Docker Compose v2 plugin
  • Linux or macOS (Windows: use WSL2)
  • 2 GB RAM minimum (ClamAV loads ~800 MB of signatures on startup)

Step 1 — Clone and configure

git clone https://github.com/getautoflow/repod
cd repod
cp .env.example .env
cp backend.env.example backend.env

.env controls Docker Compose (ports, public URLs, POSTGRES_PASSWORD). backend.env controls the FastAPI backend (secrets, database connection).

Open backend.env and set the required secrets — generate each with openssl rand -hex 32:

backend.env
DATABASE_URL=postgresql://repod:CHANGE_ME@db:5432/repod
JWT_SECRET_KEY=your-64-char-hex-secret-here
SETTINGS_ENCRYPTION_KEY=another-64-char-hex-secret-here
WEBHOOK_SECRET=another-64-char-hex-secret-here

If you change POSTGRES_PASSWORD in .env, update the password in DATABASE_URL to match — both files feed the same db (PostgreSQL 16) container.

No admin account is created at this stage — you'll create it through the setup wizard in Step 4, on first access to the web interface.

Pre-provisioning an admin (optional, for automated deployments)

If you'd rather not use the setup wizard (e.g. scripted/CI deployments), you can pre-provision the first admin account by setting both:

backend.env
ADMIN_USERNAME=admin
ADMIN_PASSWORD_HASH=$$2b$$12$$...

Generate the bcrypt hash with:

docker run --rm python:3.12-slim python -c \
  "from passlib.hash import bcrypt; print(bcrypt.hash('MyPassword1!'))"

Escape $ signs in .env files

bcrypt hashes start with $2b$. In any *.env file read by Docker Compose, every $ must be escaped as $$ to prevent variable interpolation. If ADMIN_PASSWORD_HASH is missing, empty, or not a valid bcrypt hash, no admin row is seeded and the setup wizard remains available — this is the safe default.

Protecting the setup wizard (optional)

Between container startup and the creation of the first admin, anyone who can reach the backend on the network could call POST /api/v1/setup. To close this window, set:

backend.env
SETUP_TOKEN=your-64-char-hex-secret-here

When set, POST /api/v1/setup requires a matching X-Setup-Token header (see Step 4). GET /api/v1/setup/status remains public either way.


Step 2 — Choose your package formats (REPO_FORMAT)

Repod is one backend that can serve .deb, .rpm, and Alpine .apk packages. The REPO_FORMAT environment variable (set on the backend service in docker-compose.yaml) controls which formats are active:

REPO_FORMAT Serves Repository tooling
apt (default) .deb reprepro
rpm .rpm createrepo_c
apk .apk apk index
both .deb + .rpm reprepro + createrepo_c
all .deb + .rpm + .apk reprepro + createrepo_c + apk index

The bundled docker-compose.yaml ships with REPO_FORMAT: all and starts two repository Nginx containers (depot-apt for .deb/.apk, depot-rpm for .rpm) plus one backend and one frontend. If you only need one format, change REPO_FORMAT and remove the unused repository service from docker-compose.yaml (or leave it running — it's harmless, just unused).

Standalone RPM-only stack

docker-compose.rpm.yml is a separate, self-contained stack with its own PostgreSQL database, network, and container names, pre-configured with REPO_FORMAT=rpm. Run it with docker compose -f docker-compose.rpm.yml up -d — see Production Deployment for when to use it.

Maven, PyPI, npm, and container images are always on

REPO_FORMAT only controls the OS-package side (.deb/.rpm/.apk). A Maven repository, a PyPI index, an npm registry, and a Docker/OCI registry are available on every Repod instance regardless of this setting, going through the exact same scan-before-publish pipeline. See Package formats and Client setup once you've finished this guide.


Step 3 — Start the stack

docker compose up -d

By default (REPO_FORMAT=all), five containers start:

Container Role Default port
repod-db PostgreSQL 16 — application database (internal)
depot-apt Nginx — serves .deb (APT) and .apk (Alpine) repositories :80
depot-rpm Nginx — serves .rpm repositories (YUM/DNF/Zypper) :8080
backend-api FastAPI — REST API + security pipeline :8000
frontend-ui React — web interface :3003

Watch the startup logs:

docker compose logs -f backend-api

Wait for:

INFO:     Application startup complete.

First startup is slower

ClamAV loads its signature database (~800 MB) on first start. This takes 20–60 seconds depending on disk speed. Subsequent starts are faster because the database is cached in the repos/clamav-db/ volume. PostgreSQL also runs its own first-time initialization in the postgres_data volume.


Step 4 — Create the admin account

If you pre-provisioned ADMIN_USERNAME/ADMIN_PASSWORD_HASH in Step 1, skip to Step 5 and sign in with those credentials.

Otherwise, run the setup wizard once — it creates the first admin account and returns a ready-to-use JWT:

curl -X POST http://localhost:8000/api/v1/setup/ \
  -H "Content-Type: application/json" \
  -d '{"admin_username":"admin","admin_password":"YourPassword1!"}'

If you set SETUP_TOKEN in Step 1, add the header:

  -H "X-Setup-Token: your-setup-token-value"

POST /api/v1/setup returns 409 once an admin already exists — the wizard can only run once.


Step 5 — Open the interface

Navigate to http://localhost:3003.

Sign in with the username/password you just created.

Change the password immediately if it was a placeholder

Go to Account → Change password on first login.


Step 6 — Generate a GPG signing key

Repository indexes must be GPG-signed. Without a signing key, packages cannot be published (for APT/RPM) or signed (for APK).

  1. Open Settings → GPG
  2. Click Generate key
  3. Copy the public key fingerprint — you will need it when configuring client machines

No interactive terminal required

Key generation uses --pinentry-mode loopback. The GPG key is stored in the shared /repos/gnupg volume and never leaves the server.


Step 7 — Initialize distributions

All distributions for the formats enabled by REPO_FORMAT are initialized automatically at first startup by auto_init_distributions(). No manual action is required.

To re-initialize after a configuration corruption:

TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/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"

See Supported distributions below for the full list.


Step 8 — Upload your first package

  1. Go to Upload in the sidebar
  2. Drag and drop your .deb, .rpm, or .apk file
  3. Select a distribution (e.g. jammy, almalinux9, alpine3.20)
  4. Click Upload

The pipeline runs in real time:

✅ File received
✅ Format validation
✅ SHA-256 integrity check
✅ Antivirus scan — clean
✅ CVE analysis — no blocking CVEs
✅ GPG signature check
✅ Dependency resolution
✅ Added to repository (reprepro / createrepo_c / apk index)

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

# Upload a .deb (standard JSON response)
curl -X POST http://localhost:8000/api/v1/upload/ \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@mypackage_1.0.0_amd64.deb" \
  -F "distribution=jammy"

# Upload (streaming Server-Sent Events — same endpoint for all formats)
curl -X POST http://localhost:8000/api/v1/upload/stream \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@mypackage-1.0.0-1.x86_64.rpm" \
  -F "distribution=almalinux9"

CVE review queue

If the CVE scan finds high-severity vulnerabilities matching your policy, the package status becomes pending_review. It is stored but not published until an admin approves it in Security → Review queue.


Step 9 — Configure a client machine

# 1. Import the repository GPG public key
curl -fsSL http://YOUR_HOST:80/repos/gpg.key \
  | gpg --dearmor \
  | sudo tee /etc/apt/trusted.gpg.d/repod.gpg > /dev/null

# 2. Add the APT source
echo "deb http://YOUR_HOST:80/repos jammy main" \
  | sudo tee /etc/apt/sources.list.d/repod.list

# 3. Update and install
sudo apt update
sudo apt install mypackage
# 1. Import the GPG public key
sudo rpm --import http://YOUR_HOST:8080/repos/gpg.key

# 2. Create the repo file
sudo tee /etc/yum.repos.d/repod.repo << 'EOF'
[repod]
name=Repod Private Repository
baseurl=http://YOUR_HOST:8080/repos/almalinux9/x86_64/
enabled=1
gpgcheck=1
gpgkey=http://YOUR_HOST:8080/repos/gpg.key
EOF

# 3. Install
sudo dnf install mypackage
# 1. Add the repository
sudo zypper addrepo \
  --gpgcheck \
  http://YOUR_HOST:8080/repos/opensuse-leap-15.6/x86_64/ \
  repod

# 2. Import the GPG key
sudo rpm --import http://YOUR_HOST:8080/repos/gpg.key

# 3. Refresh and install
sudo zypper refresh repod
sudo zypper install mypackage
# 1. Import the GPG/abuild public key
curl -fsSL http://YOUR_HOST:80/apk/repod.rsa.pub \
  -o /etc/apk/keys/repod.rsa.pub

# 2. Add the repository
echo "http://YOUR_HOST:80/apk/alpine3.20/main" \
  | sudo tee -a /etc/apk/repositories

# 3. Update and install
sudo apk update
sudo apk add mypackage

Replace YOUR_HOST with the hostname or IP of the machine running Repod, and the port with the one mapped to depot-apt (:80 by default) or depot-rpm (:8080 by default).

Full client setup guide →


Supported distributions

Codename Distribution
jammy Ubuntu 22.04 LTS
noble Ubuntu 24.04 LTS
focal Ubuntu 20.04 LTS
bookworm Debian 12
Codename Distribution
almalinux8 AlmaLinux 8
almalinux9 AlmaLinux 9
rocky8 Rocky Linux 8
rocky9 Rocky Linux 9
centos-stream9 CentOS Stream 9
oraclelinux8 Oracle Linux 8
fedora Fedora (latest)
opensuse-leap-15.6 openSUSE Leap 15.6
opensuse-tumbleweed openSUSE Tumbleweed
Codename Distribution
alpine3.18 Alpine Linux 3.18
alpine3.19 Alpine Linux 3.19
alpine3.20 Alpine Linux 3.20
alpine3.21 Alpine Linux 3.21

What's next