Skip to content

Self-hosting

tap ships as a Docker Compose stack: PostgreSQL, Redis, the FastAPI backend, a Celery worker and beat scheduler for automatic polling, and an nginx-served frontend that reverse-proxies /api to the backend. There is nothing else to install.

Prerequisites

  • Docker Engine and the Docker Compose plugin (docker compose version).
  • A place to run it with the ports 8080 (frontend) and 8000 (backend API) free, or edit docker-compose.yml to change them.

Quick start

git clone https://github.com/Peppe37/tap.git
cd tap
cp .env.example .env

Edit .env and fill in the secrets .env.example leaves blank:

# JWT signing key for access/refresh tokens
python3 -c "import secrets; print(secrets.token_urlsafe(48))"

# Fernet key used to encrypt provider credentials (e.g. an aggregator API key) at rest
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

(No Python locally? Run either command through Docker instead: docker run --rm python:3.12-slim python3 -c "...".)

Also change POSTGRES_PASSWORD from the placeholder. Then:

docker compose up --build

The backend container runs database migrations (alembic upgrade head) and seeds the built-in carrier/shop/provider catalogue on every startup, so a fresh instance is ready to use as soon as the containers report healthy.

Using pre-built images

docker compose up --build builds the backend and frontend images locally, which works everywhere but takes a minute on first run. As an alternative, every release publishes multi-arch (amd64/arm64) images to GitHub Container Registry -- ghcr.io/peppe37/tap-backend and ghcr.io/peppe37/tap-frontend -- and pushes to main also publish a rolling edge tag. Use them instead of building by layering docker-compose.images.yml on top, which swaps build: for image: on the backend/worker/beat/frontend services without changing anything else (Postgres, Redis, volumes, healthchecks stay exactly as in docker-compose.yml).

You don't need to clone the whole repository for this -- just the two compose files and .env:

mkdir tap && cd tap
curl -O https://raw.githubusercontent.com/Peppe37/tap/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/Peppe37/tap/main/docker-compose.images.yml
curl -O https://raw.githubusercontent.com/Peppe37/tap/main/.env.example
cp .env.example .env  # then fill in the secrets, same as the Quick start above
docker compose -f docker-compose.yml -f docker-compose.images.yml up -d

Set TAP_IMAGE_TAG in .env to pin a specific version (e.g. v0.2.0) or track edge; it defaults to latest (the most recent stable release).

Why two images, not one

The backend image is already reused three ways (API, Celery worker, Celery beat -- same image, different command), so it's not duplicated per-service. The frontend is nginx serving a static build and reverse-proxying /api, a fundamentally different runtime than the Python API. Merging the two into a single image would mean running two unrelated processes in one container (nginx + uvicorn, via something like supervisord) — it would work, but it means one container whose health check can't distinguish "the API died" from "nginx died," logs from both processes interleaved, and no way to scale or cache the static frontend independently of the API. Two images, one process each, is the more debuggable and more standard shape for this kind of app; that's why we kept it even though it means one extra docker pull.

First run

Opening the frontend for the first time shows a "Create the administrator account" screen instead of a login form — this is a one-time bootstrap step (the backend exposes GET /auth/setup-status to detect that no user exists yet). Once created, every further user is added from inside the app by an existing account; there is no public sign-up.

Guided connections

Carriers can be tracked through more than one method: an official carrier API, a maintained scraper, or a third-party aggregator. Methods that need a personal API key (aggregators, mainly) show up under Settings → Connections with:

  • step-by-step instructions for obtaining the credential from that provider's own site,
  • the exact fields to fill in,
  • a "test connection" check that calls the provider before anything is saved.

Nothing is stored until the test succeeds, and credentials are encrypted at rest with the Fernet key you generated above (TAP_CREDENTIAL_ENCRYPTION_KEY) — losing that key makes stored credentials unrecoverable, so back it up along with the database.

Environment variables

All of these live in .env at the repo root, read by docker-compose.yml:

Variable Purpose
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB Credentials for the bundled PostgreSQL container.
TAP_ENVIRONMENT production by default; informational, used in a few log/error messages.
TAP_DATABASE_URL SQLAlchemy async URL the backend uses to reach Postgres.
TAP_REDIS_URL Redis URL shared by Celery (broker/result backend) and the local rate limiter.
TAP_JWT_SECRET_KEY Signs access/refresh tokens. Rotating it invalidates every existing session.
TAP_CREDENTIAL_ENCRYPTION_KEY Fernet key encrypting stored provider credentials.
TAP_CORS_ORIGINS JSON array of allowed origins for the API (only matters if you serve the frontend from a different origin than the default nginx proxy).
TAP_RATE_LIMIT_PER_MINUTE Requests/minute allowed per caller (authenticated user, or client IP when unauthenticated) across the whole API. Default 120; raise it if a household of heavy users trips it during normal use.
VITE_API_BASE_URL Base path the frontend calls; left as /api unless you split frontend/backend across origins.

Health and monitoring

  • GET /api/health -- plain liveness: "is the process running." Always 200 {"status": "ok"} if it responds at all.
  • GET /api/health/ready -- readiness: actually checks the database and Redis connections, 503 if either is down. This is what docker-compose.yml's backend healthcheck uses (so depends_on: condition: service_healthy for worker/beat/frontend means "backend can really serve requests," not just "the process started"); point an external uptime checker or reverse proxy health check at this one, not the plain liveness endpoint.
  • Admins also get an update-available notice in the UI itself (GET /api/system/update-status), checked against GitHub Releases and cached for 6h -- no separate setup needed.

Updating

git pull
docker compose up --build

Migrations run automatically on backend startup; there's no separate migration step to remember.

Backups

Everything that matters lives in the postgres-data named volume (packages, tracking history, users, encrypted credentials) plus the two secrets in .env (TAP_JWT_SECRET_KEY, TAP_CREDENTIAL_ENCRYPTION_KEY — without the encryption key, backed-up credentials can't be decrypted even if the database is restored).

scripts/backup.sh [output-dir] dumps the database (gzip-compressed, timestamped) and prunes dumps older than TAP_BACKUP_RETENTION_DAYS (default 14 days). Run it from a cron job or systemd timer on the host for actual automation:

# crontab -e
0 3 * * * cd /path/to/tap && ./scripts/backup.sh >> backups/backup.log 2>&1

Back up .env alongside the dumps -- it never changes as often, but a dump without the encryption key it contains is only half a backup. scripts/restore.sh <dump-file> restores one (destructive: it drops and recreates the database first, asks for confirmation, and stops backend/worker/beat for the duration).

Security

Every push and pull request runs, on top of linting and tests: security.yml (pip-audit and bandit for the backend, pnpm audit for the frontend, a gitleaks secret scan across the whole repository) and codeql.yml (static analysis for Python and JavaScript/TypeScript). Two frontend advisories are currently accepted as not applicable rather than fixed — see the comment above auditConfig in frontend/pnpm-workspace.yaml for the reasoning behind each one.

Dependabot opens weekly PRs for outdated pip/npm/Docker-base-image/GitHub Actions dependencies (grouped per ecosystem, so it's one PR to review, not ten), and every published Docker image carries SBOM and provenance attestations (visible in GHCR's package view) so you can audit what went into a given ghcr.io/peppe37/tap-* tag.

Found an actual vulnerability? See SECURITY.md rather than opening a public issue.

Local development

See Contributing for running the backend and frontend outside Docker.