Zly can be deployed anywhere. This guide covers every option, ranked from most to least recommended.
Full control. Persistent PostgreSQL, Redis caching, auto-HTTPS via Caddy, zero platform lock-in.
- Linux VPS (Ubuntu 24.04+ or Debian 12+, $5–10/mo on Hetzner, DigitalOcean, or Linode)
- Docker Engine 24+ and Docker Compose v2
- A domain name pointing to your server (e.g.
zly.example.com)
# SSH into your server
ssh root@your-server-ip
# Install Docker (Ubuntu)
apt update && apt install -y docker.io docker-compose-v2
# Clone
git clone https://github.com/pythonplumber/zly.git
cd zly
# Configure
cp infrastructure/.env.example .env
nano .env # Set secrets, domain, etc.
# Start all services
docker compose -f infrastructure/docker-compose.yml up -d
# Run migrations
docker compose exec fastapi alembic upgrade headDATABASE_URL=postgresql+asyncpg://zly:your-password@postgres:5432/zly
REDIS_URL=redis://redis:6379/0
SECRET_KEY=<openssl rand -hex 64>
JWT_SECRET=<openssl rand -hex 64>
CORS_ORIGINS=https://zly.example.com
DEFAULT_DOMAIN=zly.example.com
ENVIRONMENT=production| Service | Role |
|---|---|
| Caddy | Reverse proxy, auto-Let's Encrypt TLS, rate limiting |
| FastAPI | Zly application server (4 workers) |
| PostgreSQL | Primary database (persistent volume) |
| Redis | Cache + optional job queue (persistent volume) |
# Logs
docker compose logs -f fastapi
# Backups
docker compose exec postgres pg_dump -U zly zly > backup.sql
# Upgrades
git pull
docker compose up -d --build
docker compose exec fastapi alembic upgrade head- Generate secrets with
openssl rand -hex 64 - Set
CORS_ORIGINSto your exact domain - Never commit
.envto Git
Native Docker support with a free PostgreSQL add-on and Redis. Best PaaS option.
- Push your repo to GitHub
- Go to Railway → New Project → Deploy from GitHub repo
- Add these environment variables in Railway dashboard:
DATABASE_URL=postgresql+asyncpg://<user>:<pass>@<host>:<port>/<db>
SECRET_KEY=<random-64-char>
JWT_SECRET=<random-64-char>
CORS_ORIGINS=https://your-app.railway.app
DEFAULT_DOMAIN=your-app.railway.app
ENVIRONMENT=production- Add a PostgreSQL plugin — Railway auto-generates
DATABASE_URLfrom it - Add a Redis plugin (optional, for caching)
- In Settings, set the start command:
uvicorn app.main:app --host 0.0.0.0 --port $PORT- Run migrations in the Railway shell:
alembic upgrade head- Railway handles HTTPS automatically
- The
$PORTvariable is auto-set by Railway - Custom domains work via Railway's domain settings
Web Service + managed PostgreSQL via Neon. No Redis (app degrades gracefully).
- Push your repo to GitHub
- Go to Render → New Web Service → connect your repo
- Fill in:
| Field | Value |
|---|---|
| Name | zly |
| Runtime | Python 3 |
| Build Command | pip install -e ".[dev]" |
| Start Command | uvicorn app.main:app --host 0.0.0.0 --port $PORT |
| Plan | Starter ($7/mo) or Free |
- Add environment variables:
DATABASE_URL=postgresql+asyncpg://<neon-user>:<pass>@<neon-host>/<db>?sslmode=require
SECRET_KEY=<random-64-char>
JWT_SECRET=<random-64-char>
CORS_ORIGINS=https://zly.onrender.com
DEFAULT_DOMAIN=zly.onrender.com
ENVIRONMENT=production- Create a Neon (free) or Render PostgreSQL database and set
DATABASE_URL - Deploy, then run migrations in Render Shell:
alembic upgrade head- Free tier spins down after inactivity (slow first request)
- Upgrade to Starter for always-on
- Redis is not available on Render free tier — Zly works fine without it
- Custom domains via Render dashboard
Serverless functions. Requires Neon/Supabase for database. No Redis.
- Vercel account
- Neon (free serverless PostgreSQL) or Supabase
- Vercel CLI (
npm i -g vercel)
- Push your repo to GitHub
- Create a Neon database and copy the connection string
- Deploy via Vercel dashboard or CLI:
vercel --prod- Add environment variables in Vercel dashboard → Project Settings → Environment Variables:
DATABASE_URL=postgresql+asyncpg://<user>:<pass>@<neon-host>/<db>?sslmode=require
SECRET_KEY=<random-64-char>
JWT_SECRET=<random-64-char>
CORS_ORIGINS=https://zly-ecru.vercel.app
DEFAULT_DOMAIN=zly-ecru.vercel.app
ENVIRONMENT=production- Run migrations via Vercel CLI:
vercel env pull
alembic upgrade head- The
api/index.pyfile is the Vercel entrypoint (already configured) vercel.jsonroutes all traffic to the FastAPI function- Serverless cold starts mean the first request may take 2–3 seconds
- SQLite doesn't work on Vercel (read-only filesystem) — must use PostgreSQL
- No Redis available — app falls back to direct DB lookups
| Feature | VPS 🥇 | Railway 🥈 | Render 🥉 | Vercel 🏅 |
|---|---|---|---|---|
| Cost | $5–10/mo | $5/mo+ | $7/mo+ | Free |
| Redis | ✅ Full | ✅ Add-on | ❌ | ❌ |
| PostgreSQL | ✅ Native | ✅ Add-on | ✅ Neon | ✅ Neon |
| HTTPS | ✅ Caddy auto | ✅ Auto | ✅ Auto | ✅ Auto |
| Persistent storage | ✅ Docker volume | ✅ | ✅ | ❌ ephemeral |
| Cold starts | ❌ None | ❌ None | ||
| Custom domain | ✅ | ✅ | ✅ | ✅ |
| Control | Full | Medium | Medium | Low |
| Setup time | 15 min | 5 min | 5 min | 5 min |
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
✅ Yes | — | PostgreSQL connection string |
SECRET_KEY |
✅ Yes | — | General purpose secret (64-char hex) |
JWT_SECRET |
✅ Yes | — | JWT signing key (64-char hex) |
CORS_ORIGINS |
✅ Yes | — | Allowed origins, comma-separated |
DEFAULT_DOMAIN |
✅ Yes | — | Base domain for short URLs |
REDIS_URL |
❌ No | redis://localhost:6379/0 |
Redis connection (graceful fallback) |
JWT_ALGORITHM |
❌ No | HS256 |
Signing algorithm |
ACCESS_TOKEN_EXPIRE_MINUTES |
❌ No | 15 |
JWT lifetime (minutes) |
alembic upgrade headAfter the first deploy, run this command to create all database tables.
| File | Purpose |
|---|---|
infrastructure/docker-compose.yml |
Full-stack Docker (VPS) |
infrastructure/Dockerfile |
Application container |
infrastructure/Caddyfile |
Reverse proxy + TLS |
infrastructure/.env.example |
Environment template |
api/index.py |
Vercel serverless entrypoint |
vercel.json |
Vercel routing config |
app/config.py |
All environment variables |