From 5e0ca00af2163b7086c269935f03d93bbca76d01 Mon Sep 17 00:00:00 2001 From: xmrcheckout Date: Sun, 4 Jan 2026 15:47:24 +0100 Subject: [PATCH] Add docker-compose.yml and friends --- .env.example | 28 ++++++ api/.env.example | 1 + docker-compose.yml | 189 ++++++++++++++++++++++++++++++++++++ docker/monero/Dockerfile | 31 ++++++ docker/monero/entrypoint.sh | 42 ++++++++ nginx/Dockerfile | 9 ++ nginx/conf.d/default.conf | 55 +++++++++++ nginx/docker-entrypoint.sh | 21 ++++ 8 files changed, 376 insertions(+) create mode 100644 .env.example create mode 100644 docker-compose.yml create mode 100644 docker/monero/Dockerfile create mode 100644 docker/monero/entrypoint.sh create mode 100644 nginx/Dockerfile create mode 100644 nginx/conf.d/default.conf create mode 100644 nginx/docker-entrypoint.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..65e07b3 --- /dev/null +++ b/.env.example @@ -0,0 +1,28 @@ +# Database +POSTGRES_USER=xmrcheckout +POSTGRES_PASSWORD=xmrcheckout +POSTGRES_DB=xmrcheckout +DATABASE_URL=postgresql://xmrcheckout:xmrcheckout@db:5432/xmrcheckout + +# API authentication +API_KEYS=dev-key-1 +API_KEY_ENCRYPTION_KEY=replace-with-fernet-key +COINGECKO_API_KEY= +INVOICE_RECONCILE_INTERVAL_SECONDS=30 + +# Founder donation wallet (view-only) +FOUNDER_PAYMENT_ADDRESS= +FOUNDER_VIEW_KEY= +DONATIONS_ENABLED=false + +# Monero wallet RPC (view-only wallet generation + subaddresses) +MONERO_WALLET_RPC_URLS=http://wallet-rpc-reconciler-1:18083,http://wallet-rpc-reconciler-2:18083,http://wallet-rpc-reconciler-3:18083 +MONERO_WALLET_RPC_USER= +MONERO_WALLET_RPC_PASSWORD= +MONERO_WALLET_RPC_WALLET_PASSWORD= +MONERO_WALLET_RPC_WALLET_DIR=/wallets +MONERO_DAEMON_URL=http://xmr-node.cakewallet.com:18081 + +# UI +API_BASE_URL=http://api:8000 +SITE_URL=https://example.com diff --git a/api/.env.example b/api/.env.example index 6caee57..13f58b5 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,6 +1,7 @@ DATABASE_URL=postgresql://xmrcheckout:xmrcheckout@localhost:5432/xmrcheckout API_KEYS=change-me-1,change-me-2 COINGECKO_API_KEY= +INVOICE_RECONCILE_INTERVAL_SECONDS=30 FOUNDER_PAYMENT_ADDRESS= FOUNDER_VIEW_KEY= DONATIONS_ENABLED=false diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..41ca700 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,189 @@ +services: + nginx: + build: + context: ./nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./nginx/certs:/etc/nginx/certs + - ./qr:/qr:ro + depends_on: + ui: + condition: service_started + api: + condition: service_started + restart: on-failure + api: + build: + context: ./api + environment: + DATABASE_URL: ${DATABASE_URL} + API_KEYS: ${API_KEYS} + API_KEY_ENCRYPTION_KEY: ${API_KEY_ENCRYPTION_KEY} + COINGECKO_API_KEY: ${COINGECKO_API_KEY} + FOUNDER_PAYMENT_ADDRESS: ${FOUNDER_PAYMENT_ADDRESS} + FOUNDER_VIEW_KEY: ${FOUNDER_VIEW_KEY} + DONATIONS_ENABLED: ${DONATIONS_ENABLED:-false} + QR_STORAGE_DIR: ${QR_STORAGE_DIR:-/qr} + MONERO_WALLET_RPC_URLS: ${MONERO_WALLET_RPC_URLS:-http://wallet-rpc-reconciler-1:18083,http://wallet-rpc-reconciler-2:18083,http://wallet-rpc-reconciler-3:18083} + MONERO_WALLET_RPC_USER: ${MONERO_WALLET_RPC_USER} + MONERO_WALLET_RPC_PASSWORD: ${MONERO_WALLET_RPC_PASSWORD} + MONERO_WALLET_RPC_WALLET_PASSWORD: ${MONERO_WALLET_RPC_WALLET_PASSWORD} + MONERO_WALLET_RPC_WALLET_DIR: ${MONERO_WALLET_RPC_WALLET_DIR:-/wallets} + MONERO_DAEMON_URL: ${MONERO_DAEMON_URL:-http://xmr-node.cakewallet.com:18081} + GUNICORN_WORKERS: ${GUNICORN_WORKERS:-5} + volumes: + - ./qr:/qr + ports: + - "8000:8000" + depends_on: + db: + condition: service_healthy + restart: on-failure + reconciler: + build: + context: ./api + command: ["python", "-m", "app.reconciler"] + environment: + DATABASE_URL: ${DATABASE_URL} + API_KEYS: ${API_KEYS} + API_KEY_ENCRYPTION_KEY: ${API_KEY_ENCRYPTION_KEY} + INVOICE_RECONCILE_INTERVAL_SECONDS: ${INVOICE_RECONCILE_INTERVAL_SECONDS} + LOG_LEVEL: ${LOG_LEVEL:-INFO} + MONERO_WALLET_RPC_URLS: ${MONERO_WALLET_RPC_URLS:-http://wallet-rpc-reconciler-1:18083,http://wallet-rpc-reconciler-2:18083,http://wallet-rpc-reconciler-3:18083} + MONERO_WALLET_RPC_USER: ${MONERO_WALLET_RPC_USER} + MONERO_WALLET_RPC_PASSWORD: ${MONERO_WALLET_RPC_PASSWORD} + MONERO_WALLET_RPC_WALLET_PASSWORD: ${MONERO_WALLET_RPC_WALLET_PASSWORD} + MONERO_WALLET_RPC_WALLET_DIR: ${MONERO_WALLET_RPC_WALLET_DIR:-/wallets} + MONERO_DAEMON_URL: ${MONERO_DAEMON_URL:-http://xmr-node.cakewallet.com:18081} + depends_on: + api: + condition: service_started + db: + condition: service_healthy + restart: on-failure + wallet-rpc-reconciler-1: + build: + context: ./docker/monero + args: + MONERO_VERSION: ${MONERO_VERSION:-0.18.4.4} + image: monero-local:${MONERO_VERSION:-0.18.4.4} + command: + [ + "monero-wallet-rpc", + "--non-interactive", + "--rpc-bind-ip", + "0.0.0.0", + "--rpc-bind-port", + "18083", + "--confirm-external-bind", + "--daemon-address", + "${MONERO_DAEMON_URL:-http://xmr-node.cakewallet.com:18081}", + "--rpc-login", + "${MONERO_WALLET_RPC_USER}:${MONERO_WALLET_RPC_PASSWORD}", + "--wallet-dir", + "${MONERO_WALLET_RPC_WALLET_DIR:-/wallets}", + "--password", + "${MONERO_WALLET_RPC_WALLET_PASSWORD}", + "--log-level", + "3" + ] + ports: + - "18084:18083" + volumes: + - ./wallets/reconciler:/wallets + restart: on-failure + wallet-rpc-reconciler-2: + build: + context: ./docker/monero + args: + MONERO_VERSION: ${MONERO_VERSION:-0.18.4.4} + image: monero-local:${MONERO_VERSION:-0.18.4.4} + command: + [ + "monero-wallet-rpc", + "--non-interactive", + "--rpc-bind-ip", + "0.0.0.0", + "--rpc-bind-port", + "18083", + "--confirm-external-bind", + "--daemon-address", + "${MONERO_DAEMON_URL:-http://xmr-node.cakewallet.com:18081}", + "--rpc-login", + "${MONERO_WALLET_RPC_USER}:${MONERO_WALLET_RPC_PASSWORD}", + "--wallet-dir", + "${MONERO_WALLET_RPC_WALLET_DIR:-/wallets}", + "--password", + "${MONERO_WALLET_RPC_WALLET_PASSWORD}", + "--log-level", + "3" + ] + ports: + - "18085:18083" + volumes: + - ./wallets/reconciler:/wallets + restart: on-failure + wallet-rpc-reconciler-3: + build: + context: ./docker/monero + args: + MONERO_VERSION: ${MONERO_VERSION:-0.18.4.4} + image: monero-local:${MONERO_VERSION:-0.18.4.4} + command: + [ + "monero-wallet-rpc", + "--non-interactive", + "--rpc-bind-ip", + "0.0.0.0", + "--rpc-bind-port", + "18083", + "--confirm-external-bind", + "--daemon-address", + "${MONERO_DAEMON_URL:-http://xmr-node.cakewallet.com:18081}", + "--rpc-login", + "${MONERO_WALLET_RPC_USER}:${MONERO_WALLET_RPC_PASSWORD}", + "--wallet-dir", + "${MONERO_WALLET_RPC_WALLET_DIR:-/wallets}", + "--password", + "${MONERO_WALLET_RPC_WALLET_PASSWORD}", + "--log-level", + "3" + ] + ports: + - "18086:18083" + volumes: + - ./wallets/reconciler:/wallets + restart: on-failure + ui: + build: + context: ./ui + environment: + API_BASE_URL: ${API_BASE_URL:-http://nginx/api} + NEXT_PUBLIC_SITE_URL: ${SITE_URL:-http://localhost} + NEXT_PUBLIC_DONATIONS_ENABLED: ${DONATIONS_ENABLED:-false} + ports: + - "3000:3000" + depends_on: + api: + condition: service_started + restart: on-failure + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 3s + retries: 10 +volumes: + postgres_data: diff --git a/docker/monero/Dockerfile b/docker/monero/Dockerfile new file mode 100644 index 0000000..2a97695 --- /dev/null +++ b/docker/monero/Dockerfile @@ -0,0 +1,31 @@ +FROM debian:bookworm-slim + +ARG MONERO_VERSION=0.18.4.4 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl bzip2 \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + arch="$(dpkg --print-architecture)"; \ + if [ "$arch" = "amd64" ]; then \ + monero_arch="x64"; \ + elif [ "$arch" = "arm64" ]; then \ + monero_arch="armv8"; \ + else \ + echo "Unsupported architecture: $arch" >&2; \ + exit 1; \ + fi; \ + curl -fsSL -o /tmp/monero.tar.bz2 \ + "https://downloads.getmonero.org/cli/monero-linux-${monero_arch}-v${MONERO_VERSION}.tar.bz2"; \ + tar -xjf /tmp/monero.tar.bz2 -C /tmp; \ + cp /tmp/monero-*/monerod /usr/local/bin/; \ + cp /tmp/monero-*/monero-wallet-rpc /usr/local/bin/; \ + rm -rf /tmp/monero* /tmp/monero.tar.bz2 + +COPY entrypoint.sh /usr/local/bin/monero-wallet-rpc-entrypoint +RUN chmod +x /usr/local/bin/monero-wallet-rpc-entrypoint + +EXPOSE 18081 18083 + +ENTRYPOINT ["/usr/local/bin/monero-wallet-rpc-entrypoint"] diff --git a/docker/monero/entrypoint.sh b/docker/monero/entrypoint.sh new file mode 100644 index 0000000..f55069b --- /dev/null +++ b/docker/monero/entrypoint.sh @@ -0,0 +1,42 @@ +#!/bin/sh +set -e + +normalize_address() { + value="$1" + if [ -z "$value" ]; then + return + fi + case "$value" in + http://*) + value="${value#http://}" + ;; + https://*) + value="${value#https://}" + ;; + esac + value="${value%%/*}" + printf '%s' "$value" +} + +args="" +while [ "$#" -gt 0 ]; do + case "$1" in + --daemon-address) + shift + normalized="$(normalize_address "$1")" + args="$args --daemon-address $normalized" + ;; + --daemon-address=*) + value="${1#--daemon-address=}" + normalized="$(normalize_address "$value")" + args="$args --daemon-address=$normalized" + ;; + *) + args="$args $1" + ;; + esac + shift +done + +# shellcheck disable=SC2086 +exec $args diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..80e3dec --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,9 @@ +FROM nginx:1.27-alpine + +RUN apk add --no-cache openssl + +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..2741d42 --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,55 @@ +limit_req_zone $binary_remote_addr zone=global_rate_limit:10m rate=50r/s; + +server { + listen 80; + server_name localhost host.docker.internal example.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name localhost host.docker.internal example.com; + + ssl_certificate /etc/nginx/certs/dev.crt; + ssl_certificate_key /etc/nginx/certs/dev.key; + + limit_req zone=global_rate_limit burst=200 nodelay; + + if ($request_uri ~* "//+") { + return 308 $scheme://$host$uri$is_args$args; + } + + location /api/ { + proxy_pass http://api:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api { + return 301 /api/; + } + + location = /openapi.json { + proxy_pass http://api:8000/openapi.json; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /qr/ { + alias /qr/; + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + location / { + proxy_pass http://ui:3000/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/nginx/docker-entrypoint.sh b/nginx/docker-entrypoint.sh new file mode 100644 index 0000000..124af83 --- /dev/null +++ b/nginx/docker-entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -eu + +cert_dir="/etc/nginx/certs" +cert_path="${cert_dir}/dev.crt" +key_path="${cert_dir}/dev.key" + +if [ ! -f "$cert_path" ] || [ ! -f "$key_path" ]; then + mkdir -p "$cert_dir" + rm -f "$cert_path" "$key_path" + openssl req \ + -x509 \ + -nodes \ + -newkey rsa:2048 \ + -days 365 \ + -keyout "$key_path" \ + -out "$cert_path" \ + -subj "/CN=localhost" +fi + +exec "$@"