Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ name: Docker publish
# Deliberately does NOT move `:edge` or `:latest`.
# * push to main → push `:edge` + `:sha-<short>` (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-<short>`, never to `:dev` or `:edge` — those move under
Expand All @@ -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:
Expand Down Expand Up @@ -73,14 +75,45 @@ 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 }}
build-args: |
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')
Expand All @@ -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
Expand Down Expand Up @@ -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
34 changes: 33 additions & 1 deletion DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<version>_linux_amd64.tar.gz` (x86-64) or
`calnode_<version>_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)
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
Loading