Skip to content

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.

StorePortConfigDatabaseUploadsLogs
shop-a8080/usr/local/etc/shop-a/config.tomlshop_a/var/db/shop-a/uploads/var/log/shop-a/
shop-b8081/usr/local/etc/shop-b/config.tomlshop_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.

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:

Terminal window
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.

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.

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:

Terminal window
# 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 binary
scp 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:

  1. ps aux | grep "rcommerce server" — all instances show fresh PIDs.
  2. curl -sf https://api.<store>.com/health for each store.
  3. Watch /var/log/<store>/server.log for migration or startup errors.

See also: Binary Deploy, Reverse Proxy.