Skip to content

Deploy to production

Use this guide for an app that already works on a development store. You need a production PostgreSQL database, an HTTPS app origin, a process supervisor or hosting platform, and the credentials for the Shopify app being deployed.

A deployment has three stages: build, apply migrations once, then start the web and worker services. Run app commands from the project root.

With development dependencies installed in development or CI:

Terminal window
pnpm install --frozen-lockfile
pnpm run lint
pnpm exec openshop migrate generate
pnpm exec openshop migrate check

Run your app’s tests if you have set up a test suite. Review and commit any generated files in drizzle/. Test the migrations against a separate database before applying them to production; see Manage migrations.

Inject these into the production services using your platform’s secret settings:

NODE_ENV=production
DATABASE_URL=postgresql://user:password@database:5432/openshop
SHOPIFY_API_KEY=your-client-id
SHOPIFY_API_SECRET=your-client-secret
HOST=https://your-app.example.com
ENCRYPTION_KEY=replace-with-64-hex-characters

Generate ENCRYPTION_KEY once with openssl rand -hex 32, store it securely, and reuse it across deployments. Both services need the same key, database, and Shopify configuration. Production commands do not load .env automatically. For multiple apps, use Configure Shopify apps.

Provide the app’s build environment, including SHOPIFY_API_KEY for single-app App Bridge configuration, then run:

Terminal window
pnpm run build

The output contains dist/ui/ and dist/openshop/server/. Deploy them together with the production dependencies, drizzle/, package metadata, and any referenced Shopify TOML files. The generated Dockerfile and ecosystem.config.cjs provide a starting point for a container running both services.

Run a single release job with the target production DATABASE_URL:

Terminal window
pnpm exec openshop migrate
pnpm exec openshop migrate status

Confirm pending: 0 before starting the new processes. This command applies committed SQL only. Neither the web process nor the worker migrates on startup.

Configure two independently supervised services from the same build:

Service Command Initial process count
Web pnpm exec openshop start 1
Worker pnpm exec openshop worker --concurrency=5 1

Both commands keep running. Put them in separate service definitions, containers, or process-manager entries; two sequential lines in a shell script would never start the worker. The web process serves HTTP and dispatches scheduled runs. The worker executes the queued work.

Keep one web replica when using crons: every web replica starts a scheduler and there is no leader election. Scale workers after measuring database and provider capacity. See Operate an app for that procedure.

For PM2, the template uses node_modules/openshop/bin/cli.js as the script. Keep that path: node_modules/.bin/openshop can be a shell shim, which PM2 may try to execute as JavaScript.

Set the production application and callback URLs in the linked Shopify app TOML, then release its configuration:

Terminal window
shopify app deploy --config shopify.app.toml

Repeat for every configured app. This updates Shopify-side configuration; it does not deploy the OpenShop server. Complete the app’s installation or renewed scope authorization as needed.

  1. Request https://your-app.example.com/health and expect HTTP 200.
  2. Open the installed app inside Shopify admin.
  3. Check a configured provider using its production credentials.
  4. Run a harmless flow and verify it reaches completed with the expected steps.
  5. Confirm both service logs show no database, encryption, or configuration errors.

/health only proves the HTTP process responds. A completed smoke flow also exercises the queue and worker. If runs stay pending, use Troubleshooting.

Restore the previous app build only if it remains compatible with the current database schema. Applied SQL is not undone by rolling back a process. For an incompatible schema change, use a reviewed forward fix or your database restore procedure. Keep the original encryption key available with database backups.