diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d577728 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +data/ +scripts/ +branding/ +README.md +LICENSE +nginx-security-headers.conf +docker-compose.yml +.dockerignore +.gitignore +app.js +i18n.js diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..bfa5e46 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,96 @@ +name: Build & Push Docker Image + +on: + push: + tags: + - 'v*' + +env: + IMAGE_NAME: xmrpay + +jobs: + build: + runs-on: ubuntu-latest + 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|(v)[^<]*()|\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 diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..cfc5a4d --- /dev/null +++ b/Caddyfile @@ -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 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..60d571c --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7030887 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..3a0abf1 --- /dev/null +++ b/docker-entrypoint.sh @@ -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 diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..d7a2bb8 --- /dev/null +++ b/install.sh @@ -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.link/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 <