Skip to main content
Docker is the recommended way to run Reacher in production. It handles dependencies, provides automatic restarts, and isolates the process cleanly.

Prerequisites

  • Docker installed on your host machine
  • .env file configured with your credentials (copy from .env.example)
  • reacher.config.yaml configured (copy from reacher.config.example.yaml)
Generate a strong MCP_SECRET before you start: openssl rand -hex 32
Docker Compose is the simplest path for most deployments. It builds the image, maps ports, loads your .env, and restarts automatically on crash or reboot.
1

Clone the repository

git clone --branch v0.1.0 https://github.com/thezem/reacher.git
cd reacher
2

Configure environment and config files

cp .env.example .env
cp reacher.config.example.yaml reacher.config.yaml
Edit both files with your credentials. At minimum, set:
MCP_SECRET=<random-string>
TAILSCALE_API_KEY=<your-tailscale-api-key>
GITHUB_TOKEN=<your-github-token>
PROXY_ALLOWED_DOMAINS=api.github.com
3

Start the service

docker compose up -d
Docker Compose will build the image and start the container in the background.
4

Verify it is running

docker logs mcp-server
curl "http://localhost:3000/health?token=YOUR_MCP_SECRET"
You should see {"status":"ok",...} from the health check endpoint.

docker-compose.yml

This is the full Compose file included in the repository:
docker-compose.yml
version: '3.8'

services:
  mcp-server:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: mcp-server
    ports:
      - "${PORT:-3000}:${PORT:-3000}"
    environment:
      PORT: ${PORT:-3000}
      TAILSCALE_API_KEY: ${TAILSCALE_API_KEY}
      # TELEGRAM_BOT_TOKEN and DEFAULT_CHAT_ID are unused leftover vars in the compose file
      TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
      DEFAULT_CHAT_ID: ${DEFAULT_CHAT_ID}
    env_file:
      - .env
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "node", "-e", "require('http').get('http://localhost:' + (process.env.PORT || 3000), (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 5s
    volumes:
      - ./src:/app/src
      - ./index.js:/app/index.js
    # Uncomment for local development with auto-reload
    # command: npm run dev

Manual docker run

If you prefer to manage the container directly without Compose:
1

Build the image

docker build -t reacher .
2

Run the container

docker run -d \
  -p 3000:3000 \
  --env-file .env \
  --restart unless-stopped \
  --name reacher \
  reacher
The flags do the following:
  • -d — run in the background (detached)
  • -p 3000:3000 — map host port 3000 to container port 3000
  • --env-file .env — inject all variables from your .env file
  • --restart unless-stopped — restart automatically on crash or host reboot
  • --name reacher — give the container a stable name for log and management commands

Checking logs

docker logs reacher
docker logs reacher --follow   # stream live output
For Docker Compose deployments, use the service name:
docker compose logs -f mcp-server

Health check

The /health endpoint returns the current server status. It requires the same token as the /mcp endpoint:
curl "http://localhost:3000/health?token=YOUR_MCP_SECRET"
{
  "status": "ok",
  "timestamp": "2026-03-18T12:00:00.000Z",
  "dry_run": false
}
Docker runs its own built-in health check every 30 seconds against this endpoint. You can inspect it with:
docker inspect --format='{{.State.Health.Status}}' reacher

Updating to a new version

1

Pull the latest code

git pull
2

Rebuild and restart

docker compose up -d --build

Dockerfile reference

The included Dockerfile produces a minimal production image:
Dockerfile
FROM node:22-alpine

# Install openssh-client for ssh_exec tool
RUN apk add --no-cache openssh-client

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies with production flag
RUN npm install --omit=dev

# Copy application code
COPY . .

# Expose port (default 3000, can be overridden)
EXPOSE ${PORT:-3000}

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:' + (process.env.PORT || 3000), (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"

# Start the server
CMD ["node", "index.js"]
Key details:
  • Base image: node:22-alpine — small Alpine-based Node 22 image
  • openssh-client: required for the ssh_exec tool to reach Tailscale devices
  • npm install --omit=dev: installs only production dependencies to keep the image lean
  • Built-in health check: polls the server’s HTTP port every 30 seconds