Skip to content

Sellf - Production Deployment Guide

Complete guide for deploying Sellf on a production server using Docker Compose.

  1. Requirements
  2. Server Preparation
  3. Environment Variables Configuration
  4. Database Configuration
  5. Starting the Application
  6. Domain and SSL Configuration
  7. Stripe Webhooks Configuration
  8. Initial Setup
  9. Monitoring and Logs
  10. Updating
  11. Backup and Restore
  12. Troubleshooting
  • CPU: 2 vCPU
  • RAM: 4 GB (recommended: 8 GB)
  • Disk: 20 GB SSD (recommended: 50 GB)
  • Transfer: 100 GB/month
Section titled “Recommended Server (Sellf + Supabase self-hosted on one machine)”

Hetzner CX33 — tested and recommended for early production:

SpecValue
CPU4 vCPU (AMD, shared)
RAM8 GB
Disk80 GB NVMe
Transfer20 TB/month
Price~€6.14/month

Why it works:

  • Supabase self-hosted uses ~2.0–2.5 GB RAM (13 containers)
  • Sellf (PM2/Next.js) uses ~200–300 MB RAM
  • Total: ~2.7 GB in use, 5+ GB free headroom
  • Sellf connects to Supabase via localhost → 5–10 ms latency vs. 90–130 ms over the internet
  • ~€6/month (~25 PLN) is enough to serve several thousand customers in a fully self-hosted environment — no Supabase Pro (~$25/mo), no platform lock-in

Disk space estimate:

  • Docker images (Supabase): ~3–4 GB
  • OS + swap: ~3 GB
  • App + DB data: growing over time
  • 80 GB is sufficient for early production; consider CPX32 (160 GB NVMe) if heavy Supabase Storage usage is planned

Swap (recommended): Hetzner CX33 is a KVM VM with full kernel access — swap works without restrictions. Enable 2 GB swapfile as a buffer for traffic spikes (e.g. Supabase analytics container startup):

Terminal window
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# Reduce swappiness (default 60 is too aggressive for NVMe)
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Note: LXC-based VPS (e.g. Mikrus) does not support swap — kernel access is blocked by the host.

  • Operating System: Ubuntu 22.04 LTS or newer (recommended)
  • Docker: version 24.0 or newer
  • Docker Compose: version 2.20 or newer
  • Git: for downloading the code
  • Domain: your own domain with DNS access
  • SMTP: email service (SendGrid, AWS SES, Mailgun, etc.)
  • Stripe: production account
  • Cloudflare Turnstile: account (optional, for CAPTCHA)
Terminal window
sudo apt update && sudo apt upgrade -y
Terminal window
# Remove old versions
sudo apt remove docker docker-engine docker.io containerd runc
# Install dependencies
sudo apt install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
# Add official Docker GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Verify installation
docker --version
docker compose version
Section titled “3. Docker Configuration (optional but recommended)”
Terminal window
# Add user to docker group (avoid sudo)
sudo usermod -aG docker $USER
# Log in again or:
newgrp docker
# Configure Docker to start automatically
sudo systemctl enable docker
sudo systemctl start docker
Terminal window
sudo apt install -y git
Terminal window
# Enable UFW
sudo ufw enable
# Allow SSH
sudo ufw allow 22/tcp
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Check status
sudo ufw status
Terminal window
# Go to home directory
cd ~
# Clone the repository
git clone https://github.com/your-organization/sellf.git
cd sellf
Terminal window
# Copy the example file
cp .env.production.example .env.production
# Edit the file
nano .env.production
Terminal window
# Generate JWT_SECRET
openssl rand -base64 32
# Generate REALTIME_SECRET_KEY_BASE
openssl rand -base64 32
# Generate POSTGRES_PASSWORD (long password)
openssl rand -base64 48

Below you will find a detailed description of each variable:

POSTGRES_PASSWORD=your_very_secure_postgresql_password
JWT_SECRET=paste_generated_jwt_secret
REALTIME_SECRET_KEY_BASE=paste_generated_realtime_secret
ANON_KEY=get_from_supabase_dashboard
SERVICE_ROLE_KEY=get_from_supabase_dashboard

Note: The ANON_KEY and SERVICE_ROLE_KEY keys can be generated in the Supabase Dashboard or using a JWT generation tool with the appropriate secret.

API_EXTERNAL_URL=https://api.your-domain.com
NEXT_PUBLIC_SUPABASE_URL=https://api.your-domain.com
GOTRUE_SITE_URL=https://your-domain.com
NEXT_PUBLIC_SITE_URL=https://your-domain.com
NEXT_PUBLIC_BASE_URL=https://your-domain.com
MAIN_DOMAIN=your-domain.com
GOTRUE_URI_ALLOW_LIST=https://your-domain.com/*,https://www.your-domain.com/*

Example for SendGrid:

SMTP_ADMIN_EMAIL[email protected]
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=SG.xxxxxxxxxxxxxxxxxxxxxxxxx
SMTP_SENDER_NAME=Sellf

Example for Gmail:

SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_PASS=your-app-password

METHOD 1: .env Configuration (Recommended for developers, Docker, CI/CD)

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxx
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxx # Standard Secret Key or Restricted Key
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx

METHOD 2: Admin Panel Wizard (Recommended for non-technical users)

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxx
STRIPE_ENCRYPTION_KEY=ONIgOXqmoHOYZphEDkhydpL4briQsVlS9IS3o59mW9E= # Generate: openssl rand -base64 32
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx

Then configure the Restricted API Key through the graphical interface in Settings.

Both methods are fully supported. Choose the one that fits your workflow.

Details: See section 5. Stripe Configuration below.

NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY=1x00000000000000000000AA
CLOUDFLARE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA

Sellf supports two equivalent methods for Stripe configuration. Choose the one that best fits your use case.

Section titled “Method 1: .env Configuration (Recommended for developers)”

Advantages:

  • ✅ Quick setup (one environment variable)
  • ✅ Ideal for Docker, CI/CD, automation
  • ✅ Developers are familiar with this pattern
  • ✅ Easy rollback (change .env and restart)

Steps:

  1. Get the Secret Key from Stripe Dashboard:

  2. Add to .env.production:

    Terminal window
    # Test Mode (development)
    STRIPE_SECRET_KEY=sk_test_51ABC...xyz
    # OR Live Mode (production)
    STRIPE_SECRET_KEY=sk_live_51ABC...xyz
  3. Restart the application:

    Terminal window
    docker compose restart admin-panel
  4. Verify in Settings:

    • Go to: https://your-domain.com/dashboard/settings
    • You should see a blue banner: “Currently using: .env configuration”
Section titled “Method 2: Admin Panel Wizard (Recommended for non-technical users)”

Advantages:

  • ✅ Visual step-by-step guide
  • ✅ AES-256-GCM encryption (keys in database)
  • ✅ Automatic permission validation
  • ✅ Key rotation reminders (every 90 days)
  • ✅ No file editing required

Steps:

  1. Generate an encryption key (one-time):

    Terminal window
    openssl rand -base64 32
  2. Add the key to .env.production:

    Terminal window
    echo "STRIPE_ENCRYPTION_KEY=YOUR_GENERATED_KEY" >> .env.production

    ⚠️ CRITICAL: Never commit this key to Git!

  3. Restart the application:

    Terminal window
    docker compose restart admin-panel
  4. Open the wizard:

    • Go to: https://your-domain.com/dashboard/settings
    • Click the “Configure Stripe” button
  5. Go through 5 steps:

    • Step 1 (Welcome): Click “Start Configuration”
    • Step 2 (Mode selection): Choose “Test Mode” or “Live Mode”
    • Step 3 (Create key): Follow the visual guide:
      1. Open Stripe Dashboard
      2. Go to API Keys → Create restricted key
      3. Set permissions:
        • ✅ Charges: Write
        • ✅ Customers: Write
        • ✅ Checkout Sessions: Write
        • ✅ Payment Intents: Read
        • ✅ Webhooks: Read (optional)
      4. Copy the key (starts with rk_test_ or rk_live_)
      5. Return to the wizard and click “I’ve Created the Key”
    • Step 4 (Validation): Paste the key and click “Validate API Key”
    • Step 5 (Success): Click “Finish”
  6. Verify configuration:

    • You should see a green banner: “Currently using: Database configuration”
    • Your masked key: rk_test_****1234 (only last 4 characters)
    • Status: Test Mode / Live Mode
    • Permissions: ✅ Verified

From .env to Wizard:

  1. Simply launch the wizard and configure the key
  2. Database configuration takes priority over .env
  3. You can leave the STRIPE_SECRET_KEY variable in .env as a fallback

From Wizard to .env:

  1. Add STRIPE_SECRET_KEY to .env
  2. Remove configuration from the database:
    Terminal window
    docker exec supabase_db_sellf psql -U postgres -d postgres -c \
    "DELETE FROM stripe_configurations WHERE is_active = true;"
  3. Restart the application

Test with a Stripe test card:

  1. Create a test product in the Admin Panel
  2. Go to the product page
  3. Click “Buy Now”
  4. Use the test card: 4242 4242 4242 4242
    • Expiry: any future date (e.g. 12/34)
    • CVC: any 3 digits (e.g. 123)
  5. Verify the payment in:
    • Dashboard → Payments
    • Stripe Dashboard → Payments

📖 Full testing guide: See the Stripe Testing Guide

The wizard requires the stripe_configurations table in the database:

Terminal window
# Check if the migration exists
ls -la supabase/migrations/ | grep stripe
# Should be: 20251227000000_stripe_rak_configuration.sql

If the migration does not exist, it will be automatically executed during database startup.

Check that all migrations are in place:

Terminal window
ls -la supabase/migrations/

The following files should be present:

  • 20250709000000_initial_schema.sql - Initial schema
  • 20250717000000_payment_system.sql - Payment system
  • 20251227000000_stripe_rak_configuration.sql - Stripe configuration (wizard)
  • 20251227100000_shop_config.sql - Shop configuration
  • others…

If you want to have your own sample data:

Terminal window
nano supabase/seed.sql
Terminal window
# Make sure you are in the main project directory
cd ~/sellf
# Build images (may take a few minutes on the first run)
docker compose build
# Start all services
docker compose up -d
# Check container status
docker compose ps

Expected output:

NAME STATUS PORTS
sellf-admin running 0.0.0.0:3000->3000/tcp
sellf-db running (healthy) 0.0.0.0:5432->5432/tcp
sellf-auth running
sellf-rest running
sellf-storage running
sellf-nginx running 0.0.0.0:8080->80/tcp
...
Terminal window
# All containers
docker compose logs -f
# Specific container
docker compose logs -f admin-panel
docker compose logs -f db

If the database was automatically initialized (migrations in /docker-entrypoint-initdb.d), you can skip this step. Otherwise:

Terminal window
# Connect to the database
docker compose exec db psql -U postgres
# Check tables
\dt
# Exit
\q

If the tables do not exist, run migrations manually:

Terminal window
# Copy migrations to the container
docker compose cp supabase/migrations/. db:/tmp/migrations/
# Execute migrations
docker compose exec db psql -U postgres -d postgres -f /tmp/migrations/20250709000000_initial_schema.sql
docker compose exec db psql -U postgres -d postgres -f /tmp/migrations/20250717000000_payment_system.sql
Section titled “Option 1: Nginx Proxy Manager (Recommended for beginners)”
  1. Install Nginx Proxy Manager:
Terminal window
# Create a separate directory
mkdir ~/nginx-proxy-manager
cd ~/nginx-proxy-manager
# Download docker-compose.yml for NPM
wget https://github.com/NginxProxyManager/nginx-proxy-manager/blob/main/docker-compose.yml
# Start
docker compose up -d
  1. Log in to the panel: http://your-server:81

  2. Add a Proxy Host:

    • Domain: your-domain.com
    • Forward Hostname: admin-panel
    • Forward Port: 3000
    • Websockets: ✅
    • SSL: Select “Request a new SSL Certificate” (Let’s Encrypt)
  3. Add a second Proxy Host for the API:

    • Domain: api.your-domain.com
    • Forward Hostname: kong
    • Forward Port: 8000
    • SSL: ✅
  4. Add a third Proxy Host for examples:

    • Domain: examples.your-domain.com (optional)
    • Forward Hostname: nginx
    • Forward Port: 80
    • SSL: ✅

Option 2: Certbot + Nginx (For advanced users)

Section titled “Option 2: Certbot + Nginx (For advanced users)”
Terminal window
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d your-domain.com -d www.your-domain.com -d api.your-domain.com
# Automatic renewal
sudo systemctl enable certbot.timer

Set DNS records with your provider:

Type Name Value TTL
A @ YOUR_SERVER_IP 3600
A www YOUR_SERVER_IP 3600
A api YOUR_SERVER_IP 3600

Quick path: After your first login as admin (next major section below), open Settings → Payments in the Sellf admin. Paste your Stripe keys, click Register webhook — Sellf creates the endpoint on Stripe and saves the signing secret encrypted in your Supabase DB. Skips the manual flow below.

The env-config flow in this section is still valid for CI-driven Docker deploys where you don’t want to log into the Sellf admin to click things.

1. Create a Webhook Endpoint in Stripe Dashboard

Section titled “1. Create a Webhook Endpoint in Stripe Dashboard”
  1. Go to: https://dashboard.stripe.com/webhooks
  2. Click “Add endpoint”
  3. URL: https://your-domain.com/api/webhooks/stripe
  4. Select events:
    • checkout.session.completed
    • checkout.session.async_payment_succeeded
    • payment_intent.succeeded
    • charge.refunded
    • refund.created
    • refund.updated
    • charge.dispute.created
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • customer.subscription.trial_will_end
    • customer.subscription.paused
    • customer.subscription.resumed
    • invoice.paid
    • invoice.upcoming
    • invoice.payment_succeeded
    • invoice.payment_failed
    • invoice.payment_action_required
  5. Save and copy the Signing secret (whsec_...)
Terminal window
nano .env.production

Add/update:

STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret

Restart the application:

Terminal window
docker compose restart admin-panel
  1. Go to: https://your-domain.com/login
  2. Enter your email
  3. Click “Send Magic Link”
  4. Check your email inbox and click the link
  5. The first account automatically gets administrator privileges!
  1. After logging in, go to: https://your-domain.com/dashboard
  2. Check the Admin section: https://your-domain.com/admin/products
  3. Create your first test product
  1. Create a product with a test price (e.g. 10 PLN)
  2. Go to the product page: https://your-domain.com/p/product-slug
  3. Use the Stripe test card: 4242 4242 4242 4242
  4. Verify that the payment went through
Terminal window
# Status of all containers
docker compose ps
# Resource usage
docker stats
# Real-time logs
docker compose logs -f
# Logs of a specific service
docker compose logs -f admin-panel
docker compose logs -f db

Logs are available in containers:

Terminal window
# Admin Panel
docker compose exec admin-panel sh
ls -la /app/.next/
# Database - PostgreSQL logs
docker compose logs db | grep ERROR
# Nginx
docker compose logs nginx
Terminal window
# Connect to the database
docker compose exec db psql -U postgres
# Check database size
SELECT pg_size_pretty(pg_database_size('postgres'));
# Check active connections
SELECT count(*) FROM pg_stat_activity;
# Check most popular queries
SELECT query, calls, total_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Terminal window
# Go to the project directory
cd ~/sellf
# Stop the application
docker compose down
# Pull the latest code
git pull origin main
# Rebuild images
docker compose build --no-cache
# Start again
docker compose up -d
# Check logs
docker compose logs -f admin-panel
Terminal window
# New migration will appear in supabase/migrations/
ls -la supabase/migrations/
# Execute the migration
docker compose exec db psql -U postgres -d postgres -f /tmp/migrations/NEW_MIGRATION.sql

ALWAYS make a backup before updating!

Terminal window
# Database backup
docker compose exec db pg_dump -U postgres postgres > backup_$(date +%Y%m%d_%H%M%S).sql
# Volume backup
docker run --rm \
-v sellf_postgres_data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/postgres_backup_$(date +%Y%m%d_%H%M%S).tar.gz /data

Create a backup script:

Terminal window
nano ~/backup-sellf.sh

Contents:

#!/bin/bash
BACKUP_DIR="/home/$(whoami)/backups/sellf"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# Database backup
docker compose -f /home/$(whoami)/sellf/docker-compose.yml \
exec -T db pg_dump -U postgres postgres | gzip > $BACKUP_DIR/db_$DATE.sql.gz
# Remove old backups (older than 7 days)
find $BACKUP_DIR -name "db_*.sql.gz" -mtime +7 -delete
echo "Backup completed: $BACKUP_DIR/db_$DATE.sql.gz"

Set permissions and cron:

Terminal window
chmod +x ~/backup-sellf.sh
# Add to cron (backup daily at 2:00 AM)
crontab -e
# Add the line:
0 2 * * * /home/yourusername/backup-sellf.sh >> /home/yourusername/backup-sellf.log 2>&1
Terminal window
# Stop the application
cd ~/sellf
docker compose down
# Restore the database
gunzip -c ~/backups/sellf/db_20250126_020000.sql.gz | \
docker compose run --rm -T db psql -U postgres
# Start again
docker compose up -d
Terminal window
# Volume backup (storage, uploads, etc.)
docker run --rm \
-v sellf_storage_data:/data \
-v ~/backups/sellf:/backup \
alpine tar czf /backup/storage_$(date +%Y%m%d).tar.gz /data
Terminal window
# Check logs
docker compose logs
# Check configuration
docker compose config
# Remove everything and start from scratch
docker compose down -v
docker compose up -d
Terminal window
# Check status
docker compose ps db
# Check logs
docker compose logs db
# Restart the database
docker compose restart db
# If that doesn't help, check free disk space
df -h
Terminal window
# Check logs
docker compose logs admin-panel
# Check environment variables
docker compose exec admin-panel env | grep SUPABASE
# Restart the panel
docker compose restart admin-panel
  1. Check SMTP configuration:
Terminal window
docker compose logs auth | grep SMTP
  1. Check GOTRUE_URI_ALLOW_LIST in .env.production

  2. Check if the email arrived (check spam)

  1. Check webhook secret:
Terminal window
docker compose exec admin-panel env | grep STRIPE
  1. Check webhook logs in Stripe Dashboard

  2. Test the endpoint manually:

Terminal window
curl -X POST https://your-domain.com/api/webhooks/stripe \
-H "stripe-signature: test" \
-d '{}'
Terminal window
# Check space
df -h
# Remove unused images
docker image prune -a
# Remove unused volumes
docker volume prune
# Remove old logs
docker compose logs --tail=0
  1. Check resource usage:
Terminal window
docker stats
  1. Add more RAM or CPU in server settings

  2. Optimize the database:

Terminal window
docker compose exec db psql -U postgres -c "VACUUM ANALYZE;"
  1. Add indexes to frequently used columns

After deployment, check:

  • All passwords are long and secure
  • .env.production is NOT in the Git repository
  • Firewall is configured (only ports 22, 80, 443)
  • SSL/TLS is enabled (HTTPS)
  • Backups are configured and tested
  • SMTP uses an encrypted connection
  • Stripe is in production mode (keys pk_live_ and sk_live_)
  • Rate limiting is enabled
  • Logs do not contain sensitive data
  • Monitoring is configured

Congratulations! Sellf is now running in production! 🎉