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.4The BASE_URL environment variable must match this domain exactly.
Step 3: Deploy OpenCheckout
# SSH into your serverssh user@1.2.3.4
# Clone and deploygit clone https://github.com/temidayoxyz/opencheckoutcd opencheckout
# Generate a secure encryption keyecho "ENCRYPTION_KEY=$(openssl rand -hex 32)" > .envecho "BASE_URL=https://pay.yourstore.com" >> .envecho "MAINTENANCE_SECRET=$(openssl rand -hex 32)" >> .env
# Build without sending local env files or private keys into the imagedocker compose build
# Create the merchant inside the same Docker volume used by the service.# At the private-key prompt, enter /run/secrets/merchant-private.pemdocker compose --profile setup run --rm \ -v "/absolute/path/private.pem:/run/secrets/merchant-private.pem:ro" \ setup
# Start the applicationdocker compose up -d opencheckoutOpenCheckout 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):
# 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/sessionsAuthorization: Bearer sk_YOUR_API_KEYContent-Type: application/jsonIdempotency-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
| Variable | Required | Description |
|---|---|---|
ENCRYPTION_KEY | Yes | 64-character hex string. Encrypts private keys in the database. Generate with openssl rand -hex 32. Store securely. |
BASE_URL | Yes | Full public URL of your OpenCheckout instance, including https://. Used for ASE redirect callbacks. Must match your domain. |
DATABASE_URL | No | Path to the SQLite database file. Defaults to data/opencheckout.db. |
CORS_ALLOWED_ORIGINS | No | Comma-separated browser origins allowed to call /api/checkout. Leave empty for server-to-server access only. |
MAINTENANCE_SECRET | Recommended | Enables 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.mjsDocker 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
# Stop writes, copy the database from the named volume, then restart.docker compose stop opencheckoutdocker run --rm \ -v opencheckout_opencheckout_data:/data \ -v "$PWD/backups:/backup" \ alpine cp /data/opencheckout.db /backup/opencheckout-$(date +%Y%m%d).dbdocker compose start opencheckoutFor 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_KEYsecurely. If lost, merchant private keys become unrecoverable. - Rotate API keys from the dashboard. Revoke compromised keys immediately.
- Keep
BASE_URLaccurate. 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:
# Dockergit pulldocker compose up -d --build
# Manualgit pullnpm cinpm run buildnpm startThe 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.