Skip to main content

VPS / Production Deployment

Deploy the complete Open Saasframe stack — application, PostgreSQL, Redis, and Meilisearch — to any Linux server using Docker Compose.

Watch: Deploy Open Saasframe on a VPS


Prerequisites​

  • A Linux server or VPS (Ubuntu 22.04+ recommended)
  • Docker 24+ and Docker Compose v2+
  • At least 2 GB RAM (4 GB recommended for production workloads)
  • A domain name pointed at the server (for HTTPS)

Install Docker on Ubuntu:

sudo apt update
sudo apt install -y docker.io docker-compose-v2
sudo usermod -aG docker $USER
newgrp docker

Quick start​

git clone https://github.com/saasframe/saasframe.git
cd saasframe

cp apps/saasframe/.env.example apps/saasframe/.env
# Edit apps/saasframe/.env with your production values (see below)

docker compose -f docker-compose.fullapp.yml up --build -d

The application will be available at http://your-server-ip:3000/backend.

Sign in with the default admin credentials printed during first-run initialisation.


Environment variables​

Edit apps/saasframe/.env before starting. At minimum, set:

# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-strong-db-password
POSTGRES_DB=saasframe
DATABASE_URL=postgres://postgres:your-strong-db-password@postgres:5432/saasframe

# Authentication
JWT_SECRET=your-strong-jwt-secret

# Application
APP_URL=https://your-domain.com
NODE_ENV=production
# NODE_OPTIONS=--max-old-space-size=3072 # default; raise for larger hosts — see Troubleshooting

# Cache
CACHE_REDIS_URL=redis://redis:6379
CACHE_STRATEGY=redis

# Search (optional but recommended)
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_API_KEY=your-strong-meilisearch-key
MEILISEARCH_MASTER_KEY=your-strong-meilisearch-key

# AI features (optional)
OPENAI_API_KEY=sk-...

# Vector search auto-indexing is disabled by default in the shipped example env.
# Enable it explicitly when you want automatic embedding jobs.
SF_DISABLE_VECTOR_SEARCH_AUTOINDEXING=false

If you leave SF_DISABLE_VECTOR_SEARCH_AUTOINDEXING=true, vector search can still be configured and reindexed manually. The legacy alias DISABLE_VECTOR_SEARCH_AUTOINDEXING=1 remains supported for existing deployments.

Generate strong secrets:

openssl rand -hex 32 # for JWT_SECRET
openssl rand -hex 24 # for MEILISEARCH_MASTER_KEY
warning

The container hostnames (postgres, redis, meilisearch) match the service names in docker-compose.fullapp.yml. Use these exact names in your connection strings — not localhost.


Common operations​

# Start in detached mode
docker compose -f docker-compose.fullapp.yml up -d

# View logs — all services
docker compose -f docker-compose.fullapp.yml logs -f

# View logs — app only
docker compose -f docker-compose.fullapp.yml logs -f app

# Rebuild after pulling new commits
docker compose -f docker-compose.fullapp.yml up --build -d

# Stop all services
docker compose -f docker-compose.fullapp.yml down

# Apply new migrations (after upgrade)
yarn docker:db:migrate
# or directly:
docker compose -f docker-compose.fullapp.yml exec app yarn db:migrate

Data persistence​

Data is stored in named Docker volumes that survive container restarts:

VolumeContents
saasframe-postgres-dataPostgreSQL database
saasframe-redis-dataRedis cache and events
saasframe-meilisearch-dataMeilisearch indexes

Backup PostgreSQL:

docker exec saasframe-postgres pg_dump -U postgres saasframe > backup-$(date +%Y%m%d).sql

Restore PostgreSQL:

cat backup.sql | docker exec -i saasframe-postgres psql -U postgres -d saasframe

HTTPS with a reverse proxy​

For production, put a reverse proxy in front of the app. Example with nginx:

server {
listen 80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
server_name your-domain.com;

ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Use Certbot for free Let's Encrypt certificates.


Production security checklist​

  • JWT_SECRET — strong random value, never the default
  • POSTGRES_PASSWORD — strong random value
  • MEILISEARCH_MASTER_KEY — strong random value
  • NODE_ENV=production
  • Database and Redis ports not exposed to the public internet
  • HTTPS configured via reverse proxy
  • Automated daily backups scheduled for the PostgreSQL volume
  • Docker resource limits configured for containers

Troubleshooting​

Container fails to start:

docker compose -f docker-compose.fullapp.yml logs app

Database connection errors:

# Check PostgreSQL is running
docker compose -f docker-compose.fullapp.yml ps postgres

# Check the DATABASE_URL matches the container config
grep DATABASE_URL apps/saasframe/.env

Out of disk space:

docker system df # check usage
docker system prune -a # clean unused resources (does not touch named volumes)

App container keeps restarting (OOM killed):

Check the effective V8 heap limit inside the running container:

docker compose -f docker-compose.fullapp.yml exec app node \
-e "const s=require('v8').getHeapStatistics(); \
console.log(Math.round(s.heap_size_limit/1024/1024)+' MB heap limit')"

If the value is below 1 GB, raise NODE_OPTIONS in apps/saasframe/.env then restart:

Host RAMNODE_OPTIONS value
4 GB--max-old-space-size=3072 (default)
8 GB--max-old-space-size=5120
16 GB+--max-old-space-size=8192
docker compose -f docker-compose.fullapp.yml up -d

Full reset (deletes all data):

docker compose -f docker-compose.fullapp.yml down -v
docker compose -f docker-compose.fullapp.yml up --build -d