Self-host SavSpot
Run your own instance with Docker. Full source code, no limits, no fees. One command to get started.
Quick Start
Clone the repo and run the install script. That's it.
$ git clone https://github.com/stephendeslate/savspot.git
$ cd savspot
$ ./scripts/install.shThe install script will:
- Create
.envfrom the production template - Generate JWT keys and an encryption key
- Generate a random database password
- Build all containers
- Run database migrations
- Start the full stack
SavSpot will be available at http://localhost (or https://yourdomain.com if DOMAIN is configured).
Prerequisites
Docker 24+
With Compose v2 (docker compose)
2 GB RAM minimum
4 GB recommended for production
Linux server
Ubuntu 22.04+, Debian 12+, or similar
Domain name (optional)
Works on localhost for testing
Manual Setup
If you prefer to set things up yourself:
$ git clone https://github.com/stephendeslate/savspot.git
$ cd savspot
# Create and edit your environment file
$ cp .env.production.example .env
# Generate keys (append to .env)
$ ./scripts/generate-keys.sh >> .env
# Edit .env — at minimum, set:
# POSTGRES_PASSWORD (strong random password)
# DOMAIN (your domain, or "localhost" for testing)
# Build and start
$ docker compose -f docker-compose.prod.yml up -dConfiguration Reference
Required Variables
| Variable | Description |
|---|---|
POSTGRES_PASSWORD | Database password (auto-generated by install script) |
JWT_PRIVATE_KEY_BASE64 | RS256 private key, base64-encoded |
JWT_PUBLIC_KEY_BASE64 | RS256 public key, base64-encoded |
ENCRYPTION_KEY | 32-byte hex key for encrypting sensitive data |
Domain & URLs
| Variable | Default | Description |
|---|---|---|
DOMAIN | localhost | Your domain — Caddy auto-provisions HTTPS via Let's Encrypt |
WEB_URL | http://localhost | Public URL (used for email links, CORS) |
Optional Integrations
All integrations gracefully degrade — SavSpot runs without any of them.
| Integration | Variables | Without it |
|---|---|---|
| Email (Resend) | RESEND_API_KEY | Emails logged to console |
| Payments (Stripe) | STRIPE_SECRET_KEY | Payment features disabled |
| File Uploads (R2) | R2_ACCOUNT_ID, etc. | Upload endpoints return errors |
| Google OAuth | GOOGLE_CLIENT_ID | Google login button hidden |
Architecture
Six services, one docker compose command.
┌─────────────┐
│ Caddy │ :80/:443 — auto HTTPS, reverse proxy
├─────────────┤
│ Web │ :3000 — Next.js frontend (standalone)
│ API │ :3001 — NestJS backend
│ Worker │ BullMQ background jobs
├─────────────┤
│ PostgreSQL │ :5432 — data store with RLS
│ Redis │ :6379 — cache + job queue
└─────────────┘Caddy routes /api/* to the API and everything else to the Web frontend. It handles TLS certificates automatically when DOMAIN is set to a real domain.
Common Operations
View logs
$ docker compose -f docker-compose.prod.yml logs -f # All services
$ docker compose -f docker-compose.prod.yml logs -f api # API only
$ docker compose -f docker-compose.prod.yml logs -f web # Web onlyStop / Start
$ docker compose -f docker-compose.prod.yml down # Stop all
$ docker compose -f docker-compose.prod.yml up -d # Start allUpdate to latest version
$ git pull
$ docker compose -f docker-compose.prod.yml build
$ docker compose -f docker-compose.prod.yml up -dMigrations run automatically on startup via the migrate service.
Database backup & restore
$ docker compose -f docker-compose.prod.yml exec postgres \
pg_dump -U savspot savspot > backup-$(date +%Y%m%d).sql$ docker compose -f docker-compose.prod.yml exec -T postgres \
psql -U savspot savspot < backup-20260316.sqlCustom Domain with HTTPS
Caddy automatically obtains and renews Let's Encrypt certificates — no manual TLS configuration needed.
- Point your domain's DNS A record to your server's IP address
- Set
DOMAIN=yourdomain.comin.env - Set
WEB_URL=https://yourdomain.comin.env - Restart:
docker compose -f docker-compose.prod.yml up -d
Troubleshooting
Containers won't start
# Check logs for errors
$ docker compose -f docker-compose.prod.yml logs
# Common issue: port 80/443 already in use
$ sudo lsof -i :80
$ sudo lsof -i :443API health check failing
# Check if migrations ran successfully
$ docker compose -f docker-compose.prod.yml logs migrate
# Check API logs
$ docker compose -f docker-compose.prod.yml logs apiDatabase connection errors
# Verify postgres is healthy
$ docker compose -f docker-compose.prod.yml exec postgres pg_isready
# Check the DATABASE_URL matches POSTGRES_USER/PASSWORD/DB in .envWeb app showing blank page
# Check if NEXT_PUBLIC_API_URL is reachable from the web container
$ docker compose -f docker-compose.prod.yml exec web \
wget -q --spider http://api:3001/healthHardware Recommendations
Rough estimates. Actual requirements depend on booking volume and feature usage.
| Users | RAM | CPU | Disk |
|---|---|---|---|
| 1–50 | 2 GB | 1 core | 10 GB |
| 50–500 | 4 GB | 2 cores | 20 GB |
| 500+ | 8 GB+ | 4 cores | 50 GB+ |
Enterprise Features
Self-hosted users can unlock enterprise features (audit logging, workflows, contracts, multi-location, and more) by setting a license key:
SAVSPOT_LICENSE_KEY=your-license-key-hereWithout a license key, SavSpot runs with the full AGPL core — every essential feature for running a booking business, with no limits. Enterprise features are available on managed cloud plans or with a self-hosted license key.
Ready to self-host?
Clone the repo and run one command. Or try our managed cloud with a 14-day free trial.