Deployment
Production setup options, environment variables, and operational runbook.
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
| Variable | Required | Description |
|---|---|---|
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. |
.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.
-
Copy the production compose file to your server
scp docker-compose.prod.yml user@yourserver:~/waffle/ scp .env user@yourserver:~/waffle/ -
Start the services
This pulls the pinned image from GHCR, starts PostgreSQL, runs migrations, and starts the app.docker compose -f docker-compose.prod.yml up -d -
Verify it's running
curl http://localhost:8383/health # {"status":"ok","db":"connected"} -
Open the admin and change your password
Navigate tohttps://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
- Connect your GitHub fork of the repository to a new Railway project.
- Add the PostgreSQL plugin to the project. Railway will inject
DATABASE_URLautomatically. - Set environment variables in the Railway dashboard:
JWT_SECRET,ADMIN_PASSWORD. - Deploy. Railway auto-detects the
Dockerfileat the repo root.
Option 3: Render
- Create a new Web Service on Render and connect your GitHub repo.
- Set the Runtime to Docker.
- Create a PostgreSQL database on Render and copy its connection string.
- Set environment variables:
DATABASE_URL,JWT_SECRET,ADMIN_PASSWORD. - 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.
| Channel | Tag | Description |
|---|---|---|
| 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. |
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:
- WebSocket connections (
wss://) require a secure context - Service workers (PWA) only register on HTTPS or
localhost - Secure cookies (used for JWT) only work over HTTPS
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
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
- HTTPS is required for service worker registration in production.
- Cached pages: home, waffle list, waffle detail, buyer stats. Admin routes (
/admin/*) are excluded from the service worker. - Cache invalidation: deploying a new version automatically invalidates the service worker cache. Returning users see an "Update Available" toast and can reload to get the latest version.
- Installable: the app ships a Web App Manifest (
/manifest.json), 192Ã192 and 512Ã512 icons, and a maskable icon. Buyers can add the spot page to their home screen for a native-app experience.
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:
- Viewport is locked to prevent unintended zoom on input focus
- Touch targets are a minimum of 44Ã44 px
- No pull-to-refresh conflicts
- 300ms tap delay is eliminated
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
-
Update
WAFFLE_VERSIONin your.envfile to the new tag (e.g.v0.1.18). -
Pull the new image and restart
docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d -
Migrations run automatically on startup. Check logs to confirm:
docker compose -f docker-compose.prod.yml logs app | grep migration -
Verify
curl https://yourdomain.com/health