6 Commits

Author SHA1 Message Date
Alexander Schmidt
ffd9327e3e Allow self-hosted origins in API verification
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-27 09:44:11 +01:00
Alexander Schmidt
40b81a5dc8 Fix install.sh: correct GitHub repo name in compose URL 2026-03-27 09:32:34 +01:00
Alexander Schmidt
dc5582aa04 Point source links to GitHub repo
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-27 09:11:29 +01:00
Alexander Schmidt
643ced23e9 Fix GitHub Actions: add DOCKER environment, use Node.js 24 2026-03-27 09:09:38 +01:00
Alexander Schmidt
64eee4ebc5 Add Docker self-hosting and CI/CD pipeline
- Dockerfile: Caddy + PHP-FPM + app in single Alpine container
- Caddyfile: auto-HTTPS, security headers, short URL rewrite
- docker-compose.yml: app + Watchtower for auto-updates
- install.sh: one-liner for fresh VPS setup
- GitHub Actions: build & push to Docker Hub + GHCR on tag

Self-host with:
  curl -sL https://xmrpay.link/install.sh | sh -s your-domain.com
2026-03-27 08:26:30 +01:00
Alexander Schmidt
5212f586c7 Auto-inject version from git tags in deploy
Reads version from git describe, injects into i18n.js and index.html
before minification. No manual version bumping needed.
Tag with: git tag v1.1.0
2026-03-27 08:06:57 +01:00
13 changed files with 290 additions and 8 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
.git
data/
scripts/
branding/
README.md
LICENSE
nginx-security-headers.conf
docker-compose.yml
.dockerignore
.gitignore
app.js
i18n.js

98
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
name: Build & Push Docker Image
on:
push:
tags:
- 'v*'
env:
IMAGE_NAME: xmrpay
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
runs-on: ubuntu-latest
environment: DOCKER
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Inject version into source
run: |
sed -i "s|VERSION = '[^']*'|VERSION = '${{ steps.version.outputs.version }}'|" i18n.js
sed -i -E "s|(<span class=\"version\">v)[^<]*(</span>)|\1${{ steps.version.outputs.version }}\2|" index.html
- name: Minify JS
run: |
npm i -g terser
terser app.js -c -m -o app.min.js
terser i18n.js -c -m -o i18n.min.js
- name: Update SRI hashes
run: |
sri() { echo "sha384-$(openssl dgst -sha384 -binary "$1" | openssl base64 -A)"; }
H_STYLE=$(sri style.css)
H_QRCODE=$(sri lib/qrcode.min.js)
H_I18N=$(sri i18n.min.js)
H_JSPDF=$(sri lib/jspdf.min.js)
H_CRYPTO=$(sri lib/xmr-crypto.bundle.js)
# Update dynamic SRI in app.js and re-minify
sed -i -E \
-e "s|(jspdf\.min\.js.*integrity\s*=\s*')sha384-[A-Za-z0-9+/=]+|\1${H_JSPDF}|" \
-e "s|(xmr-crypto\.bundle\.js.*integrity\s*=\s*')sha384-[A-Za-z0-9+/=]+|\1${H_CRYPTO}|" \
app.js
terser app.js -c -m -o app.min.js
H_APP=$(sri app.min.js)
# Update index.html
sed -i -E \
-e "s|(style\.css[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_STYLE}|" \
-e "s|(qrcode\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_QRCODE}|" \
-e "s|(i18n\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_I18N}|" \
-e "s|(app\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_APP}|" \
index.html
# Update privacy.html
sed -i -E \
-e "s|(style\.css[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_STYLE}|" \
privacy.html
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
schmidt1024/${{ env.IMAGE_NAME }}:latest
schmidt1024/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest
ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max

24
Caddyfile Normal file
View File

@@ -0,0 +1,24 @@
{$DOMAIN:localhost} {
root * /srv
encode gzip
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "no-referrer"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'"
}
# Short URL rewrite: /s/CODE -> s.php?c=CODE
@shorturl path_regexp short ^/s/([a-zA-Z0-9]+)$
rewrite @shorturl /s.php?c={re.short.1}
# PHP via FPM
php_fastcgi 127.0.0.1:9000
# Static files
file_server
}

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
FROM php:8.3-fpm-alpine AS base
# Install PHP curl extension (needed for API proxies)
RUN apk add --no-cache caddy curl-dev \
&& docker-php-ext-install curl \
&& rm -rf /var/cache/apk/*
# PHP-FPM tuning for low-memory VPS
RUN { \
echo '[www]'; \
echo 'pm = ondemand'; \
echo 'pm.max_children = 8'; \
echo 'pm.process_idle_timeout = 60s'; \
} > /usr/local/etc/php-fpm.d/zz-tuning.conf
# App files
COPY index.html privacy.html style.css sw.js favicon.svg s.php /srv/
COPY app.min.js /srv/app.min.js
COPY i18n.min.js /srv/i18n.min.js
COPY api/ /srv/api/
COPY lib/ /srv/lib/
COPY fonts/ /srv/fonts/
# Writable data directory
RUN mkdir -p /srv/data && chown www-data:www-data /srv/data
# Caddyfile
COPY Caddyfile /etc/caddy/Caddyfile
# Entrypoint: start PHP-FPM + Caddy
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
EXPOSE 80 443
VOLUME ["/srv/data", "/data/caddy"]
ENTRYPOINT ["docker-entrypoint.sh"]

View File

@@ -145,7 +145,7 @@ xmrpay.link/
## Self-Hosting ## Self-Hosting
```bash ```bash
git clone https://gitea.schmidt.eco/schmidt1024/xmrpay.link.git git clone https://github.com/schmidt1024/xmrpay.git
cd xmrpay.link cd xmrpay.link
# Serve with any web server that supports PHP # Serve with any web server that supports PHP
# No build tools, no npm, no database required # No build tools, no npm, no database required

View File

@@ -14,13 +14,19 @@ function send_security_headers(): void {
// ── Origin verification ─────────────────────────────────────────────────────── // ── Origin verification ───────────────────────────────────────────────────────
function verify_origin(): void { function verify_origin(): void {
$allowed = [
'https://xmrpay.link',
'http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion',
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
// Allow same-origin (no Origin header from direct same-origin requests) // Allow same-origin (no Origin header from direct same-origin requests)
if ($origin === '') return; if ($origin === '') return;
// Dynamically allow the host this instance runs on
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$self_origin = $scheme . '://' . ($_SERVER['HTTP_HOST'] ?? '');
$allowed = [
$self_origin,
'https://xmrpay.link',
'http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion',
];
if (!in_array($origin, $allowed, true)) { if (!in_array($origin, $allowed, true)) {
http_response_code(403); http_response_code(403);
echo json_encode(['error' => 'Origin not allowed']); echo json_encode(['error' => 'Origin not allowed']);

27
docker-compose.yml Normal file
View File

@@ -0,0 +1,27 @@
services:
xmrpay:
image: ${XMRPAY_IMAGE:-schmidt1024/xmrpay:latest}
container_name: xmrpay
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- DOMAIN=${DOMAIN:-localhost}
volumes:
- xmrpay-data:/srv/data
- caddy-data:/data/caddy
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: unless-stopped
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=21600
volumes:
- /var/run/docker.sock:/var/run/docker.sock
volumes:
xmrpay-data:
caddy-data:

8
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
# Start PHP-FPM in background
php-fpm &
# Run Caddy in foreground
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile

View File

@@ -13,7 +13,7 @@ var I18n = (function () {
var VERSION = '1.0.0'; var VERSION = '1.0.0';
var footer = 'Open Source &middot; No Tracking &middot; No KYC<br /><a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a><br /><span class="version">v' + VERSION + '</span>'; var footer = 'Open Source &middot; No Tracking &middot; No KYC<br /><a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a><br /><span class="version">v' + VERSION + '</span>';
var translations = { var translations = {
en: { en: {

View File

@@ -115,7 +115,7 @@
</main> </main>
<footer> <footer>
<p data-i18n-html="footer">Open Source &middot; No Tracking &middot; No KYC<br /><a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a><br /><span class="version">v1.0.0</span></p> <p data-i18n-html="footer">Open Source &middot; No Tracking &middot; No KYC<br /><a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a><br /><span class="version">v1.0.0</span></p>
</footer> </footer>
<div class="lang-picker" id="langPicker"> <div class="lang-picker" id="langPicker">

60
install.sh Normal file
View File

@@ -0,0 +1,60 @@
#!/bin/sh
set -e
# xmrpay.link — Self-hosting installer
# Usage: curl -sL https://xmrpay.link/install.sh | sh -s your-domain.com
DOMAIN="${1:-}"
INSTALL_DIR="/opt/xmrpay"
IMAGE="schmidt1024/xmrpay:latest"
COMPOSE_URL="https://raw.githubusercontent.com/schmidt1024/xmrpay/master/docker-compose.yml"
# ── Helpers ───────────────────────────────────────────────────────────────────
info() { printf '\033[1;34m→\033[0m %s\n' "$1"; }
ok() { printf '\033[1;32m✓\033[0m %s\n' "$1"; }
fail() { printf '\033[1;31m✗\033[0m %s\n' "$1" >&2; exit 1; }
# ── Preflight ─────────────────────────────────────────────────────────────────
[ "$(id -u)" -eq 0 ] || fail "Run as root: curl -sL https://xmrpay.link/install.sh | sudo sh -s $DOMAIN"
[ -n "$DOMAIN" ] || fail "Usage: curl -sL https://xmrpay.link/install.sh | sh -s YOUR-DOMAIN.COM"
# ── Install Docker if missing ─────────────────────────────────────────────────
if ! command -v docker >/dev/null 2>&1; then
info "Installing Docker..."
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
ok "Docker installed"
else
ok "Docker found"
fi
# ── Set up xmrpay ────────────────────────────────────────────────────────────
info "Setting up xmrpay in $INSTALL_DIR..."
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
curl -fsSL "$COMPOSE_URL" -o docker-compose.yml
cat > .env <<EOF
DOMAIN=$DOMAIN
XMRPAY_IMAGE=$IMAGE
EOF
# ── Start ─────────────────────────────────────────────────────────────────────
info "Starting xmrpay..."
docker compose pull
docker compose up -d
ok "xmrpay is running!"
echo ""
echo " https://$DOMAIN"
echo ""
echo " Watchtower checks for updates every 6 hours."
echo " Data stored in Docker volume: xmrpay-data"
echo " Config: $INSTALL_DIR/.env"
echo ""

View File

@@ -198,7 +198,7 @@
</main> </main>
<footer> <footer>
<p data-i18n-html="footer">Open Source &middot; No Tracking &middot; No KYC &middot; <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a></p> <p data-i18n-html="footer">Open Source &middot; No Tracking &middot; No KYC &middot; <a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a></p>
</footer> </footer>
<div class="lang-picker" id="langPicker"> <div class="lang-picker" id="langPicker">

View File

@@ -68,6 +68,15 @@ else
echo "Skipping pre-deploy backup (DEPLOY_BACKUP_ENABLE=0)." echo "Skipping pre-deploy backup (DEPLOY_BACKUP_ENABLE=0)."
fi fi
# ── Inject version from git tags ──────────────────────────────────────────────
GIT_VERSION=$(git describe --tags --always 2>/dev/null || echo "dev")
# Turn v1.0.0-3-gabc1234 into 1.0.0+3
VERSION=$(echo "$GIT_VERSION" | sed -E 's/^v//; s/-([0-9]+)-g[0-9a-f]+$/+\1/')
echo "Version: $VERSION"
sed -i -E "s|VERSION = '[^']*'|VERSION = '${VERSION}'|" i18n.js
sed -i -E "s|(<span class=\"version\">v)[^<]*(</span>)|\1${VERSION}\2|" index.html
# ── Minify & update SRI hashes ──────────────────────────────────────────────── # ── Minify & update SRI hashes ────────────────────────────────────────────────
echo "Minifying JS..." echo "Minifying JS..."
TERSER="${TERSER:-terser}" TERSER="${TERSER:-terser}"