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
| Platform | Best For | Starting Cost | Cold Starts | Database |
|---|---|---|---|---|
| Vercel | Remix/Next.js apps, serverless | Free tier | Yes (serverless) | External required |
| Railway | Full-stack apps, easy setup | $5/month | No | Built-in PostgreSQL |
| Render | Containers, background workers | Free tier | Yes (free tier) | Built-in PostgreSQL |
| Fly.io | Low-latency global apps | Pay-as-you-go | No | Built-in PostgreSQL |
| AWS | Enterprise, full control | Varies | Depends on service | RDS, 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" }
]
}
]
}
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
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 }}
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 });
}
}
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
| Item | Status | Notes |
|---|---|---|
| HTTPS enforced | Required | All platforms listed above enforce this |
| Health check endpoint | Required | Used by hosting platform and uptime monitoring |
| Environment variables secured | Required | Never in code, always in platform secrets |
| Database migrations automated | Required | Run in CI before deployment |
| Error tracking configured | Strongly recommended | Sentry, Bugsnag, or equivalent |
| Logging structured | Strongly recommended | JSON format for searchability |
| CI/CD pipeline | Strongly recommended | Automated testing and deployment |
| Staging environment | Recommended | Test before production |
| Canary deployments | Recommended | Gradual rollout for risk reduction |
| Uptime monitoring | Recommended | External 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.