# -------------------------------------------------------------------------- # SparkyFitness — FreeBSD daemonless container # Self-hosted, privacy-first fitness tracker # -------------------------------------------------------------------------- ARG BASE_VERSION=15 ARG UPSTREAM_URL="https://api.github.com/repos/CodeWithCJ/SparkyFitness/releases/latest" # ========================================================================== # Stage 1: Builder # ========================================================================== FROM ghcr.io/daemonless/base-exec:${BASE_VERSION} AS builder ARG UPSTREAM_URL # Build dependencies RUN pkg update && pkg install -y \ node24 npm-node24 corepack \ git-lite jq \ && pkg clean -ay # Enable pnpm via corepack RUN corepack enable && corepack prepare pnpm@10.33.4 --activate # Clone latest release WORKDIR /build RUN --mount=type=secret,id=github_token \ GITHUB_TOKEN=$(cat /run/secrets/github_token 2>/dev/null || echo ""); \ if [ -n "${GITHUB_TOKEN}" ]; then \ export HTTP_AUTH="basic:*:x-access-token:${GITHUB_TOKEN}"; \ fi && \ VERSION=$(fetch -qo - "${UPSTREAM_URL}" | jq -r '.tag_name') && \ echo "Resolved VERSION=$VERSION" && \ git clone --depth 1 --branch ${VERSION} \ https://github.com/CodeWithCJ/SparkyFitness.git . && \ echo "${VERSION}" > /tmp/app_version # sharp on FreeBSD is @img/sharp-freebsd-wasm32, a shim over @img/sharp-wasm32 # (cpu: wasm32). Upstream's supportedArchitectures leaves wasm32 out, so the # server dies with 'Could not load the "sharp" module'. Fail if the list moves. RUN awk '{ print } /^ cpu:$/ { print " - wasm32" }' pnpm-workspace.yaml > pnpm-workspace.yaml.new && \ mv pnpm-workspace.yaml.new pnpm-workspace.yaml && \ grep -q '^ - wasm32$' pnpm-workspace.yaml # Install workspace dependencies for frontend + server RUN pnpm install --frozen-lockfile \ --filter sparkyfitnessfrontend... \ --filter sparkyfitnessserver... # Build frontend (React + Vite) RUN pnpm --filter sparkyfitnessfrontend run build # Deploy server to standalone directory (flat node_modules, no workspace symlinks) RUN pnpm --filter=sparkyfitnessserver deploy --legacy /deploy # Ensure tsx is available in the deployed server # (pnpm deploy --legacy may skip devDependencies) RUN if [ ! -f /deploy/node_modules/.bin/tsx ]; then \ cd /deploy && npm install tsx; \ fi # ========================================================================== # Stage 2: Runtime # ========================================================================== FROM ghcr.io/daemonless/base-rcd:${BASE_VERSION} ARG FREEBSD_ARCH=amd64 ARG PACKAGES="node24 nginx postgresql18-client" ARG UPSTREAM_URL="https://api.github.com/repos/CodeWithCJ/SparkyFitness/releases/latest" ARG UPSTREAM_JQ=".tag_name" ARG HEALTHCHECK_ENDPOINT="http://localhost:3004" ENV HEALTHCHECK_URL="${HEALTHCHECK_ENDPOINT}" LABEL org.opencontainers.image.title="SparkyFitness" \ org.opencontainers.image.description="Self-hosted privacy-first fitness tracker on FreeBSD" \ org.opencontainers.image.source="https://github.com/daemonless/sparkyfitness" \ org.opencontainers.image.url="https://github.com/CodeWithCJ/SparkyFitness" \ org.opencontainers.image.version="latest" \ org.opencontainers.image.licenses="NOASSERTION" \ org.opencontainers.image.vendor="daemonless" \ org.opencontainers.image.authors="daemonless" \ io.daemonless.category="Utilities" \ io.daemonless.port="3004" \ io.daemonless.arch="${FREEBSD_ARCH}" \ io.daemonless.upstream-url="${UPSTREAM_URL}" \ io.daemonless.upstream-jq="${UPSTREAM_JQ}" \ io.daemonless.healthcheck-url="${HEALTHCHECK_ENDPOINT}" \ io.daemonless.volumes="/app/server/uploads,/app/server/backup" \ io.daemonless.packages="${PACKAGES}" # Install runtime dependencies RUN pkg update && \ pkg install -y ${PACKAGES} && \ mkdir -p /app/server /app/client \ /var/log/nginx /var/run /var/tmp/nginx && \ pkg clean -ay && \ rm -rf /var/cache/pkg/* /var/db/pkg/repos/* # Copy built artifacts from builder COPY --from=builder /tmp/app_version /app/version COPY --from=builder --chown=bsd:bsd /build/SparkyFitnessFrontend/dist /app/client COPY --from=builder --chown=bsd:bsd /deploy /app/server # Set ownership — /app and /app/client need to be world-readable for nginx worker (www user) RUN mkdir -p /app/server/uploads /app/server/backup && \ chown -R bsd:bsd /app && \ chmod o+x /app && \ chmod -R o+rX /app/client # Copy service files (s6 runners, nginx config, healthz) COPY root/ / RUN chmod +x /daemonless/libexec/* /daemonless/init.d/* RUN chmod +x /healthz WORKDIR /app # Pin the server's data dirs to the declared volumes. The values equal the app's defaults # (utils/uploadsPath.ts and services/backupService.ts resolve uploads/ and backup/ under # /app/server), so nothing changes today; if upstream moves those defaults, data still lands # in the declared volumes instead of the container layer. Backups are pinned through the # lowest-precedence variable (backupService.ts: SPARKY_FITNESS_CUSTOM_BACKUP_DIRECTORY, then # BACKUP_DIR, then the default), so a user's setting of either variable still wins. ENV NODE_ENV=production \ NODE_OPTIONS="--max-old-space-size=256" \ SPARKY_FITNESS_CUSTOM_UPLOADS_DIRECTORY=/app/server/uploads \ BACKUP_DIR=/app/server/backup EXPOSE 3004 VOLUME /app/server/uploads /app/server/backup