Blog

Run many stores from one Docker Compose file

An agency with a shop per client, or a supplier with a store per dealer, can host them two ways: one install per store, each upgraded on its own, or one install that holds them all.

GoCommerce does the second in what it calls platform mode, and the repository now ships docker-compose.platform.yml to start it with one command. If your “stores” are one merchant’s storefronts over one catalogue, you want channels instead — the multi-store page explains both.

What platform mode is

gocommerce platform serves many stores from one process and one PostgreSQL database. Each store is an ordinary GoCommerce store, with its own operators, admin panel, API, background work and modules. Nothing in the engine knows it is one of many.

QuestionAnswer
How are stores kept apart?A PostgreSQL schema per store, store_<slug>, not a store_id column a query could forget. Each store’s engine is pointed at its own schema.
Where is the list of stores?In the platform’s own schema, gocommerce_platform: stores and their domains.
How does a request find its store?By the host it was sent to: <slug>.<base domain>, or a custom domain attached to the store. An unknown host gets a JSON 404.
How is a store created?From the console or the platform API, all or nothing: schema, migrations, first owner and domains.
Who manages stores?Whoever holds the platform token. It opens no store’s admin API, and no store’s token opens the platform.

What the Compose file starts

Two services and two volumes, with the same names as the single-store docker-compose.yml.

PieceWhat it is
gocommerceThe engine, built from the repository’s Dockerfile and run as platform, on port ${PORT:-8080}.
postgrespostgres:17-alpine. The engine waits for its health check before starting.
media volumeUploads, at /data/media. Each store writes to its own folder, /data/media/<slug>.
pgdata volumeThe database: every store’s schema, and the platform’s.

The engine starts with 20 module flags — the same list as the single-store file: -identity, -webhooks, -menus, -reviews, -contact, -newsletter, -resend, -sendgrid, -twilio, -msg91, -invoices, -cms, -faq, -wishlist, -gateways, -carriers, -feeds, -sitemaps, -b2b and -recovery. Each one sits idle until a store configures it. -import-amazon is left out because it drives a real Chrome, and -meilisearch and -klaviyo are not in the list either.

Bring it up

Compose refuses to start without these three variables, and the platform token must be at least 16 characters.

Start the platform

git clone https://github.com/itswadesh/gocommerce
cd gocommerce
POSTGRES_PASSWORD=... \
GOCOMMERCE_PLATFORM_TOKEN=... \
GOCOMMERCE_BASE_DOMAIN=shops.example.com \
docker compose -f docker-compose.platform.yml up --build

From docker-compose.platform.yml. Set GOCOMMERCE_PLATFORM_HOST to put the console somewhere other than platform.shops.example.com.

Then point *.shops.example.com at the server. One wildcard DNS record covers the console and every <slug> address.

Add a store

Open the platform host in a browser. / goes to the console at /platform. Sign in with the platform token and choose “New store”.

Or call the platform API. Sending the Host header yourself works before DNS does:

Create a store, then attach its own domain

curl -X POST http://127.0.0.1:8080/api/platform/tenants \
  -H "Host: platform.shops.example.com" \
  -H "Authorization: Bearer $GOCOMMERCE_PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug": "acme", "name": "Acme", "owner_email": "owner@acme.example", "currency": "EUR"}'

curl -X POST http://127.0.0.1:8080/api/platform/tenants/acme/domains \
  -H "Host: platform.shops.example.com" \
  -H "Authorization: Bearer $GOCOMMERCE_PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain": "shop.acme.example"}'

Routes from platform/http.go. The full contract is at GET /api/platform/doc.

The first response holds the store, its own admin token and — if you gave no password — the owner’s generated one. Both are shown once. The store’s admin panel and API answer at acme.shops.example.com straight away.

Slugs platform, api, www and admin are reserved, and a custom domain cannot sit under the base domain. A suspended store answers 503. A store can be deleted only once suspended, with its slug repeated as ?confirm=acme.

What stores share, and what they do not

SharedPer store
ProcessOne process, one binaryIts own engine inside it, with its own background work
DatabaseOne PostgreSQL databaseIts own schema, and a connection pool of up to 4
Admin panelThe same buildIts own operators and sign-in, on its own host
Admin token—Its own, minted at creation, opening this store and no other
ModulesThe same 20 flags for every storeEach module’s settings, such as keys on the Plugins screen
Provider keys you add to the environment, like RESEND_API_KEYUsed by every storeUntil a store sets its own on its Plugins screen
Currency and languagesDefaults from the flagsCan be set when the store is created
MediaOne media volumeIts own folder
Custom SQL reportsOne template login, GOCOMMERCE_REPORTS_DATABASE_URL (off when unset)Its own read-only login, granted only its own schema

One store or many

docker-compose.ymldocker-compose.platform.yml
Commandserveplatform
StoresOneAs many as you create
TablesThe public schemastore_<slug> per store, plus gocommerce_platform
Required variablesPOSTGRES_PASSWORD, GOCOMMERCE_ADMIN_TOKEN, GOCOMMERCE_ADMIN_PASSWORDPOSTGRES_PASSWORD, GOCOMMERCE_PLATFORM_TOKEN, GOCOMMERCE_BASE_DOMAIN
First operatorFrom GOCOMMERCE_ADMIN_EMAIL and GOCOMMERCE_ADMIN_PASSWORDThe owner email given when the store is created
AddressWhatever host reaches port 8080<slug>.<base domain>, or an attached domain
Uploads/data/media/data/media/<slug>
Services and volumesgocommerce, postgres; media, pgdataThe same names

The names match on purpose: point an existing single-store deployment at the platform file and its volume is kept. Its tables stay in the public schema, untouched. The platform creates its stores beside them, and does not serve the old one.

What it does not do yet

Migrations run store by store when the process starts. A store that fails to start answers 503, and the console counts it under “failed to start”.

Further reading

Chat on WhatsApp