Multi-Store Operations
rCommerce is built to run one binary, many stores. Each store is an
independent instance — its own port, config file, database, and service —
sharing a single deployed binary. This page walks through running two
example stores, shop-a (store-a.example.com) and shop-b
(store-b.example.com), on one host.
Layout
Section titled “Layout”| Store | Port | Config | Database | Uploads | Logs |
|---|---|---|---|---|---|
| shop-a | 8080 | /usr/local/etc/shop-a/config.toml | shop_a | /var/db/shop-a/uploads | /var/log/shop-a/ |
| shop-b | 8081 | /usr/local/etc/shop-b/config.toml | shop_b | /var/db/shop-b/uploads | /var/log/shop-b/ |
Each config.toml carries everything store-specific: its own [database]
credentials, [branding] (name, support email, site URL, logo, accent
color — drives the email shell and invoice branding), its own
[payment.*] gateway keys, its own email provider token under
[notifications.email], and store-level behavior like
[orders] auto_cancel_pending_minutes. Configs are root:wheel 644; the
service runs as the rcommerce user and must be able to read them.
Per-store API keys live in the store’s own database, so an API key issued for one store can never touch another.
Services (FreeBSD rc.d)
Section titled “Services (FreeBSD rc.d)”On FreeBSD each instance is an rc.d service. Copy the service script per
store, changing the name, config path, data dir, and log file. Example
/usr/local/etc/rc.d/rcommerce-b:
#!/bin/sh## PROVIDE: rcommerce_b# REQUIRE: LOGIN# KEYWORD: shutdown
. /etc/rc.subr
name="rcommerce_b"desc="rCommerce API server (shop-b)"rcommand="/usr/local/sbin/rcommerce"pidfile="/var/run/${name}/${name}.pid"
load_rc_config $name
: ${rcommerce_b_enable="NO"}: ${rcommerce_b_config="/usr/local/etc/shop-b/config.toml"}: ${rcommerce_b_data_dir="/var/db/shop-b"}: ${rcommerce_b_log_file="/var/log/shop-b/server.log"}
start_cmd="${name}_start"stop_cmd="${name}_stop"status_cmd="${name}_status"
rcommerce_b_start(){ /usr/bin/install -d -o rcommerce -g rcommerce -m 750 /var/run/${name} /usr/bin/install -d -o rcommerce -g rcommerce -m 750 ${rcommerce_b_data_dir} /usr/bin/install -d -o rcommerce -g rcommerce -m 750 $(dirname ${rcommerce_b_log_file})
echo "Starting ${desc}."
/usr/sbin/daemon \ -f \ -S \ -p ${pidfile} \ -u rcommerce \ -t ${name} \ env RCOMMERCE_CONFIG="${rcommerce_b_config}" \ IMAGE_BASE_URL="https://images.store-b.example.com" \ ${rcommand}}
rcommerce_b_stop(){ echo "Stopping ${desc}." /usr/bin/kill -TERM $(cat ${pidfile})}
rcommerce_b_status(){ if [ -f ${pidfile} ]; then if /bin/pkill -0 -F ${pidfile} >/dev/null 2>&1; then echo "${desc} is running as pid $(cat ${pidfile})." return 0 else echo "${desc} is not running but pid file exists." return 1 fi else echo "${desc} is not running." return 1 fi}
run_rc_command "$1"Enable in /etc/rc.conf:
rcommerce_a_enable="YES"rcommerce_b_enable="YES"On Linux the same pattern maps to one systemd unit per store (copy the
systemd service unit, change Description,
ExecStart config path, and unit name).
After any restart, verify the new processes actually replaced the old ones
(ps aux | grep "rcommerce server" — PIDs and start times must be new; a
stale process behind a “successful” restart has bitten us before) and hit
/health on each port.
Reverse proxy (Caddy)
Section titled “Reverse proxy (Caddy)”One Caddyfile serves all stores. Each vhost terminates TLS (Caddy obtains
Let’s Encrypt certificates automatically, including through a Cloudflare-proxied
DNS record via HTTP-01), serves /uploads/* from the store’s upload
directory, and reverse-proxies everything else to the store’s port. Note
root points at the parent of uploads/:
api.store-a.example.com { root * /var/db/shop-a file_server /uploads/* reverse_proxy 127.0.0.1:8080}
api.store-b.example.com { root * /var/db/shop-b file_server /uploads/* reverse_proxy 127.0.0.1:8081}The file_server block is the fallback for image URLs that predate R2/CDN
hosting — keep it even after moving images to
R2.
Upgrading the shared binary
Section titled “Upgrading the shared binary”All instances share /usr/local/sbin/rcommerce, so an upgrade restarts
every store. Build natively for the host’s target — never on the production
box itself. On FreeBSD:
# 1. Build on a build host (e.g. a remote builder with the source checkout)cargo build --release --bin rcommerce --target x86_64-unknown-freebsd# If the repo's .cargo/config.toml defaults to another host target,# always pass the target explicitly.
# 2. Ship the binaryscp target/x86_64-unknown-freebsd/release/rcommerce shopserver:/tmp/rcommerce-new
# 3. Restart all instances (stop each, swap, start each)ssh shopserver ' for s in rcommerce-a rcommerce-b; do sudo service $s stop; sleep 2; done sudo mv /tmp/rcommerce-new /usr/local/sbin/rcommerce && sudo chmod +x /usr/local/sbin/rcommerce for s in rcommerce-a rcommerce-b; do sudo service $s start; done'Migrations run automatically on each instance’s startup, against its own database — run the upgrade during a quiet window for stores with pending schema changes.
Post-deploy checklist:
ps aux | grep "rcommerce server"— all instances show fresh PIDs.curl -sf https://api.<store>.com/healthfor each store.- Watch
/var/log/<store>/server.logfor migration or startup errors.
See also: Binary Deploy, Reverse Proxy.