OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / Dockerfile

181 lines · text ref:
# syntax=docker/dockerfile:1.6
#
# p2p-hiverelay — P2P relay backbone for the Holepunch/Pear ecosystem.
#
# Multi-arch (linux/amd64, linux/arm64) — designed for Umbrel Home (ARM)
# AND x86 Umbrel/server hosts. Built via `docker buildx build --platform`.
#
# Multi-stage build:
#   Stage 1 (deps):    install production deps for all workspaces
#   Stage 2 (runtime): minimal Debian bookworm-slim runtime, non-root user, tini PID 1
#
# Build:
#   docker build -t p2p-hiverelay:latest .
#
# Multi-arch build (push to registry):
#   docker buildx build --platform linux/amd64,linux/arm64 \
#     -t hiverelay/hiverelay:0.6.0 -t hiverelay/hiverelay:latest --push .
#
# Quick run (data volume + API port published):
#   docker run -d --name hiverelay \
#     -v hiverelay-data:/data \
#     -p 9100:9100 \
#     p2p-hiverelay:latest
#
# Open the TUI (connects to the running container's API):
#   docker exec -it hiverelay hiverelay tui
#
# Environment overrides:
#   HIVERELAY_REGION=NA           (region code)
#   HIVERELAY_MAX_STORAGE=50GB    (accepts human-readable sizes)
#   HIVERELAY_API_KEY=...         (secures management endpoints)
#   HIVERELAY_API_PORT=9100       (API port inside container)
#   HIVERELAY_HOLESAIL=1          (enable Holesail for NAT traversal)
#   LNBITS_URL=http://...         (LNbits payment provider; auto-detected on Umbrel)
#   LNBITS_ADMIN_KEY=...          (LNbits admin key for invoice creation)

# ─── Stage 1: dependencies ────────────────────────────────────────────
# Use Debian bookworm-slim (glibc) instead of Alpine (musl). Two upstream
# Bare ecosystem packages — udx-native, sodium-native — ship prebuilt
# binaries for `linux-x64`/`linux-arm64` (glibc) but NOT for
# `linux-x64-musl`/`linux-arm64-musl`. On Alpine, require-addon detects
# musl via /etc/alpine-release and looks for a musl prebuild that doesn't
# exist → crash at first import. Building from source on Alpine works
# but requires cmake-bare/cmake-napi + python3 + make + g++ in BOTH
# stages (the binary lands in a path require-addon doesn't search by
# default) and roughly doubles the runtime image. Debian bookworm-slim
# is ~50 MB larger than Alpine but loads the glibc prebuilds directly.
#
# Tracked in issue #21. Reconsider when udx-native ships musl prebuilds:
#   https://github.com/holepunchto/udx-native
#
# Node 22 LTS — Bare/Pear runtime targets stay aligned.
FROM node:22-bookworm-slim@sha256:53ada149d435c38b14476cb57e4a7da73c15595aba79bd6971b547ceb6d018bf AS deps
WORKDIR /app

# Install build tools needed for any native deps that DO build from
# source on Linux (sodium-universal's fallback, hypercore-crypto, etc).
# Debian-based — no musl complications.
RUN apt-get update && \
    apt-get install -y --no-install-recommends python3 make g++ git ca-certificates && \
    rm -rf /var/lib/apt/lists/*

# Copy ALL workspace package.json files (npm needs them all to resolve workspaces)
COPY package.json package-lock.json ./
COPY packages/blind-protocol/package.json packages/blind-protocol/
COPY packages/blind-ipc/package.json packages/blind-ipc/
COPY packages/blind-client/package.json packages/blind-client/
COPY packages/blind-peercred/package.json packages/blind-peercred/
COPY packages/blind-peercred/binding.gyp packages/blind-peercred/peercred.cc packages/blind-peercred/
COPY packages/blind-edge/package.json packages/blind-edge/
COPY packages/blind-daemon/package.json packages/blind-daemon/
COPY packages/core/package.json packages/core/
COPY packages/services/package.json packages/services/
COPY packages/client/package.json packages/client/
COPY packages/verifier/package.json packages/verifier/

# Install with build-time tooling so patch-package and native workspace builds
# run against the exact lockfile, then prune development dependencies without
# rerunning lifecycle scripts. The resulting node_modules tree is production-only.
RUN npm ci --workspaces --include-workspace-root --no-audit --no-fund && \
    npm prune --omit=dev --workspaces --include-workspace-root --ignore-scripts --no-audit --no-fund

# ─── Stage 2: runtime ─────────────────────────────────────────────────
FROM node:22-bookworm-slim@sha256:53ada149d435c38b14476cb57e4a7da73c15595aba79bd6971b547ceb6d018bf AS runtime

# tini for proper PID 1 signal handling (graceful shutdown).
# wget for HEALTHCHECK without bringing curl/openssl bloat.
# ca-certificates so HTTPS to public registries / payment providers works.
# libatomic1 is required by the shipped rocksdb-native Linux prebuilds on ARM64
# (and is harmless on AMD64); without it Node reports the present addon as
# ADDON_NOT_FOUND because its dynamic linker dependency cannot be resolved.
RUN apt-get update && \
    apt-get install -y --no-install-recommends tini wget ca-certificates gosu libatomic1 && \
    rm -rf /var/lib/apt/lists/*

# Keep source identity after the expensive OS layer so a new sealed commit does
# not invalidate dependency caches. The release workflow supplies the exact SHA.
ARG SOURCE_COMMIT
LABEL org.opencontainers.image.title="p2p-hiverelay"
LABEL org.opencontainers.image.description="Always-on P2P relay infrastructure for the Holepunch/Pear ecosystem"
LABEL org.opencontainers.image.source="https://github.com/bigdestiny2/P2P-Hiverelay"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL org.opencontainers.image.revision="$SOURCE_COMMIT"

WORKDIR /app

# Bring in already-installed modules from the deps stage. npm 7+ hoists
# most workspace deps to the root `node_modules/`. Per-package
# `node_modules/` only exist when there's a version conflict — historically
# `packages/core/node_modules/` etc. weren't created by `npm ci --workspaces`
# at all, so the per-package COPY commands here used to fail the whole
# build. Copy the root tree once; that's enough for production startup.
COPY --from=deps /app/node_modules ./node_modules

# Copy application source (respects .dockerignore)
COPY . .

# blind-peercred is compiled in the dependency stage where node-gyp and the
# compiler toolchain are present. Its workspace build output is not hoisted
# into node_modules, so carry it into the otherwise toolchain-free runtime.
COPY --from=deps /app/packages/blind-peercred/build ./packages/blind-peercred/build

# Non-root user for security. Fixed UID/GID so volume permissions stay
# consistent across image rebuilds — operators with existing data
# volumes don't get bitten by an auto-assigned UID drift between builds.
RUN groupadd -r -g 999 hiverelay && \
    useradd -r -u 999 -g hiverelay -d /data -s /usr/sbin/nologin hiverelay && \
    mkdir -p /data /config && \
    chown -R hiverelay:hiverelay /app /data /config

# Make the hiverelay binary globally callable inside the container, so
# `docker exec -it hiverelay hiverelay tui` just works.
RUN ln -s /app/packages/core/cli/index.js /usr/local/bin/p2p-hiverelay && \
    ln -s /app/packages/core/cli/index.js /usr/local/bin/hiverelay && \
    chmod +x /app/packages/core/cli/index.js

# Entrypoint: self-heal /data ownership then drop privileges.
#
# Self-hosting platforms (Umbrel, StartOS) bind-mount a host directory over
# /data whose owner is the host user, NOT the image's build-time uid 999. A
# non-root container then can't create its store -> EACCES on startup. So we
# start as root, fix ownership only when it's wrong (cheap on restarts), and
# drop to the unprivileged `hiverelay` user via gosu before exec'ing node.
# COPY the entrypoint from a committed file rather than generating it inline.
# Some remote builders can lose heredoc-generated files, which leaves the
# image without /usr/local/bin/docker-entrypoint.sh and exits 127 on boot.
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

# NOTE: no `USER` directive — the entrypoint starts as root to fix the
# bind-mount ownership, then gosu-drops to uid 999. The relay process itself
# runs unprivileged.

VOLUME ["/data", "/config"]

# API port. Gateway (9200) and other transport ports may need their own
# `-p` mappings when you enable them.
EXPOSE 9100

ENV NODE_ENV=production \
    HIVERELAY_STORAGE=/data \
    HIVERELAY_CONFIG_DIR=/config \
    HIVERELAY_LOG_LEVEL=info \
    HIVERELAY_API_PORT=9100 \
    HIVERELAY_API_HOST=0.0.0.0

# Health check hits the local API. wget is a small http client; using it
# instead of node -e fetch() keeps healthcheck startup fast and avoids
# loading the entire app just to check liveness.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
  CMD wget --quiet --tries=1 --timeout=4 --spider \
    http://127.0.0.1:${HIVERELAY_API_PORT:-9100}/health || exit 1

# tini as PID 1 → graceful SIGTERM handling so shutdown actually runs.
# Debian installs tini at /usr/bin/tini (vs Alpine's /sbin/tini).
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/docker-entrypoint.sh", "node", "/app/packages/core/cli/index.js"]

# Default: start a relay node. Override to run other subcommands, e.g.:
#   docker run ... p2p-hiverelay:latest help
CMD ["start"]