spree-deployment
Use when deploying a Spree 6 application to production or scaling it — Docker image builds, Render/AWS/Railway/Kubernetes/VPS, required environment variables, Solid Queue (jobs in Puma vs a dedicated `bin/jobs` worker), Solid Cache, optional Sidekiq/Redis, the Mission Control `/jobs` dashboard, serving the React dashboard at `/dashboard` or from a static host, migrations on boot and `spree:upgrade`, S3/R2 storage, CDN_HOST, Meilisearch, PostgreSQL/MySQL/SQLite, and observability (spree_opentelemetry, Sentry). Common phrasings include "deploy Spree", "Spree Docker image", "spree build --production", "Render deploy", "Spree environment variables", "split the worker", "SOLID_QUEUE_IN_PUMA", "jobs not running", "/jobs dashboard locked", "dashboard 404 in production", "images point at localhost", "S3 setup", "Sidekiq instead of Solid Queue", "OpenTelemetry tracing". Spree-specific bits only — generic Rails deployment is out of scope.
How do I install this agent skill?
npx skills add https://github.com/spree/agent-skills --skill spree-deploymentIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides standard deployment documentation for Spree applications, covering environment variables, Sidekiq configuration, and release-phase commands. It uses well-known services and refers to official project resources. No security issues were detected.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
Deploying Spree
A Spree 6 production deployment is one Docker image + one database. The same image runs the Store/Admin API, serves the React admin at /dashboard, and — by default — processes background jobs in-process. Jobs (Solid Queue) and cache (Solid Cache) live in the database; Redis is optional. Everything is configured with environment variables.
Project layout reminder: the Rails app is server/ (older projects: backend/), the admin dashboard is apps/dashboard/. Classic flavor (plain Rails app with Spree gems at the repo root) uses the same env vars and Rails config — only the paths and the CLI wrapper differ.
Topology decision
| Shape | When | How |
|---|---|---|
| Combined (default) | Most stores; one container is the whole app | Web container runs Puma with the Solid Queue plugin (SOLID_QUEUE_IN_PUMA=true) |
| Split worker | Heavy imports, big catalogs, marketplaces syncing many sellers, multi-tenant | Same image, second service with command bin/jobs; set SOLID_QUEUE_IN_PUMA=false on web |
| Distributed | Independent scaling/release of surfaces | API behind a load balancer; dashboard as static files on a CDN; storefront (Next.js) on Vercel/Node host |
| Sidekiq | Only when a dedicated Solid Queue worker with raised JOB_THREADS/JOB_CONCURRENCY no longer keeps up | Swap Active Job adapter to Sidekiq on Redis/Valkey (see below) |
Start combined; every step up is configuration, not code.
Building the image
spree eject # once — materializes server/ incl. its production Dockerfile
spree build --production # → <project>-spree:latest
spree build --production --tag registry.example.com/my-store:v42
# without the CLI, from the project root:
docker build . -f server/Dockerfile -t my-store
Build from the project root as context: the Dockerfile detects server/ (Rails) and apps/dashboard/ (your customized dashboard) and bakes your dashboard build into the image. Without apps/dashboard/, the stock dashboard is baked in. spree build --production recognizes the layout-aware Dockerfile by its .spree-custom-app marker (.spree-custom-dashboard in the first release); without either it falls back to building with server/ as the context. So if your server/Dockerfile predates this layout support, update it from the spree-starter template — otherwise you ship the stock dashboard over your customized one.
Uncustomized stores can run the official multi-arch image ghcr.io/spree/spree:<version> directly.
Environment variables
Strictly required: DATABASE_URL, SECRET_KEY_BASE. Practically required: RAILS_HOST and the three Active Record encryption keys. Full table: references/environment-variables.md.
| Variable | Why it matters |
|---|---|
DATABASE_URL | postgres://…, mysql2://…, or sqlite3:db/production.sqlite3 |
SECRET_KEY_BASE | openssl rand -hex 64. Keep stable — sessions, cookies, JWT fallback and secret-API-key digests depend on it |
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY, ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY, ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT | Encrypt webhook signing secrets, gateway customer IDs and OAuth identity tokens at rest. Unset → plaintext + a boot warning. Generate with spree encryption init --print or bin/rails db:encryption:init; a separate set per environment, backed up; never change them once data is encrypted. Workers need the same values as web. See spree-security |
RAILS_HOST | Public host (no protocol) for email links, webhook payloads, and attachment URLs in API responses. Unset → URLs point at localhost. On Render, falls back to RENDER_EXTERNAL_HOSTNAME |
CDN_HOST | Optional separate host for assets and images (sets config.asset_host and Spree.cdn_host) |
SOLID_QUEUE_IN_PUMA | true (default) runs jobs in the web process; false when a bin/jobs worker runs |
JOB_THREADS / JOB_CONCURRENCY | Worker threads (default 3) / worker processes (default 1) |
SPREE_IMPORT_JOB_CONCURRENCY | Cap on concurrent CSV import group jobs (default 75% of JOB_THREADS, min 1; 0 = no cap) |
MISSION_CONTROL_USER / MISSION_CONTROL_PASSWORD | HTTP Basic auth for /jobs. Unset in production → dashboard locked |
SMTP_HOST (+ SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM_ADDRESS) | Without SMTP_HOST, production sends no email. Any SMTP provider works, with nothing to install. STARTTLS is always attempted, so use port 587, not an implicit-TLS 465. Auth is plain, sent only when SMTP_USERNAME is set. Merchants override From/Reply-To per store in Settings → Emails |
AWS_* or CLOUDFLARE_* | Object storage for uploads — required on ephemeral filesystems |
RAILS_FORCE_SSL / RAILS_ASSUME_SSL | Both default true (behind a TLS-terminating proxy). Set both false only when there is no TLS anywhere |
SENTRY_DSN, OTEL_*, MEILISEARCH_URL | Optional integrations (below) |
Puma: PORT (3000), RAILS_MAX_THREADS (3), WEB_CONCURRENCY (1; auto = one per core, ~1 GB RAM each).
Database
- PostgreSQL recommended; MySQL/MariaDB and SQLite are supported in production too (SQLite: single host only, small stores).
- Pool size must cover web threads plus in-process job threads: the template's
database.ymlusesRAILS_MAX_THREADS + JOB_THREADS + 3. If you hand-rolldatabase.yml, keep that formula or jobs in Puma will exhaust the pool. - Solid Queue, Solid Cache and Solid Cable tables live in the same database — back it up as one unit.
Migrations, seeds, and upgrades
- The image entrypoint (
bin/docker-entrypoint) runsbin/rails db:prepareonly when the container command is the Rails server — creates + seeds on first boot, migrates on later boots.bin/jobsworkers don't migrate. - First boot: run it once before scaling out — a release/init job running
bin/rails db:prepare, or a single web replica. Rails' migration advisory lock (PostgreSQL/MySQL) serializes migrations only, not database creation, schema load or seeding, so replicas racing on an empty database can collide or double-seed. - Later boots: several web replicas migrating at once is fine on PostgreSQL/MySQL (the advisory lock). If your platform has a release/pre-deploy phase and you want failed migrations to block the rollout, run
bin/rails db:migratethere instead. - After upgrading Spree gems, run the data backfills once per environment:
bin/rails spree:upgrade(idempotent;DRY_RUN=1to preview). The entrypoint does not run it — use a release command or one-off task. See thespree-upgradeskill. - No default admin exists. Seeding prints a one-time setup link; print it again with
bin/rails spree:setup:token.
Background jobs (Solid Queue)
-
config/puma.rbloadsplugin :solid_queuewhenSOLID_QUEUE_IN_PUMAistrue. -
config/queue.ymldefines worker queues in polling order (checkout-critical first, bulk catalog next, Active Storage housekeeping last) with a trailing"*"so any unlisted queue still runs. -
Spree jobs pick their queue from
Spree.queues.<name>(every entry defaults to:default); the template'sconfig/initializers/spree.rbmaps them tospree_*names. Seespree-performancefor tuning. -
config/recurring.ymlis Solid Queue's cron. The template schedules (production only):Spree::StockReservations::ExpireJob— every minute (releases expired checkout holds)Spree::Orders::FinalizeStaleDraftsJob— every 5 minutes (finishes orders whose payment succeeded but whose completion never committed; never refunds)Spree::Carts::ReapExpiredJob— hourly (deletes abandoned carts past expiry; carts with a live payment are kept)Spree::Collections::RegenerateTimeBasedJob— hourly (automatic collections with time-based rules)spree:price_history:prunerake task (via acommand:entry) — daily (EU Omnibus retention, per store)Spree::SellerPayouts::SweepDueJob— daily (marketplace payouts; run it daily regardless of seller payout intervals — due-ness is decided per seller)Spree::SellerTransfers::ExecutePendingDueJob— hourly (retries refused seller transfers)- Solid Queue finished-job and Solid Cable message cleanup — hourly
The marketplace jobs are no-ops on a store without sellers — keep them. If your project predates any entry, copy it from the spree-starter
config/recurring.yml. -
Split mode: add a service running
bin/jobswith the same env (DATABASE_URL,SECRET_KEY_BASE,RAILS_HOST), raiseJOB_THREADSthere, scale withJOB_CONCURRENCYor more replicas, and setSOLID_QUEUE_IN_PUMA=falseon web. On a split deployment setSPREE_IMPORT_JOB_CONCURRENCYexplicitly on web — the cap is computed by the process that enqueues the import.
Mission Control (/jobs)
Mounted at /jobs in the template's routes. Set MISSION_CONTROL_USER and MISSION_CONTROL_PASSWORD; in development they default to spree / spree123. It's Solid Queue-specific.
Swapping to Sidekiq (optional)
# Gemfile
gem "sidekiq"
gem "sidekiq-cron" # replaces config/recurring.yml
# config/application.rb
config.active_job.queue_adapter = :sidekiq
config/sidekiq.ymlmust list every queue you assigned viaSpree.queuesplusdefault,mailers, and theactive_storage_*queues — Sidekiq has no"*"catch-all, so an unlisted queue never runs.- Move
recurring.ymlschedules toconfig/schedule.yml(sidekiq-cron). sidekiq-cron schedules jobs, not commands, so wrap thespree:price_history:prunetask in a small job (Rails.application.load_tasksunless defined, thenRake::Task[...].tap(&:reenable).invoke). - Mount
Sidekiq::Webbehind auth instead of Mission Control; runbundle exec sidekiqas its own service withRAILS_MAX_THREADS >= SIDEKIQ_CONCURRENCY; dropSOLID_QUEUE_IN_PUMA.
Cache
Solid Cache (config.cache_store = :solid_cache_store) is the default — nothing to provision. For an in-memory cache: add the redis gem and set config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] } in config/environments/production.rb. Works with Valkey.
The admin dashboard
- Single node (default): the
spree_dashboardgem serves the built dashboard at/dashboard(and an optional marketplace seller panel at/sellers) with SPA fallback. It serves the directory fromSpree::Dashboard.dist_pathorSPREE_DASHBOARD_DIST_PATH(seller panel:SPREE_SELLER_PANEL_DIST_PATH); unset → 404. The image build sets this up — same origin, no CORS, no API keys. - Static host / CDN:
cd apps/dashboard && VITE_SPREE_API_URL=https://api.example.com pnpm build, uploaddist/, rewrite/* → /index.html, HTTPS on both sides (the refresh cookie isSameSite=None; Secure), and add the dashboard origin under Settings → Allowed Origins.VITE_*values are baked into the bundle — never put secrets there; one build per environment. - API-only deployments can drop the
spree_dashboardgem.
File storage and CDN
- Uploads go through Active Storage. Local disk is the default and is lost on ephemeral platforms (Render, Heroku, Fargate, most K8s). Set
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY(+AWS_REGION,AWS_BUCKET) orCLOUDFLARE_ENDPOINT+CLOUDFLARE_ACCESS_KEY_ID/CLOUDFLARE_SECRET_ACCESS_KEY(+CLOUDFLARE_BUCKET); the template auto-detects the service. Other backends: standardconfig/storage.yml. - Put a CDN in front of
/rails/active_storage/representations/and/assets. With a separate CDN hostname, setCDN_HOST.
Search
Default search is SQL (Spree::SearchProvider::Database). For large catalogs add Meilisearch:
# Gemfile
gem "spree_meilisearch"
# config/initializers/spree.rb
Spree.search_provider = 'SpreeMeilisearch::SearchProvider'
Env: MEILISEARCH_URL (default http://localhost:7700), MEILISEARCH_API_KEY. Build the index after the first deploy and after provider config changes: bin/rails spree:search:reindex (spree task search:reindex locally).
Observability
- Errors: set
SENTRY_DSN(template shipssentry-rails). - Tracing: add
gem 'spree_opentelemetry'and set standard OTel vars —OTEL_SERVICE_NAME,OTEL_EXPORTER_OTLP_ENDPOINT(orOTEL_EXPORTER_OTLP_TRACES_ENDPOINT), optionalOTEL_TRACES_SAMPLER=parentbased_traceidratio+OTEL_TRACES_SAMPLER_ARG. Dormant until an exporter var is set;OTEL_SDK_DISABLED=truealways wins. Spans cover HTTP, SQL, jobs, everySpree::Workflowrun/step/hook, event dispatch, webhook deliveries (W3C trace context propagated), and gateway calls. Span attributes carry no PII. - Sentry + OTel: keep Sentry for errors and don't also set Sentry's
traces_sample_rate(double instrumentation). To use Sentry as the trace backend, see the Sentry section of the observability doc (sentry-opentelemetry,SpreeOpenTelemetry.install!beforeSentry.init,OTEL_TRACES_EXPORTER=none). - Health check:
/up(Rails built-in; it does not touch the DB).
Platform notes
- Render:
create-spree-appmoves the starter's Blueprint to the project root asrender.yamland points every service'sdockerfilePath(including the commented worker) at./server/Dockerfile, with the repo root asdockerContext— one web service + Postgres. The Blueprint generatesSECRET_KEY_BASE, the threeACTIVE_RECORD_ENCRYPTION_*keys (copy them from the Environment tab into your secret manager) andMISSION_CONTROL_PASSWORD(userjobs). Uncomment the worker block for split mode. Filesystem is ephemeral — configure S3/R2. - AWS: single EC2 + RDS, or ECS Fargate (web and
bin/jobsas separate services from one image). - Kubernetes: one Deployment for web, optionally one for
bin/jobs; Secrets forSECRET_KEY_BASE/DATABASE_URL/storage creds; liveness on/up; migrations either via the entrypoint (server command) or a Job/init container runningdb:migrate. - VPS:
docker composewithweb+postgresis a complete deployment; add Caddy/nginx/Traefik for TLS.
Common problems
| Symptom | Cause / fix |
|---|---|
Image/attachment URLs point at https://localhost/... | RAILS_HOST (or CDN_HOST) unset |
/dashboard returns 404 | spree_dashboard gem missing or SPREE_DASHBOARD_DIST_PATH/Spree::Dashboard.dist_path unset (custom image built without the dashboard stage) |
| Static-host dashboard can't log in / CORS errors | Origin not in Settings → Allowed Origins, or not HTTPS on both sides |
/jobs always 401 | MISSION_CONTROL_USER/PASSWORD unset in production |
| Jobs never run after splitting | Worker not running bin/jobs, or SOLID_QUEUE_IN_PUMA=false set on web with no worker |
| Jobs run on Solid Queue but not on Sidekiq | Queue missing from sidekiq.yml (no catch-all) |
ActiveRecord::ConnectionTimeoutError under load | Pool smaller than RAILS_MAX_THREADS + JOB_THREADS |
| Uploads vanish after redeploy | Local disk storage on an ephemeral filesystem |
| No emails in production | SMTP_HOST unset |
| Search returns nothing on Meilisearch | Index never built — run spree:search:reindex |
| Checkout stock held forever / sellers never paid | recurring.yml entries missing or not in the production: key, or you moved to Sidekiq without porting them |
| Store data looks half-migrated after a gem bump | spree:upgrade not run |
Boot log: [Spree] Active Record encryption is not fully configured | ACTIVE_RECORD_ENCRYPTION_* unset (or not read into config.active_record.encryption) — secrets are being stored in plaintext |
| Webhook secrets / gateway customers unreadable after a deploy | Encryption keys changed or missing on this environment, or encryption enabled without first encrypting existing rows (see spree-security) |
Where to read further
node_modules/@spree/docs/dist/developer/deployment/—quickstart.md,docker.md,environment_variables.md,background_jobs.md,caching.md,database.md,assets.md,cdn.md,emails.md,render.md,aws.md,aws_ecs.mdnode_modules/@spree/docs/dist/developer/dashboard/deployment.mdnode_modules/@spree/docs/dist/developer/providers/observability.md- https://spreecommerce.org/docs/developer/deployment/quickstart
- Related skills:
spree-performance(queue and worker tuning),spree-upgrade(release-phase upgrades),spree-security(secrets, encryption keys),spree-multi-tenant(root domain, cookies, hosts)
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/spree/agent-skills/spree-deployment">View spree-deployment on skillZs</a>