DigitalOcean quickstart with doctl #

Download, create a Droplet, copy files, start Docker Compose.

DROPLET="qpayd"
REGION="nyc3"
SIZE="s-1vcpu-1gb"
DOMAIN="pay.example.com"
ACME_EMAIL="you@example.com"
SSH_KEY_ID="$(doctl compute ssh-key list --format ID --no-header | head -n1)"

mkdir qpayd-doctl
cd qpayd-doctl

base="https://raw.githubusercontent.com/earonesty/qpayd/master/examples/digitalocean-doctl"
curl -fsSLO "$base/Dockerfile"
curl -fsSLO "$base/entrypoint.sh"
curl -fsSLO "$base/compose.yaml"
curl -fsSLO "$base/Caddyfile"
curl -fsSLO "$base/cloud-init.yaml"
curl -fsSLO "$base/qpayd.toml"
chmod +x entrypoint.sh

perl -pi -e "s/YOUR_DOMAIN/$DOMAIN/g" qpayd.toml

Create .env:

export QPAYD_MAIN_API_TOKEN="$(openssl rand -hex 32)"
export QPAYD_MAIN_DESCRIPTOR='wpkh([00000000/84h/0h/0h]xpub.../0/*)'
export QPAYD_MAIN_TREASURY_DESCRIPTOR="$QPAYD_MAIN_DESCRIPTOR"

cat >.env <<EOF
DOMAIN=$DOMAIN
ACME_EMAIL=$ACME_EMAIL
QPAYD_MAIN_API_TOKEN=$QPAYD_MAIN_API_TOKEN
QPAYD_MAIN_DESCRIPTOR=$QPAYD_MAIN_DESCRIPTOR
QPAYD_MAIN_TREASURY_DESCRIPTOR=$QPAYD_MAIN_TREASURY_DESCRIPTOR
EOF

Create the Droplet:

doctl compute droplet create "$DROPLET" \
  --region "$REGION" \
  --image ubuntu-24-04-x64 \
  --size "$SIZE" \
  --ssh-keys "$SSH_KEY_ID" \
  --user-data-file cloud-init.yaml \
  --wait

IP="$(doctl compute droplet get "$DROPLET" --format PublicIPv4 --no-header)"
echo "$IP"

Point DNS at $IP.

Copy and start:

ssh root@"$IP" 'mkdir -p /opt/qpayd'
scp Dockerfile entrypoint.sh compose.yaml Caddyfile qpayd.toml .env root@"$IP":/opt/qpayd/
ssh root@"$IP" 'cd /opt/qpayd && docker compose up -d --build'

Open admin:

open "https://$DOMAIN/admin"

Use QPAYD_MAIN_API_TOKEN to log in.

Create a test invoice:

curl -sS "https://$DOMAIN/v1/stores/main/invoices" \
  -H "Authorization: Bearer $QPAYD_MAIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount":"1.00","currency":"USD"}'

Logs:

ssh root@"$IP" 'cd /opt/qpayd && docker compose logs -f'

Dockerfile #

FROM rust:1.95-bookworm AS qpayd
WORKDIR /src
ARG QPAYD_REF=master
RUN git clone https://github.com/earonesty/qpayd.git . \
  && git checkout "$QPAYD_REF" \
  && cargo build --release --locked

FROM debian:bookworm-slim AS barkd
ARG BARKD_VERSION=0.1.4
RUN apt-get update \
  && apt-get install -y --no-install-recommends ca-certificates curl \
  && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL "https://gitlab.com/ark-bitcoin/bark/-/releases/bark-${BARKD_VERSION}/downloads/barkd-${BARKD_VERSION}-linux-x86_64" \
  -o /usr/local/bin/barkd \
  && chmod +x /usr/local/bin/barkd

FROM debian:bookworm-slim
RUN apt-get update \
  && apt-get install -y --no-install-recommends ca-certificates curl \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /data
COPY --from=qpayd /src/target/release/qpayd /usr/local/bin/qpayd
COPY --from=barkd /usr/local/bin/barkd /usr/local/bin/barkd
COPY qpayd.toml /etc/qpayd/qpayd.toml
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 8080
CMD ["entrypoint.sh"]

compose.yaml #

services:
  qpayd:
    build: .
    restart: unless-stopped
    env_file:
      - .env
    environment:
      QPAYD_MIGRATE_ON_BOOT: "true"
      RUST_LOG: "qpayd=info,tower_http=info"
    volumes:
      - qpayd_data:/data
    expose:
      - "8080"

  caddy:
    image: caddy:2
    restart: unless-stopped
    depends_on:
      - qpayd
    env_file:
      - .env
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  qpayd_data:
  caddy_data:
  caddy_config:

Caddyfile #

{$DOMAIN} {
  reverse_proxy qpayd:8080
}

qpayd.toml #

[server]
listen = "0.0.0.0:8080"
onchain_poll_seconds = 30

[server.admin]
enabled = true
store_id = "main"
asset_source = "https://cdn.jsdelivr.net/npm/@qpayd/admin@0.4.0/src/index.js"
asset_integrity = "sha384-UoC2NIFvzDHT33cpI06LUEsLaLvgk0omHv0u+NBuC2jZc2BQGoZQlalmYQWDt7Hu"

[database]
url = "sqlite:///data/qpayd.db"

[pricing]
kraken_url = "https://api.kraken.com/0/public/Ticker"
stale_after_seconds = 60

[[stores]]
id = "main"
name = "Main Store"
api_token_env = "QPAYD_MAIN_API_TOKEN"
invoice_expiry_minutes = 15
min_confirmations = 1
public_allowed_origins = ["https://YOUR_DOMAIN"]
admin_allowed_origins = ["https://YOUR_DOMAIN"]

[stores.onchain]
network = "bitcoin"
descriptor_env = "QPAYD_MAIN_DESCRIPTOR"
electrum_servers = ["ssl://electrum.blockstream.info:50002"]

[stores.lightning]
backend = "barkd"
url = "http://127.0.0.1:3000"
api_password_env = "BARKD_AUTH_TOKEN"

[stores.lightning_sweep]
backend = "barkd"
url = "http://127.0.0.1:3000"
full_api_password_env = "BARKD_SWEEP_AUTH_TOKEN"
destination_descriptor_env = "QPAYD_MAIN_TREASURY_DESCRIPTOR"
min_balance_sats = 100000
target_balance_sats = 25000
interval_seconds = 3600

[[stores.payment_links]]
id = "donate-10"
amount = "10.00"
currency = "USD"
public_allowed_origins = ["https://YOUR_DOMAIN"]
metadata = { kind = "donation", source = "digitalocean-doctl" }

entrypoint.sh #

#!/bin/sh
set -eu

export BARKD_DATADIR="${BARKD_DATADIR:-/data/barkd}"
mkdir -p "$BARKD_DATADIR"

barkd \
  --datadir "$BARKD_DATADIR" \
  --host 127.0.0.1 \
  --port 3000 \
  -q &
barkd_pid="$!"
sweep_pid=""

cleanup() {
  if [ -n "$sweep_pid" ]; then
    kill "$sweep_pid" 2>/dev/null || true
    wait "$sweep_pid" 2>/dev/null || true
  fi
  kill "$barkd_pid" 2>/dev/null || true
  wait "$barkd_pid" 2>/dev/null || true
}
trap cleanup INT TERM EXIT

for _ in $(seq 1 100); do
  if curl -fsS http://127.0.0.1:3000/ping >/dev/null 2>&1; then
    break
  fi
  sleep 0.2
done
curl -fsS http://127.0.0.1:3000/ping >/dev/null

BARKD_TOKEN="$(barkd --datadir "$BARKD_DATADIR" secret show | tr -d '\r')"
export BARKD_AUTH_TOKEN="$BARKD_TOKEN"
export BARKD_SWEEP_AUTH_TOKEN="$BARKD_TOKEN"

if [ "${QPAYD_MIGRATE_ON_BOOT:-}" = "true" ]; then
  qpayd --config /etc/qpayd/qpayd.toml migrate
fi

qpayd --config /etc/qpayd/qpayd.toml sweep &
sweep_pid="$!"

qpayd --config /etc/qpayd/qpayd.toml serve

cloud-init.yaml #

#cloud-config
package_update: true
packages:
  - ca-certificates
  - curl
  - docker.io
  - docker-compose-plugin
runcmd:
  - systemctl enable --now docker
  - mkdir -p /opt/qpayd