Self-hosting
Sokisoko ships as one Go API, one background worker, one PostgreSQL database and three web front-ends (admin console, vendor portal, storefront) that all talk to the same API. Docker Compose runs the backend; a reverse proxy (Caddy in the reference deployment) serves the front-ends and routes each tenant's domain.
What runs
| Service | Role |
|---|---|
postgres | PostgreSQL 16 with pgvector — data, the job queue and full-text search all live here |
migrate | One-shot: applies the embedded SQL migrations idempotently, then exits |
seed | Optional one-shot: creates the platform superadmin (and demo data when SEED_DEMO is on) |
api | The HTTP API on :8080 |
worker | Background jobs — email, invoice PDFs, ERP sweeps, automations, digests |
gotenberg | Renders invoice PDFs; without it the API uses a stub renderer |
Uploaded media (product photos, logos, renditions) lives on the mediadata volume at MEDIA_ROOT. In a multi-node deployment that volume must be shared.
First start
- Create a
.envnext todocker-compose.ymlwith at leastPOSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB,JWT_SECRET(32+ random bytes —openssl rand -base64 32) and, for production,CONFIG_ENCRYPTION_KEY,SUPERADMIN_EMAILandSUPERADMIN_PASSWORD. See Configuration for everything else. Never commit.env. - Bring the stack up:
docker compose up -d --build
- Check health:
curl -s localhost:8080/healthz # liveness
curl -s localhost:8080/readyz # readiness — pings the database
- Sign in to the admin console with the superadmin email and password you set. In development the seed creates
admin@demo.test/admin1234instead; production refuses to start with it.
Production guards
When ENV=production, the API validates its configuration at boot and exits rather than run unsafely:
SUPERADMIN_PASSWORDmust be at least 12 characters and not a default such asadmin1234,passwordorchangeme.CONFIG_ENCRYPTION_KEYmust be set and distinct fromJWT_SECRET— it seals tenant credentials (payment keys, ERP secrets, mailboxes) at rest.JWT_SECRETmust be at least 32 characters.SEED_DEMOmust be off — it creates demo logins.
Tenant domains
One installation hosts many organizations. Each storefront website has a domain (Settings → Websites, plus verified custom domains), and every storefront request is bound to a website by the host it arrives on — there is no fallback. Requests on an unknown host get "No store is configured for this address". The admin console is served under /console/ and the vendor portal under /portal/ on the same tenant host; the platform's own domain is PLATFORM_BASE_DOMAIN.
In the reference Caddy configuration every tenant host proxies /media/* to the API, serves the two SPAs from static builds, and proxies everything else to the storefront's server-side renderer, with on-demand TLS. If your proxy sits in front of the API on a different host, forward the store's host in X-Forwarded-Host (or X-Tenant-Host) so tenant resolution still works — see Authentication.
Email, PDFs and integrations
- Email sends through Mailgun when
MAILGUN_API_KEYandMAILGUN_DOMAINare set, otherwise SMTP whenSMTP_HOSTis set, otherwise it is logged. Tenants can bring their own mailbox or Mailgun account from Settings → Configuration. - PDFs need
GOTENBERG_URL; the Compose file wires it. - ERP providers that use OAuth (QuickBooks Online, Xero, Business Central) need platform app credentials in the environment before the admin can connect them; SAP, SAP Business One, Odoo and the custom webhook need nothing platform-side. See ERP and accounting sync.
- The agent API (OAuth 2.1 + MCP) is mounted only when
API_PUBLIC_URLis set. See AI agents and MCP.
Upgrading
Pull the new images or source, then docker compose up -d --build. The migrate service applies any new migrations before the API starts; migrations are forward-only. Keep a database backup routine outside the containers.