Sellf - Production Deployment Guide
Complete guide for deploying Sellf on a production server using Docker Compose.
Table of Contents
Section titled “Table of Contents”- Requirements
- Server Preparation
- Environment Variables Configuration
- Database Configuration
- Starting the Application
- Domain and SSL Configuration
- Stripe Webhooks Configuration
- Initial Setup
- Monitoring and Logs
- Updating
- Backup and Restore
- Troubleshooting
Requirements
Section titled “Requirements”Minimum Hardware Requirements
Section titled “Minimum Hardware Requirements”- CPU: 2 vCPU
- RAM: 4 GB (recommended: 8 GB)
- Disk: 20 GB SSD (recommended: 50 GB)
- Transfer: 100 GB/month
Recommended Server (Sellf + Supabase self-hosted on one machine)
Section titled “Recommended Server (Sellf + Supabase self-hosted on one machine)”Hetzner CX33 — tested and recommended for early production:
| Spec | Value |
|---|---|
| CPU | 4 vCPU (AMD, shared) |
| RAM | 8 GB |
| Disk | 80 GB NVMe |
| Transfer | 20 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):
sudo fallocate -l 2G /swapfilesudo chmod 600 /swapfilesudo mkswap /swapfilesudo swapon /swapfileecho '/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.confsudo sysctl -pNote: LXC-based VPS (e.g. Mikrus) does not support swap — kernel access is blocked by the host.
Software
Section titled “Software”- 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
External Services
Section titled “External Services”- Domain: your own domain with DNS access
- SMTP: email service (SendGrid, AWS SES, Mailgun, etc.)
- Stripe: production account
- Cloudflare Turnstile: account (optional, for CAPTCHA)
Server Preparation
Section titled “Server Preparation”1. System Update
Section titled “1. System Update”sudo apt update && sudo apt upgrade -y2. Docker Installation
Section titled “2. Docker Installation”# Remove old versionssudo apt remove docker docker-engine docker.io containerd runc
# Install dependenciessudo apt install -y \ apt-transport-https \ ca-certificates \ curl \ gnupg \ lsb-release
# Add official Docker GPG keycurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repositoryecho \ "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 Enginesudo apt updatesudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Verify installationdocker --versiondocker compose version3. Docker Configuration (optional but recommended)
Section titled “3. Docker Configuration (optional but recommended)”# Add user to docker group (avoid sudo)sudo usermod -aG docker $USER
# Log in again or:newgrp docker
# Configure Docker to start automaticallysudo systemctl enable dockersudo systemctl start docker4. Git Installation
Section titled “4. Git Installation”sudo apt install -y git5. Firewall Configuration
Section titled “5. Firewall Configuration”# Enable UFWsudo ufw enable
# Allow SSHsudo ufw allow 22/tcp
# Allow HTTP and HTTPSsudo ufw allow 80/tcpsudo ufw allow 443/tcp
# Check statussudo ufw statusEnvironment Variables Configuration
Section titled “Environment Variables Configuration”1. Download Source Code
Section titled “1. Download Source Code”# Go to home directorycd ~
# Clone the repositorygit clone https://github.com/your-organization/sellf.gitcd sellf2. Create Configuration File
Section titled “2. Create Configuration File”# Copy the example filecp .env.production.example .env.production
# Edit the filenano .env.production3. Generate Secure Keys
Section titled “3. Generate Secure Keys”# Generate JWT_SECRETopenssl rand -base64 32
# Generate REALTIME_SECRET_KEY_BASEopenssl rand -base64 32
# Generate POSTGRES_PASSWORD (long password)openssl rand -base64 484. Fill In All Variables
Section titled “4. Fill In All Variables”Below you will find a detailed description of each variable:
Database
Section titled “Database”POSTGRES_PASSWORD=your_very_secure_postgresql_passwordJWT and Authorization
Section titled “JWT and Authorization”JWT_SECRET=paste_generated_jwt_secretREALTIME_SECRET_KEY_BASE=paste_generated_realtime_secretANON_KEY=get_from_supabase_dashboardSERVICE_ROLE_KEY=get_from_supabase_dashboardNote: 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.
URLs and Domains
Section titled “URLs and Domains”API_EXTERNAL_URL=https://api.your-domain.comNEXT_PUBLIC_SUPABASE_URL=https://api.your-domain.comGOTRUE_SITE_URL=https://your-domain.comNEXT_PUBLIC_SITE_URL=https://your-domain.comNEXT_PUBLIC_BASE_URL=https://your-domain.comMAIN_DOMAIN=your-domain.comGOTRUE_URI_ALLOW_LIST=https://your-domain.com/*,https://www.your-domain.com/*SMTP (Email)
Section titled “SMTP (Email)”Example for SendGrid:
SMTP_ADMIN_EMAIL=noreply@your-domain.comSMTP_HOST=smtp.sendgrid.netSMTP_PORT=587SMTP_USER=apikeySMTP_PASS=SG.xxxxxxxxxxxxxxxxxxxxxxxxxSMTP_SENDER_NAME=SellfExample for Gmail:
SMTP_HOST=smtp.gmail.comSMTP_PORT=587SMTP_USER=your-email@gmail.comSMTP_PASS=your-app-passwordStripe - Choose ONE Configuration Method
Section titled “Stripe - Choose ONE Configuration Method”METHOD 1: .env Configuration (Recommended for developers, Docker, CI/CD)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxxSTRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxx # Standard Secret Key or Restricted KeySTRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxMETHOD 2: Admin Panel Wizard (Recommended for non-technical users)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxxSTRIPE_ENCRYPTION_KEY=ONIgOXqmoHOYZphEDkhydpL4briQsVlS9IS3o59mW9E= # Generate: openssl rand -base64 32STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxThen 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.
Cloudflare Turnstile (CAPTCHA)
Section titled “Cloudflare Turnstile (CAPTCHA)”NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY=1x00000000000000000000AACLOUDFLARE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA5. Stripe Configuration
Section titled “5. Stripe Configuration”Sellf supports two equivalent methods for Stripe configuration. Choose the one that best fits your use case.
Method 1: .env Configuration (Recommended for developers)
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:
-
Get the Secret Key from Stripe Dashboard:
- Test Mode: https://dashboard.stripe.com/test/apikeys
- Live Mode: https://dashboard.stripe.com/apikeys
- You can use the Standard Secret Key (
sk_test_orsk_live_) - Or a Restricted Key (
rk_test_orrk_live_) with the appropriate permissions
-
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 -
Restart the application:
Terminal window docker compose restart admin-panel -
Verify in Settings:
- Go to:
https://your-domain.com/dashboard/settings - You should see a blue banner: “Currently using: .env configuration”
- Go to:
Method 2: Admin Panel Wizard (Recommended for non-technical users)
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:
-
Generate an encryption key (one-time):
Terminal window openssl rand -base64 32 -
Add the key to
.env.production:Terminal window echo "STRIPE_ENCRYPTION_KEY=YOUR_GENERATED_KEY" >> .env.production⚠️ CRITICAL: Never commit this key to Git!
-
Restart the application:
Terminal window docker compose restart admin-panel -
Open the wizard:
- Go to:
https://your-domain.com/dashboard/settings - Click the “Configure Stripe” button
- Go to:
-
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:
- Open Stripe Dashboard
- Go to API Keys → Create restricted key
- Set permissions:
- ✅ Charges: Write
- ✅ Customers: Write
- ✅ Checkout Sessions: Write
- ✅ Payment Intents: Read
- ✅ Webhooks: Read (optional)
- Copy the key (starts with
rk_test_orrk_live_) - 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”
-
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
Switching Between Methods
Section titled “Switching Between Methods”From .env to Wizard:
- Simply launch the wizard and configure the key
- Database configuration takes priority over .env
- You can leave the
STRIPE_SECRET_KEYvariable in .env as a fallback
From Wizard to .env:
- Add
STRIPE_SECRET_KEYto .env - 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;" - Restart the application
Testing Configuration
Section titled “Testing Configuration”Test with a Stripe test card:
- Create a test product in the Admin Panel
- Go to the product page
- Click “Buy Now”
- Use the test card:
4242 4242 4242 4242- Expiry: any future date (e.g. 12/34)
- CVC: any 3 digits (e.g. 123)
- Verify the payment in:
- Dashboard → Payments
- Stripe Dashboard → Payments
📖 Full testing guide: See the Stripe Testing Guide
Required Database Migrations
Section titled “Required Database Migrations”The wizard requires the stripe_configurations table in the database:
# Check if the migration existsls -la supabase/migrations/ | grep stripe
# Should be: 20251227000000_stripe_rak_configuration.sqlIf the migration does not exist, it will be automatically executed during database startup.
Database Configuration
Section titled “Database Configuration”1. Prepare Migrations
Section titled “1. Prepare Migrations”Check that all migrations are in place:
ls -la supabase/migrations/The following files should be present:
20250709000000_initial_schema.sql- Initial schema20250717000000_payment_system.sql- Payment system20251227000000_stripe_rak_configuration.sql- Stripe configuration (wizard)20251227100000_shop_config.sql- Shop configuration- others…
2. Optionally: Modify Seed Data
Section titled “2. Optionally: Modify Seed Data”If you want to have your own sample data:
nano supabase/seed.sqlStarting the Application
Section titled “Starting the Application”1. Build and Start Containers
Section titled “1. Build and Start Containers”# Make sure you are in the main project directorycd ~/sellf
# Build images (may take a few minutes on the first run)docker compose build
# Start all servicesdocker compose up -d
# Check container statusdocker compose psExpected output:
NAME STATUS PORTSsellf-admin running 0.0.0.0:3000->3000/tcpsellf-db running (healthy) 0.0.0.0:5432->5432/tcpsellf-auth runningsellf-rest runningsellf-storage runningsellf-nginx running 0.0.0.0:8080->80/tcp...2. Check Logs
Section titled “2. Check Logs”# All containersdocker compose logs -f
# Specific containerdocker compose logs -f admin-paneldocker compose logs -f db3. Initialize Database
Section titled “3. Initialize Database”If the database was automatically initialized (migrations in /docker-entrypoint-initdb.d), you can skip this step. Otherwise:
# Connect to the databasedocker compose exec db psql -U postgres
# Check tables\dt
# Exit\qIf the tables do not exist, run migrations manually:
# Copy migrations to the containerdocker compose cp supabase/migrations/. db:/tmp/migrations/
# Execute migrationsdocker compose exec db psql -U postgres -d postgres -f /tmp/migrations/20250709000000_initial_schema.sqldocker compose exec db psql -U postgres -d postgres -f /tmp/migrations/20250717000000_payment_system.sqlDomain and SSL Configuration
Section titled “Domain and SSL Configuration”Option 1: Nginx Proxy Manager (Recommended for beginners)
Section titled “Option 1: Nginx Proxy Manager (Recommended for beginners)”- Install Nginx Proxy Manager:
# Create a separate directorymkdir ~/nginx-proxy-managercd ~/nginx-proxy-manager
# Download docker-compose.yml for NPMwget https://github.com/NginxProxyManager/nginx-proxy-manager/blob/main/docker-compose.yml
# Startdocker compose up -d-
Log in to the panel:
http://your-server:81- Email:
admin@example.com - Password:
changeme
- Email:
-
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)
- Domain:
-
Add a second Proxy Host for the API:
- Domain:
api.your-domain.com - Forward Hostname:
kong - Forward Port:
8000 - SSL: ✅
- Domain:
-
Add a third Proxy Host for examples:
- Domain:
examples.your-domain.com(optional) - Forward Hostname:
nginx - Forward Port:
80 - SSL: ✅
- Domain:
Option 2: Certbot + Nginx (For advanced users)
Section titled “Option 2: Certbot + Nginx (For advanced users)”# Install Certbotsudo apt install -y certbot python3-certbot-nginx
# Obtain certificatesudo certbot --nginx -d your-domain.com -d www.your-domain.com -d api.your-domain.com
# Automatic renewalsudo systemctl enable certbot.timerDNS Configuration
Section titled “DNS Configuration”Set DNS records with your provider:
Type Name Value TTLA @ YOUR_SERVER_IP 3600A www YOUR_SERVER_IP 3600A api YOUR_SERVER_IP 3600Stripe Webhooks Configuration
Section titled “Stripe Webhooks Configuration”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”- Go to: https://dashboard.stripe.com/webhooks
- Click “Add endpoint”
- URL:
https://your-domain.com/api/webhooks/stripe - Select events:
checkout.session.completedcheckout.session.async_payment_succeededpayment_intent.succeededcharge.refundedrefund.createdrefund.updatedcharge.dispute.createdcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.subscription.trial_will_endcustomer.subscription.pausedcustomer.subscription.resumedinvoice.paidinvoice.upcominginvoice.payment_succeededinvoice.payment_failedinvoice.payment_action_required
- Save and copy the Signing secret (
whsec_...)
2. Update Environment Variables
Section titled “2. Update Environment Variables”nano .env.productionAdd/update:
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secretRestart the application:
docker compose restart admin-panelInitial Setup
Section titled “Initial Setup”1. Create First Administrator Account
Section titled “1. Create First Administrator Account”- Go to:
https://your-domain.com/login - Enter your email
- Click “Send Magic Link”
- Check your email inbox and click the link
- The first account automatically gets administrator privileges!
2. Test the Dashboard
Section titled “2. Test the Dashboard”- After logging in, go to:
https://your-domain.com/dashboard - Check the Admin section:
https://your-domain.com/admin/products - Create your first test product
3. Test a Payment
Section titled “3. Test a Payment”- Create a product with a test price (e.g. 10 PLN)
- Go to the product page:
https://your-domain.com/p/product-slug - Use the Stripe test card:
4242 4242 4242 4242 - Verify that the payment went through
Monitoring and Logs
Section titled “Monitoring and Logs”Checking Status
Section titled “Checking Status”# Status of all containersdocker compose ps
# Resource usagedocker stats
# Real-time logsdocker compose logs -f
# Logs of a specific servicedocker compose logs -f admin-paneldocker compose logs -f dbApplication Logs
Section titled “Application Logs”Logs are available in containers:
# Admin Paneldocker compose exec admin-panel shls -la /app/.next/
# Database - PostgreSQL logsdocker compose logs db | grep ERROR
# Nginxdocker compose logs nginxDatabase Monitoring
Section titled “Database Monitoring”# Connect to the databasedocker compose exec db psql -U postgres
# Check database sizeSELECT pg_size_pretty(pg_database_size('postgres'));
# Check active connectionsSELECT count(*) FROM pg_stat_activity;
# Check most popular queriesSELECT query, calls, total_exec_timeFROM pg_stat_statementsORDER BY total_exec_time DESCLIMIT 10;Updating
Section titled “Updating”Updating the Code
Section titled “Updating the Code”# Go to the project directorycd ~/sellf
# Stop the applicationdocker compose down
# Pull the latest codegit pull origin main
# Rebuild imagesdocker compose build --no-cache
# Start againdocker compose up -d
# Check logsdocker compose logs -f admin-panelUpdating the Database (Migrations)
Section titled “Updating the Database (Migrations)”# New migration will appear in supabase/migrations/ls -la supabase/migrations/
# Execute the migrationdocker compose exec db psql -U postgres -d postgres -f /tmp/migrations/NEW_MIGRATION.sqlBackup Before Updating
Section titled “Backup Before Updating”ALWAYS make a backup before updating!
# Database backupdocker compose exec db pg_dump -U postgres postgres > backup_$(date +%Y%m%d_%H%M%S).sql
# Volume backupdocker 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 /dataBackup and Restore
Section titled “Backup and Restore”Automatic Database Backup
Section titled “Automatic Database Backup”Create a backup script:
nano ~/backup-sellf.shContents:
#!/bin/bashBACKUP_DIR="/home/$(whoami)/backups/sellf"DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# Database backupdocker 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:
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>&1Restoring from Backup
Section titled “Restoring from Backup”# Stop the applicationcd ~/sellfdocker compose down
# Restore the databasegunzip -c ~/backups/sellf/db_20250126_020000.sql.gz | \ docker compose run --rm -T db psql -U postgres
# Start againdocker compose up -dFile Backup
Section titled “File Backup”# 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 /dataTroubleshooting
Section titled “Troubleshooting”Problem: Containers won’t start
Section titled “Problem: Containers won’t start”# Check logsdocker compose logs
# Check configurationdocker compose config
# Remove everything and start from scratchdocker compose down -vdocker compose up -dProblem: Database not responding
Section titled “Problem: Database not responding”# Check statusdocker compose ps db
# Check logsdocker compose logs db
# Restart the databasedocker compose restart db
# If that doesn't help, check free disk spacedf -hProblem: Admin Panel returns 500
Section titled “Problem: Admin Panel returns 500”# Check logsdocker compose logs admin-panel
# Check environment variablesdocker compose exec admin-panel env | grep SUPABASE
# Restart the paneldocker compose restart admin-panelProblem: Magic link doesn’t work
Section titled “Problem: Magic link doesn’t work”- Check SMTP configuration:
docker compose logs auth | grep SMTP-
Check
GOTRUE_URI_ALLOW_LISTin.env.production -
Check if the email arrived (check spam)
Problem: Stripe payments don’t work
Section titled “Problem: Stripe payments don’t work”- Check webhook secret:
docker compose exec admin-panel env | grep STRIPE-
Check webhook logs in Stripe Dashboard
-
Test the endpoint manually:
curl -X POST https://your-domain.com/api/webhooks/stripe \ -H "stripe-signature: test" \ -d '{}'Problem: No disk space
Section titled “Problem: No disk space”# Check spacedf -h
# Remove unused imagesdocker image prune -a
# Remove unused volumesdocker volume prune
# Remove old logsdocker compose logs --tail=0Problem: Slow performance
Section titled “Problem: Slow performance”- Check resource usage:
docker stats-
Add more RAM or CPU in server settings
-
Optimize the database:
docker compose exec db psql -U postgres -c "VACUUM ANALYZE;"- Add indexes to frequently used columns
Support and Documentation
Section titled “Support and Documentation”- Sellf Documentation:
/CLAUDE.mdin the repository - Docker Documentation: https://docs.docker.com/
- Supabase Documentation: https://supabase.com/docs
- Stripe Documentation: https://stripe.com/docs
- GitHub Issues: [link to repository]
Security - Checklist
Section titled “Security - Checklist”After deployment, check:
- All passwords are long and secure
-
.env.productionis 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_andsk_live_) - Rate limiting is enabled
- Logs do not contain sensitive data
- Monitoring is configured
Congratulations! Sellf is now running in production! 🎉