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 (recommended)
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.
Clone the repository
git clone --branch v0.1.0 https://github.com/thezem/reacher.git
cd reacher
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
Start the service
Docker Compose will build the image and start the container in the background.
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:
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:
Build the image
docker build -t reacher .
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
Rebuild and restart
Docker Compose
Manual docker run
docker compose up -d --build
Dockerfile reference
The included Dockerfile produces a minimal production image:
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