Skip to content

Make Dockerfile cross-platform (ARM support) #1

Closed

Description

@readtedium

Hey, this is pretty solid and clean, and makes me less mad than cal dot com does. One issue I ran into however is that I run an ARM server and this doesn’t currently account for an ARM build (even though it is fully capable of compiling on ARM). Here’s the replacement Dockerfile I’m working with.

# syntax=docker/dockerfile:1
# ── Frontend build stage ───────────────────────────────────────────────────────
FROM node:22-alpine AS frontend-builder
# Pin pnpm to the version in package.json's `packageManager` field (not @latest)
# so CI builds are reproducible and match the committed lockfile.
RUN corepack enable && corepack prepare pnpm@10.32.1 --activate
WORKDIR /app
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY frontend/ .
RUN pnpm build
# ── Go build stage ─────────────────────────────────────────────────────────────
FROM golang:1.26-alpine AS builder
RUN apk add --no-cache ca-certificates wget
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=frontend-builder /app/build ./frontend/build
# Build natively for whatever architecture this stage is running on
# (TARGETARCH is auto-populated by Docker buildx; falls back correctly
# on ARM hosts where the original hardcoded amd64 would produce a
# binary that can't execute at all).
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \
    -ldflags="-s -w" \
    -o calnode ./cmd/calnode
# Download Litestream for the deployment target, matching TARGETARCH
ARG LITESTREAM_VERSION=0.3.13
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
# ── Runtime stage ─────────────────────────────────────────────────────────────
# alpine (not scratch) — needed for the shell entrypoint and Litestream.
# No --platform pin here: inherits the build host's native architecture,
# matching whatever TARGETARCH the binary above was actually compiled for.
FROM alpine:3.21
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /usr/local/bin/litestream /usr/local/bin/litestream
COPY --from=builder /build/calnode /calnode
COPY litestream.yml /etc/litestream.yml
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Note: no `VOLUME` directive — persistent storage is provided by the platform's
# managed volume mounted at /data (Railway rejects the Docker VOLUME instruction;
# Fly mounts via fly.toml). The dir is created at runtime by entrypoint.sh.
EXPOSE 3000
ENV PORT=3000 \
    DATABASE_URL=sqlite:///data/calnode.db
ENTRYPOINT ["/entrypoint.sh"]

Happy to propose a merge, but given that you’re still in the throes of working on this I don’t want to cramp your style just yet.

Activity

  1. shockalotti commented on Jun 29, 2026

    @shockalotti
    Contributor

    Hey @readtedium — thanks, genuinely useful catch and exactly the kind of issue I like to get.

    You're dead right: the Dockerfile hardcodes amd64 in three spots (the Go build, the Litestream download, and the runtime FROM --platform). The nice part is it'll cross-compile cleanly — Calnode uses pure-Go SQLite (modernc.org/sqlite), so with CGO_ENABLED=0 (already there) and GOARCH=${TARGETARCH} there's no C toolchain or qemu dance. ARM-everywhere (Graviton, Ampere, Apple Silicon, the Hetzner/Oracle ARM tiers) is squarely on-brand for a "runs anywhere, cheaply" tool, so this belongs in.

    Please do send the PR — you're not cramping anything. Couple of small notes:

    Keep it scoped to the Dockerfile; the hosted deploy uses Nixpacks, so this only touches the self-host Docker path (low risk).
    If it's easy, a line in DEPLOY.md on building for arm64 (docker buildx build --platform linux/arm64 …) would help the next person.

    Appreciate you testing on ARM and pinning the exact cause before opening — top-tier first issue 🙏

  2. readtedium commented on Jul 7, 2026

    @readtedium
    Author

    @shockalotti Hey, I apologize that I never got back to this, but thanks again for working the fix in!

  3. shockalotti commented on Jul 7, 2026

    @shockalotti
    Contributor

    Applied your fix as-is (thank you — saved a lot of digging): TARGETARCH for both the Go build and the Litestream download, plus dropping the --platform=linux/amd64 pin on the runtime stage. Verified locally on both linux/amd64 and linux/arm64 (QEMU) — built, booted, ran migrations, and served /admin/login successfully on each.

    Landed in a8c7571. Also wired up CI (.github/workflows/docker-publish.yml) to build and push a multi-arch image to ghcr.io/calnode/calnode on every push to main, so this doesn't just fix your local build — it becomes the actual published image going forward.

    Closing this out — shout if you hit anything on real ARM hardware that the QEMU emulation didn't catch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions