diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 727cb844..b303a088 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -8,7 +8,8 @@ name: Docker publish # Deliberately does NOT move `:edge` or `:latest`. # * push to main → push `:edge` + `:sha-` (bleeding edge). # * push a `vX.Y.Z` tag → push `:X.Y.Z`, `:X.Y`, and `:latest`, then cut a GitHub -# Release. So `:latest` always tracks the newest *release*, +# Release with standalone binaries and checksums. +# So `:latest` always tracks the newest *release*, # never raw main. # # Pin a deployment to `:sha-`, never to `:dev` or `:edge` — those move under @@ -28,6 +29,7 @@ env: # Hardcoded lowercase — GHCR tags reject the repo's actual "Calnode/calnode" # casing (${{ github.repository }} would break the push). IMAGE_NAME: calnode/calnode + BUILD_PLATFORMS: linux/amd64,linux/arm64 jobs: build: @@ -73,7 +75,7 @@ jobs: uses: docker/build-push-action@v7 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: ${{ env.BUILD_PLATFORMS }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -81,6 +83,37 @@ jobs: VERSION=${{ steps.meta.outputs.version }} COMMIT=${{ github.sha }} + - name: Export standalone binaries + uses: docker/build-push-action@v7 + with: + context: . + target: standalone + platforms: ${{ env.BUILD_PLATFORMS }} + outputs: type=local,dest=dist/binaries,platform-split=true + build-args: | + VERSION=${{ steps.meta.outputs.version }} + COMMIT=${{ github.sha }} + + - name: Package standalone binaries + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + mkdir -p dist/release + for platform_dir in dist/binaries/linux_*; do + platform="${platform_dir##*/}" + tar -czf "dist/release/calnode_${VERSION#v}_${platform}.tar.gz" \ + -C "$platform_dir" calnode LICENSE + done + cd dist/release + sha256sum ./*.tar.gz > checksums.txt + + - name: Upload standalone binaries + uses: actions/upload-artifact@v4 + with: + name: standalone-binaries + path: dist/release/ + if-no-files-found: error + release: # Only on a version tag, and only once the image built + pushed. if: startsWith(github.ref, 'refs/tags/v') @@ -91,6 +124,11 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: standalone-binaries + path: dist/release + # Pull the matching version's section out of CHANGELOG.md for the release body, # so the GitHub Release and the CHANGELOG stay identical. Index-based (not a `\[` # regex) so it's portable across awk flavours. Falls back to GitHub's @@ -119,8 +157,8 @@ jobs: run: | if [ "${{ steps.notes.outputs.have_notes }}" = "true" ]; then gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" \ - --notes-file release-notes.md --verify-tag + --notes-file release-notes.md --verify-tag dist/release/* else gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" \ - --generate-notes --verify-tag + --generate-notes --verify-tag dist/release/* fi diff --git a/DEPLOY.md b/DEPLOY.md index 0cddc97a..7e298686 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -14,9 +14,41 @@ small Alpine runtime) and published to GHCR: Or build it yourself with `docker build -t calnode .`. -This guide covers a generic Docker deploy and a step-by-step **Railway** deploy +This guide covers a standalone binary, a generic Docker deploy and a step-by-step **Railway** deploy (the current reference host). +## Standalone binary + +Download `calnode__linux_amd64.tar.gz` (x86-64) or +`calnode__linux_arm64.tar.gz` (ARM64), plus `checksums.txt`, from the same +[GitHub release](https://github.com/Calnode/calnode/releases). Each archive contains +the same static executable as the corresponding container image, with the admin +UI and timezone database embedded, and the Apache-2.0 license. No Go, Node or Docker +installation is needed to run it. + +For example, after downloading the amd64 archive and checksums for your chosen release: + +```bash +version=X.Y.Z # replace with the downloaded release version, without the v prefix +sha256sum --check --ignore-missing checksums.txt +tar -xzf "calnode_${version}_linux_amd64.tar.gz" +mkdir -p data +export CALNODE_ENCRYPTION_KEY="$(openssl rand -hex 32)" +export CALNODE_RECOVERY_SECRET="$(openssl rand -hex 32)" +BASE_URL=http://localhost:3000 DATABASE_URL=sqlite://./data/calnode.db ./calnode +``` + +Save both generated secrets before restarting; reuse them on subsequent starts and +keep the recovery secret separately. For production, set `BASE_URL` to your HTTPS +address, use a persistent data directory, and place the process behind a +TLS-terminating proxy that preserves `Host`. The Linux host needs a CA certificate +store for outbound HTTPS. Run the process under a service manager for automatic restarts. + +The archive does **not** include Litestream or the container entrypoint. Setting +`LITESTREAM_REPLICA_URL` alone will not start backups or restore a database when running +the binary directly; configure Litestream separately or use another SQLite-aware +backup method. The email, calendar and other integration settings below still apply. + --- ## 1. Configuration (environment variables) diff --git a/Dockerfile b/Dockerfile index 605af93c..ab6d4426 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,11 @@ RUN wget -qO- \ "https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-v${LITESTREAM_VERSION}-linux-${TARGETARCH}.tar.gz" \ | tar -xz -C /usr/local/bin litestream +# Export the same executable as the container, including its embedded frontend. +FROM scratch AS standalone +COPY --from=builder /build/calnode /calnode +COPY LICENSE /LICENSE + # ── Runtime stage ───────────────────────────────────────────────────────────── # alpine (not scratch) — needed for the shell entrypoint and Litestream. # No --platform pin here: inherits the build host's native architecture, diff --git a/README.md b/README.md index ac5c67d0..411efeb1 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,12 @@ bookings they host. (The stdio subcommand is the local operator → full access. ## Deploy for real +**Without Docker:** [GitHub releases](https://github.com/Calnode/calnode/releases) +include standalone Linux amd64 and arm64 archives, each containing the static +`calnode` binary and its license, alongside SHA-256 checksums. See +[running the standalone binary](DEPLOY.md#standalone-binary) for setup and backup +differences from the container. + ```bash docker run -d -p 3000:3000 \ -e BASE_URL=https://booking.example.com \