Skip to content

Deployment

This guide walks through deploying OpenCheckout to a production server, setting up HTTPS, and connecting it to your application. By the end, your checkout page will be live at a public URL and your backend will be able to create checkout sessions.

OpenCheckout is framework-agnostic and language-agnostic — your main application can be written in any language and use any framework. It communicates with OpenCheckout over standard HTTPS REST calls.

End-to-End Production Flow

Step 1: Get a Server

Any Linux server with at least 512MB RAM and Docker installed. A $6/month VPS from any provider works.

Step 2: Point a Domain

Create a DNS A record pointing your checkout subdomain to the server’s IP address:

pay.yourstore.com → 1.2.3.4

The BASE_URL environment variable must match this domain exactly.

Step 3: Deploy OpenCheckout

Terminal window
# SSH into your server
ssh user@1.2.3.4
# Clone and deploy
git clone https://github.com/temidayoxyz/opencheckout
cd opencheckout
# Generate a secure encryption key
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" > .env
echo "BASE_URL=https://pay.yourstore.com" >> .env
echo "MAINTENANCE_SECRET=$(openssl rand -hex 32)" >> .env
# Build without sending local env files or private keys into the image
docker compose build
# Create the merchant inside the same Docker volume used by the service.
# At the private-key prompt, enter /run/secrets/merchant-private.pem
docker compose --profile setup run --rm \
-v "/absolute/path/private.pem:/run/secrets/merchant-private.pem:ro" \
setup
# Start the application
docker compose up -d opencheckout

OpenCheckout is now running on port 3080 inside the container, mapped to 3080 on the host.

Step 4: Install a Reverse Proxy

OpenCheckout must be served over HTTPS. Wallet addresses require HTTPS by protocol specification. Use Caddy, Nginx, or any reverse proxy that terminates TLS.

Using Caddy (simplest — automatic HTTPS):

Terminal window
# Install Caddy, then create /etc/caddy/Caddyfile:
pay.yourstore.com {
reverse_proxy localhost:3080
}

Using Nginx with Let’s Encrypt:

server {
listen 443 ssl;
server_name pay.yourstore.com;
ssl_certificate /etc/letsencrypt/live/pay.yourstore.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pay.yourstore.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
server {
listen 80;
server_name pay.yourstore.com;
return 301 https://$host$request_uri;
}

Step 5: Verify

Visit https://pay.yourstore.com — you should see the OpenCheckout landing page.

Visit https://pay.yourstore.com/dashboard and sign in with your API key to verify the dashboard works.

Step 6: Connect Your Application

Your main application (written in any language, any framework) uses OpenCheckout by making HTTP requests to the checkout sessions API:

POST https://pay.yourstore.com/api/checkout/sessions
Authorization: Bearer sk_YOUR_API_KEY
Content-Type: application/json
Idempotency-Key: order-789-unique
{
"mode": "payment",
"line_items": [...],
"success_url": "https://yourstore.com/order/789/success",
"cancel_url": "https://yourstore.com/order/789/cart",
"metadata": { "order_id": "789" }
}

The response includes url — redirect your customer there. That is the entire integration.

Environment Variables

VariableRequiredDescription
ENCRYPTION_KEYYes64-character hex string. Encrypts private keys in the database. Generate with openssl rand -hex 32. Store securely.
BASE_URLYesFull public URL of your OpenCheckout instance, including https://. Used for ASE redirect callbacks. Must match your domain.
DATABASE_URLNoPath to the SQLite database file. Defaults to data/opencheckout.db.
CORS_ALLOWED_ORIGINSNoComma-separated browser origins allowed to call /api/checkout. Leave empty for server-to-server access only.
MAINTENANCE_SECRETRecommendedEnables authenticated session expiry, idempotency cleanup, and durable webhook replay.

Run maintenance on a schedule, for example every five minutes:

*/5 * * * * cd /srv/opencheckout && docker compose exec -T opencheckout node scripts/maintenance.mjs

Docker Compose Configuration

The default docker-compose.yml:

services:
opencheckout:
build: .
ports:
- "3080:3080"
environment:
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- BASE_URL=${BASE_URL:-http://localhost:3080}
- DATABASE_URL=data/opencheckout.db
volumes:
- opencheckout_data:/app/data
volumes:
opencheckout_data:

The named volume opencheckout_data persists the database across container restarts and upgrades.

The setup profile uses that same volume. Do not run npm run setup on the host and then start the default named-volume deployment: the host database is a different file and the container would not contain that merchant.

Database

OpenCheckout uses SQLite by default. The database is a single file — no separate database server, no connection strings, no network configuration. This is intentional: self-hosted software should be simple to operate.

Backups

Terminal window
# Stop writes, copy the database from the named volume, then restart.
docker compose stop opencheckout
docker run --rm \
-v opencheckout_opencheckout_data:/data \
-v "$PWD/backups:/backup" \
alpine cp /data/opencheckout.db /backup/opencheckout-$(date +%Y%m%d).db
docker compose start opencheckout

For continuous backup, Litestream replicates the SQLite WAL to S3-compatible storage in real time.

Database Portability

OpenCheckout uses Drizzle ORM, but the current schema and connection code are SQLite-specific. PostgreSQL or MySQL would require a deliberate schema migration, a new adapter, concurrency testing, and an operational rollout; it is not a configuration-only switch.

Framework and Language Compatibility

OpenCheckout is framework-agnostic and language-agnostic for integrators. Your main application talks to OpenCheckout over HTTPS using REST calls. This means:

  • Any backend language — PHP, Python, Ruby, Go, Rust, Java, .NET, Node.js, Elixir, or anything that makes HTTP requests
  • Any framework — Laravel, Django, Rails, Express, Spring, Phoenix, or no framework at all
  • Any frontend — React, Vue, Svelte, HTMX, server-rendered HTML, or static sites

Your application never imports an OpenCheckout library. The only integration is an HTTP POST to create a session and an HTTP redirect to the checkout page.

For the Open Payments protocol itself, language-specific SDKs are available if you ever need to interact with wallet addresses or the Open Payments API directly, but OpenCheckout handles all of this for you.

Security Checklist

  • Serve exclusively over HTTPS. Wallet addresses require it and the Open Payments protocol enforces it.
  • Store ENCRYPTION_KEY securely. If lost, merchant private keys become unrecoverable.
  • Rotate API keys from the dashboard. Revoke compromised keys immediately.
  • Keep BASE_URL accurate. It is used for authorization server redirect callbacks and must match your public domain.
  • Place OpenCheckout behind a reverse proxy. Do not expose it directly to the internet.
  • Restrict database file permissions. Only the OpenCheckout process should read data/opencheckout.db.

Updating

To update to a newer version:

Terminal window
# Docker
git pull
docker compose up -d --build
# Manual
git pull
npm ci
npm run build
npm start

The container and npm start run the schema bootstrapper before serving traffic. This is not a substitute for release-specific migrations: read the release notes, back up the database, and run the full health and checkout smoke tests after rebuilding.