âš ī¸ Prerequisites
All production deployment methods require Docker and Docker Compose v2+. You also need a publicly accessible domain and HTTPS for WebSockets and PWA to work correctly.

Environment Variables

Create a .env file in the project root (copy from .env.example):

# Docker image version — pin this for reproducible deploys
WAFFLE_VERSION=v0.1.18

# PostgreSQL connection string
DATABASE_URL=postgres://syrup:your_secure_password@postgres:5432/syrup?sslmode=disable

# JWT signing secret — generate with: openssl rand -base64 32
JWT_SECRET=your-secure-random-secret-here

# Default admin password — change immediately after first login
ADMIN_PASSWORD=your-secure-admin-password

# Trusted reverse proxy CIDRs (optional — defaults to RFC 1918)
# TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
VariableRequiredDescription
WAFFLE_VERSION✅Docker image tag to pull. Use a pinned version for production (e.g. v0.1.18). Use latest only if you want auto-tracking.
DATABASE_URL✅Full PostgreSQL connection string including credentials, host, port, and database name.
JWT_SECRET✅Random secret for signing admin session tokens. Minimum 32 characters. Changing this invalidates all active sessions.
ADMIN_PASSWORD✅Password for the default admin account created on first run. Change immediately after first login.
TRUSTED_PROXIES❌Comma-separated CIDR ranges for trusted reverse proxies. Used to correctly extract the real client IP for login history and lockout tracking. Defaults to RFC 1918 ranges if unset or invalid. Set this to your load balancer or Cloudflare IP range when running behind a proxy.
🔐 Secret management
Never commit your .env file to version control. Use your platform's secret management (Railway Variables, Render Environment, Fly.io Secrets) instead of a plain .env file on hosted platforms.

Option 1: Docker Compose (Single Server)

The recommended approach for a VPS or dedicated server.

  1. Copy the production compose file to your server
    scp docker-compose.prod.yml user@yourserver:~/waffle/
    scp .env user@yourserver:~/waffle/
  2. Start the services
    docker compose -f docker-compose.prod.yml up -d
    This pulls the pinned image from GHCR, starts PostgreSQL, runs migrations, and starts the app.
  3. Verify it's running
    curl http://localhost:8383/health
    # {"status":"ok","db":"connected"}
  4. Open the admin and change your password
    Navigate to https://yourdomain.com/admin/login.

Common operations

# View logs (follow)
docker compose -f docker-compose.prod.yml logs -f

# Restart the app container only
docker compose -f docker-compose.prod.yml restart app

# Stop everything
docker compose -f docker-compose.prod.yml down

Option 2: Railway

  1. Connect your GitHub fork of the repository to a new Railway project.
  2. Add the PostgreSQL plugin to the project. Railway will inject DATABASE_URL automatically.
  3. Set environment variables in the Railway dashboard: JWT_SECRET, ADMIN_PASSWORD.
  4. Deploy. Railway auto-detects the Dockerfile at the repo root.

Option 3: Render

  1. Create a new Web Service on Render and connect your GitHub repo.
  2. Set the Runtime to Docker.
  3. Create a PostgreSQL database on Render and copy its connection string.
  4. Set environment variables: DATABASE_URL, JWT_SECRET, ADMIN_PASSWORD.
  5. Deploy.

Option 4: Fly.io

# Install flyctl
curl -L https://fly.io/install.sh | sh

# Launch the app (uses the root Dockerfile)
fly launch --dockerfile Dockerfile --name syrup-app

# Create a managed Postgres database
fly postgres create --name syrup-db

# Attach the database (sets DATABASE_URL automatically)
fly postgres attach --app syrup-app syrup-db

# Set remaining secrets
fly secrets set JWT_SECRET="your-secret" ADMIN_PASSWORD="your-password"

# Deploy
fly deploy

Release Channels

Pre-built images are published to ghcr.io/notfixingit3/waffle for linux/amd64 and linux/arm64.

ChannelTagDescription
Pinned v0.1.18 A specific stable release. Recommended for production. Fully reproducible.
Stable latest Tracks the latest stable release from the main branch. Auto-updates on pull.
Dev dev Tracks the dev branch. For staging and testing only — may be unstable.
💡 Pin in production
Always pin to a specific version tag in production (e.g. WAFFLE_VERSION=v0.1.18). Using latest means a docker compose pull could silently update your app.

SSL / HTTPS

HTTPS is required for production because:

Recommended: Put Cloudflare or nginx in front for SSL termination. The Go app itself serves plain HTTP behind the proxy.

nginx Reverse Proxy

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # Regular HTTP traffic
    location / {
        proxy_pass         http://localhost:8383;
        proxy_http_version 1.1;
        proxy_set_header   Host            $host;
        proxy_set_header   X-Real-IP       $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # WebSocket upgrade — required for real-time spot updates
    location /ws {
        proxy_pass         http://localhost:8383;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade    $http_upgrade;
        proxy_set_header   Connection "upgrade";
        proxy_set_header   Host       $host;
        proxy_read_timeout 86400s;
    }
}

# Redirect HTTP → HTTPS
server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$host$request_uri;
}

Database Migrations

Migrations run automatically every time the backend container starts. They are safe to run on an already-migrated database (idempotent via migration tracking).

To run migrations manually (e.g. after a restore):

docker compose exec app ./main migrate

Migration files live in backend/migrations/ and are numbered sequentially (001_, 002_, â€Ļ). Each has an .up.sql and a .down.sql.

Backup & Restore

Backup

# Dump the database to a file
docker compose exec postgres pg_dump -U syrup syrup > backup-$(date +%Y%m%d).sql

Restore

# Restore from a dump file
docker compose exec -T postgres psql -U syrup syrup < backup-20260531.sql
💡 Automate backups
Set up a daily cron job to dump the database and ship it to S3, Backblaze B2, or any object store. A simple approach: pg_dump | gzip | aws s3 cp - s3://your-bucket/backup-$(date +%Y%m%d).sql.gz

Monitoring

The app exposes a health check endpoint:

GET /health
# 200 OK
{"status": "ok", "db": "connected"}

It returns 503 if the database connection is unhealthy. Set up an external uptime monitor (UptimeRobot, Better Uptime, Freshping) to ping /health every 5 minutes.

The app also exposes a readiness probe at GET /ready (same response format) intended for container orchestrators.

PWA Notes

Instagram In-App Browser Notes

The buyer-facing UI is optimized specifically for Instagram's in-app browser, which is WebKit on iOS and Chrome Custom Tabs on Android:

Test by DMing your waffle URL to yourself inside Instagram and opening it there — don't only test in a desktop browser.

Updating to a New Release

  1. Update WAFFLE_VERSION in your .env file to the new tag (e.g. v0.1.18).
  2. Pull the new image and restart
    docker compose -f docker-compose.prod.yml pull
    docker compose -f docker-compose.prod.yml up -d
  3. Migrations run automatically on startup. Check logs to confirm:
    docker compose -f docker-compose.prod.yml logs app | grep migration
  4. Verify
    curl https://yourdomain.com/health
â„šī¸ Zero-downtime updates
Docker Compose will stop the old container, start the new one, and run migrations. There will be a brief downtime (typically under 10 seconds) during the container swap. For zero-downtime deploys, put a load balancer in front with multiple instances, or use a platform like Railway/Render that handles rolling deploys natively.