neural-runner / Dockerfile
glutamatt's picture
glutamatt HF Staff
chore: rebuild with updated GHCR images (45a463a691d190bfc41275335703fbc29195abb5)
67f00dc verified
# Multi-stage Dockerfile for Hugging Face Spaces
# Copies pre-built services from GHCR images into single runtime
# Build arg for image tag (defaults to latest, overridden by deploy workflow with SHA)
ARG IMAGE_TAG=main-sha-45a463a
# Build arg for git commit hash (passed from GitHub Actions)
ARG GIT_COMMIT=45a463a691d190bfc41275335703fbc29195abb5
# Stage 1: Extract Next.js standalone build
FROM ghcr.io/glutamatt/neural-runner/neural-runner-app:${IMAGE_TAG} AS nextjs-build
# Contains /app/.next/standalone, /app/.next/static, /app/public
# Stage 2: Extract Garmin MCP server
FROM ghcr.io/glutamatt/neural-runner/neural-runner-garmin-mcp:${IMAGE_TAG} AS garmin-mcp-build
# Contains /app/garmin_mcp Python package
# Stage 3: Extract COROS MCP server
FROM ghcr.io/glutamatt/neural-runner/neural-runner-coros-mcp:${IMAGE_TAG} AS coros-mcp-build
# Contains /app/coros_mcp Python package
# Stage 4: Extract Agent Tools server
FROM ghcr.io/glutamatt/neural-runner/neural-runner-agent-tools:${IMAGE_TAG} AS agent-tools-build
# Contains /app/agent_tools Python package
# Stage 5: Extract HF token sync sidecar
FROM ghcr.io/glutamatt/neural-runner/neural-runner-hf-storage-sync:${IMAGE_TAG} AS hf-storage-sync-build
# Contains /app/hf-storage-sync.py + huggingface_hub, watchfiles installed
# Stage 6: Tailscale binaries (VPN to garmin-connector on homelab)
FROM tailscale/tailscale:latest AS tailscale
# Final stage: Runtime environment for HF Spaces
FROM python:3.12-slim
# Re-declare build arg in final stage (ARGs don't carry over across FROM stages)
ARG GIT_COMMIT=45a463a691d190bfc41275335703fbc29195abb5
# Install Node.js 20.x and process-compose
RUN apt-get update && apt-get install -y \
curl \
wget \
jq \
bubblewrap \
fonts-dejavu-core fonts-liberation fonts-noto-color-emoji \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Tailscale binaries (userspace networking — no TUN device on HF Space)
COPY --from=tailscale /usr/local/bin/tailscaled /usr/local/bin/
COPY --from=tailscale /usr/local/bin/tailscale /usr/local/bin/
# Install process-compose (modern supervisor alternative)
RUN wget -qO- https://github.com/F1bonacc1/process-compose/releases/latest/download/process-compose_linux_amd64.tar.gz | \
tar xz -C /usr/local/bin process-compose && \
chmod +x /usr/local/bin/process-compose
# Set working directory
WORKDIR /home/user/app
# Create user with UID 1000 (HF requirement)
RUN useradd -m -u 1000 user && \
chown -R user:user /home/user
# Copy Next.js standalone build from stage 1
# The app target has files at /app/frontend/ (WORKDIR in app target)
COPY --from=nextjs-build --chown=user:user /app/frontend ./frontend
# Copy MCP server source code and dependencies to separate directories
COPY --from=garmin-mcp-build --chown=user:user /app ./mcp-servers/garmin-mcp
COPY --from=coros-mcp-build --chown=user:user /app ./mcp-servers/coros-mcp
COPY --from=agent-tools-build --chown=user:user /app ./mcp-servers/agent-tools
# Install MCP servers (regular install, NOT editable to avoid .pth path issues in multi-stage builds)
# IMPORTANT: Install forked garminconnect FIRST from the local copy inside the garmin-mcp image,
# otherwise pip resolves 'garminconnect' from PyPI (which lacks schedule_workout, delete_workout, etc.)
RUN pip install --no-cache-dir ./mcp-servers/garmin-mcp/python-garminconnect && \
pip install --no-cache-dir ./mcp-servers/garmin-mcp && \
pip install --no-cache-dir ./mcp-servers/coros-mcp && \
pip install --no-cache-dir ./mcp-servers/agent-tools && \
pip install --no-cache-dir PySocks
# Install hf-storage-sync deps and copy script
RUN pip install --no-cache-dir huggingface_hub watchfiles
COPY --from=hf-storage-sync-build --chown=user:user /app/hf-storage-sync.py ./hf-storage-sync.py
# Copy process-compose configuration
COPY --chown=user:user process-compose.yaml ./
# Copy boot script
COPY --chown=user:user boot.py ./
# Set environment variables
ENV PORT=7860 \
NODE_ENV=production \
MCP_GARMIN_URL=http://localhost:8080/mcp \
MCP_COROS_URL=http://localhost:8081/mcp \
MCP_AGENT_URL=http://localhost:8082/mcp \
PYTHONUNBUFFERED=1 \
NODE_OPTIONS="--max-old-space-size=4096" \
GIT_COMMIT=${GIT_COMMIT}
# Switch to user
USER user
# Expose HF Spaces default port
EXPOSE 7860
# Start services via boot script
CMD ["python3", "boot.py"]