Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules/
build/
.git/
.github/
test/
images/
*.md
.gitignore
.dockerignore
install-systemd.sh
run-service.sh
skills/
docs/
65 changes: 65 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Build and Push Docker Image

on:
push:
branches:
- main
tags:
- "v*"
pull_request:
branches:
- main

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up QEMU (multi-platform)
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix=sha-,format=short
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
37 changes: 37 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Stage 1: build
FROM node:22-alpine AS builder

WORKDIR /app

COPY package.json ./
RUN npm install

COPY tsconfig.json ./
COPY src/ ./src/
COPY scripts/ ./scripts/

RUN npm run build

# Stage 2: runtime
FROM node:22-alpine AS runner

WORKDIR /app

COPY package.json ./
RUN npm install --omit=dev

COPY --from=builder /app/build ./build
COPY docker-entrypoint.sh ./

RUN chmod +x docker-entrypoint.sh

# HTTP transport env vars
ENV HTTP_HOST=0.0.0.0
ENV HTTP_PORT=3001
ENV HTTP_PATH=/mcp
# HTTP_API_KEY — set at runtime, do not hardcode
# SSH_CONFIG_FILE — path inside container to mounted JSON config

EXPOSE 3001

ENTRYPOINT ["./docker-entrypoint.sh"]
127 changes: 127 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,133 @@ Options:
--pre-connect Pre-connect to all configured SSH servers on startup
```

## 🐳 Running with Docker (HTTP Transport)

The pre-built Docker image is published to GitHub Container Registry on every push to `main` and for every version tag.

### Pull the image

```bash
# Latest build from main
docker pull ghcr.io/someblackmagic/ssh-mcp-server:latest

# Specific release
docker pull ghcr.io/someblackmagic/ssh-mcp-server:1.8.2
Comment on lines +572 to +575

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point Docker commands at the published GHCR image

The workflow publishes to ghcr.io/${{ github.repository }}, and this README identifies the repository as classfang/ssh-mcp-server, so these commands direct users to ghcr.io/someblackmagic/ssh-mcp-server instead of the image this CI will build. Anyone following the Docker instructions will pull or run the wrong/nonexistent package unless the namespace matches the repository that owns the workflow.

Useful? React with 👍 / 👎.

```

### Step 1 — Create an SSH server config file

Create `ssh-config.json` on the host machine (it will be mounted read-only into the container):

```json
[
{
"name": "dev",
"host": "192.168.1.10",
"port": 22,
"username": "alice",
"privateKey": "/root/.ssh/id_rsa",
"commandWhitelist": ["^ls", "^cat ", "^df", "^free", "^ps"]
},
{
"name": "prod",
"host": "10.0.0.5",
"port": 22,
"username": "deploy",
"password": "secret",
"commandWhitelist": ["^ls", "^systemctl status"]
}
]
```

> When `privateKey` is used, the path must match the location **inside** the container. Mount your `~/.ssh` directory to `/root/.ssh` (see the `docker run` example below).

### Step 2 — Start the container

```bash
docker run -d \
--name ssh-mcp-server \
-p 3001:3001 \
-e HTTP_API_KEY=change-me-to-a-strong-secret \
-v "$HOME/.ssh:/root/.ssh:ro" \
-v "$(pwd)/ssh-config.json:/app/ssh-config.json:ro" \
-e SSH_CONFIG_FILE=/app/ssh-config.json \
ghcr.io/someblackmagic/ssh-mcp-server:latest
```

Environment variables:

| Variable | Default | Description |
|---|---|---|
| `HTTP_HOST` | `0.0.0.0` | Bind address |
| `HTTP_PORT` | `3001` | HTTP port |
| `HTTP_PATH` | `/mcp` | MCP endpoint path |
| `HTTP_API_KEY` | *(empty)* | Bearer token for auth — set this in production |
| `SSH_CONFIG_FILE` | *(empty)* | Absolute path inside the container to the JSON config file |

Check that the server is ready:

```bash
docker logs ssh-mcp-server
# MCP HTTP server listening on http://0.0.0.0:3001/mcp
```

### Step 3 — Connect an MCP client over HTTP

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
"mcpServers": {
"ssh-mcp-server": {
"url": "http://localhost:3001/mcp",
"headers": {
"Authorization": "Bearer change-me-to-a-strong-secret"
}
}
}
}
```

**Claude Code** (`.mcp.json` or `~/.claude.json`):

```json
{
"mcpServers": {
"ssh-mcp-server": {
"type": "http",
"url": "http://localhost:3001/mcp",
"headers": {
"Authorization": "Bearer change-me-to-a-strong-secret"
}
}
}
}
```

### Docker Compose example

```yaml
services:
ssh-mcp-server:
image: ghcr.io/someblackmagic/ssh-mcp-server:latest
restart: unless-stopped
ports:
- "3001:3001"
environment:
HTTP_API_KEY: change-me-to-a-strong-secret
SSH_CONFIG_FILE: /app/ssh-config.json
volumes:
- ~/.ssh:/root/.ssh:ro
- ./ssh-config.json:/app/ssh-config.json:ro
```

```bash
docker compose up -d
```

---

## 🛡️ Security Considerations

This server provides powerful capabilities to execute commands and transfer files on remote servers. To ensure it is used securely, please consider the following:
Expand Down
18 changes: 18 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh
set -e

set -- \
"--mcp-transport" "http" \
"--http-host" "${HTTP_HOST:-0.0.0.0}" \
"--http-port" "${HTTP_PORT:-3001}" \
"--http-path" "${HTTP_PATH:-/mcp}"

if [ -n "${HTTP_API_KEY:-}" ]; then
set -- "$@" "--http-api-key" "${HTTP_API_KEY}"
fi

if [ -n "${SSH_CONFIG_FILE:-}" ]; then
set -- "$@" "--config-file" "${SSH_CONFIG_FILE}"
fi

exec node build/index.js "$@"
Loading