Skip to content

Commit 486a0c6

Browse files
chris-freeman-gleanglean-code-writerclaude
authored
[feat] Add Docker support with GHCR publishing (#311)
* [feat] Add Docker support with GHCR publishing - Add Dockerfile for multi-arch Docker image support (amd64/arm64) - Create GitHub Actions workflow to publish images to GHCR on releases - Add docker-test workflow for CI validation - Simplify Docker documentation to use published GHCR images - Remove .dockerignore (no longer needed with npm install approach) Docker images will be automatically published to ghcr.io/gleanwork/local-mcp-server when releases are created, providing users with a sandboxed execution environment that abstracts over local environment differences. * Improve Docker implementation based on review feedback - Increase test sleep times from 2s to 5s for reliability on slower systems - Add tmpfs mount for .npm directory in read-only filesystem test - Add PACKAGE_VERSION build arg to Dockerfile for version pinning - Document both env block and -e flag approaches for environment variables - Add comprehensive Docker troubleshooting section to README - Address container exits, authentication errors, and connection issues * Fix tmpfs mount path to match actual user home directory The Dockerfile creates user 'mcpserver' with home at /home/mcpserver, but the test was mounting tmpfs at /home/mcp/.npm. This caused the mount to be created at an incorrect path that doesn't match the user's actual home directory. * Fix contradictory Docker flags in stdio tests Remove -d (detached) flag which conflicts with -i (interactive) for stdio testing. Detached containers don't have stdin connected, causing MCP servers to fail or hang when trying to read from unavailable stdin. New approach: - Run container in foreground with -i flag and stdin from /dev/null - Use timeout to prevent hanging - Run in background with & to allow process checking - Verify process is running after startup - Properly cleanup with kill and wait This properly tests that the server can start and accept stdio communication. * Fix process monitoring logic in stdio tests The previous approach had critical flaws: - $! captured the timeout command PID, not docker - Killing timeout left docker running uncontrolled in background - Process checks verified wrong PID - No actual verification that stdio communication worked New approach: - Pipe actual MCP initialize message to container via stdin - Use 'timeout' as prefix (not wrapper) so docker is the background process - Capture docker's PID directly with $! - Use 'kill -0' to check if process is still running (non-destructive) - Capture output to verify MCP server responds - Check for JSON-RPC response to confirm stdio communication works - Proper cleanup with kill and wait - Show output on failure for debugging This properly tests that: 1. Container starts successfully 2. Accepts stdin input 3. MCP server can process requests 4. Works with security constraints * fix: Install npm package as root before switching to non-root user The Docker build was failing because npm install -g was being run as the mcpserver user, which doesn't have permission to write to global npm directories. This change installs the package as root first, then switches to the non-root user only for running the server. This maintains security best practices while fixing the EACCES errors during the Docker build process. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Complete Docker support with compose example and CI enhancements Add production-ready Docker deployment files and enhance CI workflows: - Add .dockerignore for optimized build context - Add docker-compose.yaml example in examples/ directory with: - Environment variable configuration - Resource limits and security options - Comprehensive inline documentation - Enhance publish-docker.yml workflow: - Add triggers for main branch and version tags - Add id-token write permission for provenance - Improve metadata with OCI labels - Add branch and SHA tags for better traceability - Update README with docker-compose reference These additions provide users with production-grade deployment options and improved CI/CD automation for multi-arch Docker images. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Add required MCP protocol fields to Docker test initialize requests * fix: Keep stdin open in Docker tests to prevent premature server exit * fix: Add tmpfs mount for .local directory in read-only mode The MCP server logger needs write access to ~/.local/state/glean for log files. When running with --read-only, this directory must be mounted as tmpfs to allow the logger to create its state directory. * fix: Set uid for tmpfs mounts to match non-root mcpserver user Tmpfs mounts are owned by root by default. Since the container runs as the non-root mcpserver user (uid 1001), we need to specify uid=1001 for the .npm and .local tmpfs mounts so the user can write to them. * chore: Update actions/checkout to v5 in publish-docker.yml workflow This change upgrades the checkout action version to ensure compatibility and access to the latest features and improvements. --------- Co-authored-by: Glean Code Writer <code_writer@glean.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent d4507ee commit 486a0c6

7 files changed

Lines changed: 388 additions & 0 deletions

File tree

.dockerignore

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Dependencies
2+
node_modules/
3+
pnpm-lock.yaml
4+
package-lock.json
5+
yarn.lock
6+
7+
# Build outputs
8+
build/
9+
dist/
10+
*.tsbuildinfo
11+
12+
# Tests and coverage
13+
coverage/
14+
test/
15+
tests/
16+
**/*.test.ts
17+
**/*.test.js
18+
**/*.spec.ts
19+
**/*.spec.js
20+
21+
# Development files
22+
.git/
23+
.github/
24+
.vscode/
25+
.idea/
26+
*.log
27+
*.swp
28+
*.swo
29+
*~
30+
31+
# Environment and secrets
32+
.env
33+
.env.*
34+
!.env.example
35+
36+
# Documentation
37+
docs/
38+
*.md
39+
!README.md
40+
41+
# CI/CD
42+
.circleci/
43+
.gitlab-ci.yml
44+
.travis.yml
45+
46+
# Misc
47+
.DS_Store
48+
.editorconfig
49+
.eslintrc*
50+
.prettierrc*
51+
.gitignore
52+
.dockerignore
53+
Dockerfile*
54+
docker-compose*.yml

.github/workflows/docker-test.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Docker Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
name: Build and Test Docker Image
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
16+
- name: Build Docker image
17+
run: docker build -t glean-mcp-server:test .
18+
19+
- name: Test image structure
20+
run: |
21+
docker run --rm glean-mcp-server:test node --version
22+
docker run --rm glean-mcp-server:test which npx
23+
24+
- name: Verify MCP server starts with stdio
25+
run: |
26+
(echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'; sleep 3) | docker run --rm -i glean-mcp-server:test > /tmp/output.txt 2>&1 &
27+
docker_pid=$!
28+
sleep 2
29+
if kill -0 $docker_pid 2>/dev/null; then
30+
echo "Container is running and accepting stdio"
31+
kill $docker_pid 2>/dev/null || true
32+
wait $docker_pid 2>/dev/null || true
33+
else
34+
echo "Container exited immediately"
35+
cat /tmp/output.txt 2>/dev/null || true
36+
exit 1
37+
fi
38+
if grep -q '"result"' /tmp/output.txt 2>/dev/null; then
39+
echo "MCP server responded successfully to initialize request"
40+
else
41+
echo "MCP server output:"
42+
cat /tmp/output.txt
43+
exit 1
44+
fi
45+
46+
- name: Test with security constraints
47+
run: |
48+
(echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'; sleep 3) | docker run --rm -i \
49+
--cpus="1.0" \
50+
--memory="2g" \
51+
--read-only \
52+
--tmpfs /tmp \
53+
--tmpfs /home/mcpserver/.npm:uid=1001 \
54+
--tmpfs /home/mcpserver/.local:uid=1001 \
55+
--cap-drop=ALL \
56+
--security-opt=no-new-privileges:true \
57+
glean-mcp-server:test > /tmp/output-secure.txt 2>&1 &
58+
docker_pid=$!
59+
sleep 2
60+
if kill -0 $docker_pid 2>/dev/null; then
61+
echo "Container is running with security constraints"
62+
kill $docker_pid 2>/dev/null || true
63+
wait $docker_pid 2>/dev/null || true
64+
else
65+
echo "Container exited immediately with security constraints"
66+
cat /tmp/output-secure.txt 2>/dev/null || true
67+
exit 1
68+
fi
69+
if grep -q '"result"' /tmp/output-secure.txt 2>/dev/null; then
70+
echo "MCP server responded successfully with security constraints"
71+
else
72+
echo "MCP server output:"
73+
cat /tmp/output-secure.txt
74+
exit 1
75+
fi
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Publish Docker Image
2+
3+
on:
4+
release:
5+
types: [published]
6+
push:
7+
branches:
8+
- main
9+
tags:
10+
- 'v*.*.*'
11+
workflow_dispatch:
12+
13+
jobs:
14+
publish:
15+
name: Build and Publish Multi-Arch Docker Image
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
packages: write
20+
id-token: write
21+
steps:
22+
- name: Checkout repository
23+
uses: actions/checkout@v5
24+
25+
- name: Set up QEMU
26+
uses: docker/setup-qemu-action@v3
27+
28+
- name: Set up Docker Buildx
29+
uses: docker/setup-buildx-action@v3
30+
31+
- name: Log in to GitHub Container Registry
32+
uses: docker/login-action@v3
33+
with:
34+
registry: ghcr.io
35+
username: ${{ github.actor }}
36+
password: ${{ secrets.GITHUB_TOKEN }}
37+
38+
- name: Extract metadata
39+
id: meta
40+
uses: docker/metadata-action@v5
41+
with:
42+
images: ghcr.io/gleanwork/local-mcp-server
43+
tags: |
44+
type=semver,pattern={{version}}
45+
type=semver,pattern={{major}}.{{minor}}
46+
type=ref,event=branch
47+
type=sha,prefix={{branch}}-
48+
type=raw,value=latest,enable={{is_default_branch}}
49+
labels: |
50+
org.opencontainers.image.title=Glean MCP Server
51+
org.opencontainers.image.description=Model Context Protocol server for Glean API integration
52+
org.opencontainers.image.vendor=Glean
53+
54+
- name: Build and push multi-architecture image
55+
uses: docker/build-push-action@v5
56+
with:
57+
context: .
58+
platforms: linux/amd64,linux/arm64
59+
push: true
60+
tags: ${{ steps.meta.outputs.tags }}
61+
labels: ${{ steps.meta.outputs.labels }}
62+
cache-from: type=gha
63+
cache-to: type=gha,mode=max

Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Use Alpine-based Node.js 20 for minimal image size and security
2+
FROM node:20-alpine
3+
4+
# Install the published @gleanwork/local-mcp-server package from npm
5+
# PACKAGE_VERSION can be set at build time to pin to a specific version
6+
ARG PACKAGE_VERSION=latest
7+
RUN npm install -g @gleanwork/local-mcp-server@${PACKAGE_VERSION}
8+
9+
# Create non-root user for security best practices
10+
# uid/gid 1001 is a common convention for application users
11+
RUN addgroup -g 1001 -S nodejs && \
12+
adduser -S mcpserver -u 1001
13+
14+
# Switch to non-root user for running the server
15+
USER mcpserver
16+
17+
# Run the MCP server
18+
# The server uses stdio transport and keeps running until terminated
19+
CMD ["local-mcp-server"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This monorepo contains packages for Glean's local MCP server. For more details s
1010
- [@gleanwork/configure-mcp-server](https://github.com/gleanwork/configure-mcp-server) for configuring the local MCP server with popular MCP clients.
1111
- [@gleanwork/local-mcp-server](https://github.com/gleanwork/mcp-server/tree/main/packages/local-mcp-server) on running the local MCP server.
1212

13+
The local MCP server can be run via npx or Docker. See the [@gleanwork/local-mcp-server README](https://github.com/gleanwork/mcp-server/tree/main/packages/local-mcp-server#docker-deployment) for Docker deployment instructions.
14+
1315
## Contributing
1416

1517
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.

examples/docker-compose.yaml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
version: '3.8'
2+
3+
services:
4+
glean-mcp-server:
5+
# Use the latest published image from GitHub Container Registry
6+
# For specific versions, use: ghcr.io/gleanwork/local-mcp-server:v0.8.0
7+
image: ghcr.io/gleanwork/local-mcp-server:latest
8+
9+
# Container name for easy reference
10+
container_name: glean-mcp-server
11+
12+
# Required for stdio transport communication
13+
stdin_open: true
14+
tty: false
15+
16+
# Environment variables for Glean API configuration
17+
environment:
18+
# REQUIRED: Your Glean instance name (e.g., "acme-corp")
19+
GLEAN_INSTANCE: ${GLEAN_INSTANCE}
20+
21+
# REQUIRED: Your Glean API token
22+
GLEAN_API_TOKEN: ${GLEAN_API_TOKEN}
23+
24+
# OPTIONAL: Set to "production" for optimized performance
25+
NODE_ENV: production
26+
27+
# Resource limits (optional but recommended)
28+
deploy:
29+
resources:
30+
limits:
31+
cpus: '1.0'
32+
memory: 2G
33+
reservations:
34+
cpus: '0.5'
35+
memory: 512M
36+
37+
# Security options (optional but recommended)
38+
security_opt:
39+
- no-new-privileges:true
40+
41+
# Read-only root filesystem for enhanced security (optional)
42+
# Uncomment if your use case allows it
43+
# read_only: true
44+
# tmpfs:
45+
# - /tmp
46+
# - /home/mcpserver/.npm:uid=1001
47+
# - /home/mcpserver/.local:uid=1001
48+
49+
# Drop all capabilities for enhanced security (optional)
50+
cap_drop:
51+
- ALL
52+
53+
# Restart policy
54+
restart: unless-stopped
55+
56+
# Logging configuration
57+
logging:
58+
driver: "json-file"
59+
options:
60+
max-size: "10m"
61+
max-file: "3"
62+
63+
# To use this docker-compose file:
64+
#
65+
# 1. Create a .env file in the same directory with your credentials:
66+
# GLEAN_INSTANCE=your-instance-name
67+
# GLEAN_API_TOKEN=your-api-token
68+
#
69+
# 2. Start the service:
70+
# docker-compose up -d
71+
#
72+
# 3. View logs:
73+
# docker-compose logs -f
74+
#
75+
# 4. Stop the service:
76+
# docker-compose down
77+
#
78+
# Note: For MCP client integration, you'll typically use the direct
79+
# docker run command instead of docker-compose, as MCP clients manage
80+
# the container lifecycle. This compose file is provided as a reference
81+
# for standalone deployment scenarios.

0 commit comments

Comments
 (0)