Self-Hosting Guide
User Types
| Type | Description | Database | LLM |
|---|---|---|---|
| A | Everything local | SQLite | Ollama (local) or cloud API |
| B | Remote service, local Ollama | PostgreSQL | Ollama |
| C | Remote service, cloud LLM | PostgreSQL | OpenRouter / OpenAI / Anthropic |
| D | Fully hosted (markcat.app) | PostgreSQL | OpenRouter (managed) |
This guide covers types A, B, and C.
User A — Local (Default)
No configuration needed. SQLite database + SQLite blob storage.
pip install -r service/requirements.txt
python -m service.main
# → http://127.0.0.1:8080
Moving blobs out of SQLite
If your database grows large, move blobs (favicons, OG images) to the filesystem:
export BLOB_BACKEND=local
python -m service.scripts.migrate_blobs_to_storage --execute
python -m service.main
User B/C — Self-Hosted with PostgreSQL
# 1. Configure environment
export DB_BACKEND=postgres
export DATABASE_URL=postgres://user:pass@server:5432/markcat
export BLOB_BACKEND=local
export BLOB_DIR=/data/markcat/blobs
# 2. Start service (creates schema automatically)
python -m service.main
# 3. (Optional) Migrate from existing SQLite
python -m service.scripts.export_to_postgres --execute
python -m service.scripts.migrate_blobs_to_storage --execute
Docker Deployment
For a production setup with Caddy reverse proxy and auto-TLS:
# See deploy/ directory for Docker Compose and Caddyfile
# Key components: FastAPI (port 8080) + PostgreSQL (5432) + Caddy (443)
The repository’s deploy/ directory contains the Docker Compose files and Caddyfile for a full VPS deployment.
Serve over HTTPS for PWA install & Android share
The PWA only installs in a secure context — HTTPS on any hostname, or localhost/127.0.0.1. Installing the PWA is also what registers Markcat in Android’s Share sheet (share-as-bookmark). Consequences for self-hosters:
- A plain-HTTP LAN address (e.g.
http://192.168.1.50:8080) is not a secure context — Chrome won’t offer install, and the share target never appears. - Behind a TLS reverse proxy (Caddy/nginx + Let’s Encrypt), or via a Tailscale/Cloudflare-tunnel HTTPS hostname, install and share target work exactly as on the hosted app.
This adds no requirement beyond what PWA install already needs — if “Add to Home Screen” works, the Android share target works.
Environment Variables
Required (Cloud Mode)
| Variable | Description |
|---|---|
DB_BACKEND |
postgres |
DATABASE_URL |
PostgreSQL connection string |
BLOB_BACKEND |
local (recommended) |
BLOB_DIR |
Path to blob storage directory |
Authentication (Multi-Tenant)
| Variable | Description |
|---|---|
AUTH_MODE |
jwt for multi-tenant mode |
JWT_SECRET |
Secret key for JWT signing |
SETTINGS_ENCRYPTION_KEY |
Required when AUTH_MODE=jwt. Encrypts BYOK API keys at rest (AES-GCM). Generate with python -c "import os, base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())". |
CORS_ORIGINS |
Allowed origins (comma-separated) |
Billing (Optional)
| Variable | Description |
|---|---|
STRIPE_SECRET_KEY |
Stripe API key |
STRIPE_WEBHOOK_SECRET |
Stripe webhook signing secret |
STRIPE_PRICE_BASIC_MONTHLY / _ANNUAL |
Stripe price IDs for Basic plans |
STRIPE_PRICE_PRO_MONTHLY / _ANNUAL |
Stripe price IDs for Pro plans |
STRIPE_PRICE_TOPUP_500 / _2K / _10K |
Stripe price IDs for top-up packs |
The repository’s docs/reference/deployment.md holds the complete environment variable reference.
Database Backends
| Backend | File | Concurrency |
|---|---|---|
| SQLite | Single file, WAL mode | One process only |
| PostgreSQL | Connection pool (2-10) | Multiple instances OK |
Utility Scripts
Run with python -m service.scripts.<name>:
| Script | Purpose |
|---|---|
export_to_postgres |
Copy SQLite data to PostgreSQL |
migrate_blobs_to_storage |
Move blobs from SQLite to filesystem |
fix_duplicates |
Deduplicate bookmarks by normalized URL |
fix_favicons |
Re-fetch missing favicons |
find_dead_sites |
Detect dead bookmarks |
compact_categories |
LLM-powered category merging |
The repository’s docs/reference/scripts.md lists every script.