Skip to main content

App Deployment

Building a great Shopify app is only half the battle -- deploying and running it reliably in production is the other half. This module covers hosting platform selection, environment variable management, CI/CD pipeline setup with GitHub Actions, monitoring, logging, and error tracking. By the end, you will have a production-grade deployment pipeline that gives you confidence in every release.

Hosting Options

Shopify apps are web applications that need to be publicly accessible over HTTPS. The best hosting platform depends on your app's architecture, traffic patterns, and budget.

Platform Comparison

PlatformBest ForStarting CostCold StartsDatabase
VercelRemix/Next.js apps, serverlessFree tierYes (serverless)External required
RailwayFull-stack apps, easy setup$5/monthNoBuilt-in PostgreSQL
RenderContainers, background workersFree tierYes (free tier)Built-in PostgreSQL
Fly.ioLow-latency global appsPay-as-you-goNoBuilt-in PostgreSQL
AWSEnterprise, full controlVariesDepends on serviceRDS, DynamoDB

Deploying to Railway

Railway is an excellent choice for Shopify apps because it supports long-running processes (needed for webhooks and background jobs) and includes built-in PostgreSQL:

# Install Railway CLI
npm install -g @railway/cli

# Login and initialize
railway login
railway init

# Add PostgreSQL
railway add --plugin postgresql

# Deploy
railway up

Configure your Procfile for Railway:

web: npm run start
worker: npm run worker

Deploying to Fly.io

Fly.io is ideal when you need low-latency globally distributed deployments:

# fly.toml
app = "your-shopify-app"
primary_region = "iad"

[build]
builder = "heroku/buildpacks:20"

[env]
NODE_ENV = "production"
PORT = "8080"

[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1

[[services]]
protocol = "tcp"
internal_port = 8080

[[services.ports]]
port = 80
handlers = ["http"]

[[services.ports]]
port = 443
handlers = ["tls", "http"]

[services.concurrency]
type = "connections"
hard_limit = 250
soft_limit = 200

[[services.http_checks]]
interval = 10000
grace_period = "5s"
method = "get"
path = "/health"
protocol = "http"
timeout = 2000
# Deploy to Fly.io
fly launch
fly deploy

# Scale to multiple regions
fly regions add lhr sin
fly scale count 2

Deploying to Vercel

For Remix-based Shopify apps, Vercel provides zero-config deployments:

// vercel.json
{
"framework": "remix",
"buildCommand": "npm run build",
"installCommand": "npm install",
"regions": ["iad1", "lhr1"],
"functions": {
"api/**/*.js": {
"maxDuration": 30
}
},
"headers": [
{
"source": "/webhooks/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store" }
]
}
]
}
warning

Vercel's serverless functions have a maximum execution time (30 seconds on Pro, 300 seconds on Enterprise). If your app processes long-running webhooks or bulk operations, use a platform that supports persistent processes like Railway or Fly.io, or offload heavy work to a separate worker service.

Environment Variables

Essential Environment Variables

Every Shopify app needs these environment variables configured in production:

# .env.example (never commit actual .env files)

# Shopify App Credentials
SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret
SCOPES=read_products,write_products,read_orders

# App Configuration
HOST=https://your-app.fly.dev
PORT=8080
NODE_ENV=production

# Database
DATABASE_URL=postgresql://user:pass@host:5432/dbname

# Session Storage
REDIS_URL=redis://default:pass@host:6379

# Encryption (for access token storage)
ENCRYPTION_KEY=generate-a-32-byte-hex-string

# Error Tracking
SENTRY_DSN=https://key@sentry.io/project-id

# Logging
LOG_LEVEL=info
danger

Never commit .env files to version control. Add .env* to your .gitignore. Use your hosting platform's secrets management (Railway variables, Fly.io secrets, Vercel environment variables) to inject these at runtime.

Generating Secure Encryption Keys

# Generate a 32-byte encryption key for AES-256
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Platform-Specific Secret Management

# Fly.io
fly secrets set SHOPIFY_API_KEY=xxx SHOPIFY_API_SECRET=yyy

# Railway (via dashboard or CLI)
railway variables set SHOPIFY_API_KEY=xxx

# Vercel
vercel env add SHOPIFY_API_KEY production

CI/CD with GitHub Actions

A proper CI/CD pipeline ensures that every deployment is tested, built, and deployed consistently.

Complete GitHub Actions Workflow

# .github/workflows/deploy.yml
name: Test, Build, and Deploy

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
NODE_VERSION: '20'

jobs:
lint-and-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck

unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info

integration-tests:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: shopify_app_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/shopify_app_test
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/shopify_app_test
SHOPIFY_API_KEY: ${{ secrets.TEST_SHOPIFY_API_KEY }}
SHOPIFY_API_SECRET: ${{ secrets.TEST_SHOPIFY_API_SECRET }}

security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm audit --audit-level=high
- name: Check for secrets in code
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified

deploy-staging:
needs: [lint-and-typecheck, unit-tests, security-audit]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: staging
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --app your-app-staging
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Run smoke tests against staging
run: |
npm run test:smoke -- --base-url=https://your-app-staging.fly.dev

deploy-production:
needs: [deploy-staging]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://your-app.fly.dev
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --app your-app-production --strategy canary
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Verify deployment health
run: |
for i in 1 2 3 4 5; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://your-app.fly.dev/health)
if [ "$STATUS" = "200" ]; then
echo "Health check passed"
exit 0
fi
sleep 10
done
echo "Health check failed"
exit 1

Shopify CLI Extension Deployment

If your app includes extensions (theme app extensions, checkout UI extensions, etc.), add a separate deployment step:

  deploy-extensions:
needs: [deploy-production]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: Deploy Shopify extensions
run: npx shopify app deploy --force
env:
SHOPIFY_API_KEY: ${{ secrets.SHOPIFY_API_KEY }}
SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}
tip

Use GitHub Actions environments with required reviewers for production deployments. This adds a manual approval gate before code reaches production, giving your team a chance to review staging test results.

Monitoring and Logging

Health Check Endpoint

Every production app needs a health check endpoint:

// routes/health.js
export async function loader() {
const checks = {};

// Database connectivity
try {
await prisma.$queryRaw`SELECT 1`;
checks.database = 'healthy';
} catch (error) {
checks.database = 'unhealthy';
}

// Redis connectivity
try {
await redis.ping();
checks.redis = 'healthy';
} catch (error) {
checks.redis = 'unhealthy';
}

const allHealthy = Object.values(checks).every((v) => v === 'healthy');

return new Response(JSON.stringify({
status: allHealthy ? 'healthy' : 'degraded',
checks,
timestamp: new Date().toISOString(),
version: process.env.COMMIT_SHA || 'unknown',
}), {
status: allHealthy ? 200 : 503,
headers: { 'Content-Type': 'application/json' },
});
}

Structured Logging

// utils/logger.js
import pino from 'pino';

export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
base: {
app: 'shopify-app',
env: process.env.NODE_ENV,
version: process.env.COMMIT_SHA,
},
});

// Usage
logger.info({ shop: 'example.myshopify.com', event: 'install' }, 'App installed');
logger.error({ err, shop, webhookTopic }, 'Webhook processing failed');

Error Tracking

Sentry Integration

// utils/sentry.js
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.COMMIT_SHA,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.2 : 1.0,
integrations: [
Sentry.prismaIntegration(),
Sentry.httpIntegration(),
],
beforeSend(event) {
// Scrub sensitive data
if (event.request?.headers) {
delete event.request.headers['x-shopify-access-token'];
delete event.request.headers['authorization'];
}
return event;
},
});

export { Sentry };

LogRocket for Session Replay

For debugging merchant-reported issues in your app's admin UI:

// app/entry.client.jsx
import LogRocket from 'logrocket';

if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
LogRocket.init('your-org/your-app');

// Identify the merchant
const shop = new URLSearchParams(window.location.search).get('shop');
if (shop) {
LogRocket.identify(shop, { shop });
}
}
info

LogRocket records user sessions so you can replay exactly what a merchant experienced when they reported a bug. This is invaluable for debugging issues that are hard to reproduce. Be mindful of privacy -- configure LogRocket to redact sensitive fields like payment information and customer PII.

Deployment Checklist

ItemStatusNotes
HTTPS enforcedRequiredAll platforms listed above enforce this
Health check endpointRequiredUsed by hosting platform and uptime monitoring
Environment variables securedRequiredNever in code, always in platform secrets
Database migrations automatedRequiredRun in CI before deployment
Error tracking configuredStrongly recommendedSentry, Bugsnag, or equivalent
Logging structuredStrongly recommendedJSON format for searchability
CI/CD pipelineStrongly recommendedAutomated testing and deployment
Staging environmentRecommendedTest before production
Canary deploymentsRecommendedGradual rollout for risk reduction
Uptime monitoringRecommendedExternal monitoring (Uptime Robot, Better Stack)

A solid deployment pipeline is the foundation of a reliable Shopify app. Invest the time to set it up properly from day one, and you will spend far less time fighting production issues and far more time building features that merchants love.