Skip to content

Commit 9e02964

Browse files
authored
Add Docker support with GHCR automated publishing (#3)
1 parent 9fae4b2 commit 9e02964

6 files changed

Lines changed: 431 additions & 0 deletions

File tree

.dockerignore

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Git and version control
2+
.git
3+
.gitignore
4+
.gitattributes
5+
6+
# Python cache and build artifacts
7+
__pycache__
8+
*.py[cod]
9+
*$py.class
10+
*.so
11+
.Python
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# Virtual environments
30+
venv/
31+
ENV/
32+
env/
33+
.venv
34+
35+
# IDE and editor files
36+
.vscode/
37+
.idea/
38+
*.swp
39+
*.swo
40+
*~
41+
.DS_Store
42+
43+
# Testing and coverage
44+
.pytest_cache/
45+
.coverage
46+
.coverage.*
47+
htmlcov/
48+
.tox/
49+
.nox/
50+
coverage.xml
51+
*.cover
52+
53+
# Documentation
54+
docs/
55+
*.md
56+
!README.md
57+
58+
# CI/CD
59+
.github/
60+
.gitlab-ci.yml
61+
.travis.yml
62+
63+
# Development and testing data
64+
tests/
65+
test/
66+
data/
67+
*.parquet
68+
*.csv
69+
70+
# Environment files (should be provided at runtime)
71+
.env
72+
.env.*
73+
74+
# Logs
75+
*.log
76+
77+
# OS files
78+
Thumbs.db
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: Build and Publish Docker Image
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- 'v*.*.*'
9+
pull_request:
10+
branches:
11+
- main
12+
workflow_dispatch:
13+
14+
env:
15+
REGISTRY: ghcr.io
16+
IMAGE_NAME: ${{ github.repository }}
17+
18+
jobs:
19+
build-and-push:
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
packages: write
24+
attestations: write
25+
id-token: write
26+
27+
steps:
28+
- name: Checkout repository
29+
uses: actions/checkout@v4
30+
31+
- name: Set up Docker Buildx
32+
uses: docker/setup-buildx-action@v3
33+
34+
- name: Log in to GitHub Container Registry
35+
if: github.event_name != 'pull_request'
36+
uses: docker/login-action@v3
37+
with:
38+
registry: ${{ env.REGISTRY }}
39+
username: ${{ github.actor }}
40+
password: ${{ secrets.GITHUB_TOKEN }}
41+
42+
- name: Extract metadata (tags, labels) for Docker
43+
id: meta
44+
uses: docker/metadata-action@v5
45+
with:
46+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
47+
tags: |
48+
# Set latest tag for main branch
49+
type=raw,value=latest,enable={{is_default_branch}}
50+
# Tag with version for releases (v1.0.0 -> 1.0.0)
51+
type=semver,pattern={{version}}
52+
# Tag with major.minor for releases (v1.0.0 -> 1.0)
53+
type=semver,pattern={{major}}.{{minor}}
54+
# Tag with major for releases (v1.0.0 -> 1)
55+
type=semver,pattern={{major}}
56+
# Tag with PR number for PRs
57+
type=ref,event=pr
58+
# Tag with branch name (sanitized)
59+
type=ref,event=branch
60+
# Tag with short SHA
61+
type=sha
62+
63+
- name: Build and push Docker image
64+
id: push
65+
uses: docker/build-push-action@v6
66+
with:
67+
context: .
68+
platforms: linux/amd64,linux/arm64
69+
push: ${{ github.event_name != 'pull_request' }}
70+
tags: ${{ steps.meta.outputs.tags }}
71+
labels: ${{ steps.meta.outputs.labels }}
72+
cache-from: type=gha
73+
cache-to: type=gha,mode=max
74+
75+
- name: Generate artifact attestation
76+
if: github.event_name != 'pull_request'
77+
uses: actions/attest-build-provenance@v1
78+
with:
79+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
80+
subject-digest: ${{ steps.push.outputs.digest }}
81+
push-to-registry: true
82+
83+
- name: Output image details
84+
if: github.event_name != 'pull_request'
85+
run: |
86+
echo "Image published:"
87+
echo "Registry: ${{ env.REGISTRY }}"
88+
echo "Repository: ${{ env.IMAGE_NAME }}"
89+
echo "Tags:"
90+
echo "${{ steps.meta.outputs.tags }}"
91+
echo ""
92+
echo "Pull with:"
93+
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"

Dockerfile

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Multi-stage build for FOCUS MCP Server
2+
# Stage 1: Build environment with dependencies
3+
# Use official uv image with Python pre-installed
4+
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim AS builder
5+
6+
# Set working directory
7+
WORKDIR /app
8+
9+
# Copy dependency files first for better caching
10+
COPY pyproject.toml uv.lock ./
11+
12+
# Install dependencies in a virtual environment
13+
# This creates a clean, isolated Python environment
14+
RUN uv sync --frozen --no-dev
15+
16+
# Stage 2: Runtime environment
17+
# Use minimal Python image for runtime
18+
FROM python:3.11-slim-bookworm
19+
20+
# Install runtime dependencies for DuckDB
21+
# These are required for DuckDB's C++ components
22+
RUN apt-get update && \
23+
apt-get install -y --no-install-recommends \
24+
ca-certificates \
25+
&& rm -rf /var/lib/apt/lists/*
26+
27+
# Set working directory
28+
WORKDIR /app
29+
30+
# Copy the virtual environment from builder
31+
COPY --from=builder /app/.venv /app/.venv
32+
33+
# Copy application source code
34+
COPY *.py ./
35+
COPY LICENSE ./
36+
COPY resources/ ./resources/
37+
38+
# Set environment variables
39+
ENV PATH="/app/.venv/bin:$PATH" \
40+
PYTHONUNBUFFERED=1 \
41+
FOCUS_DATA_LOCATION="/data" \
42+
FOCUS_VERSION="1.0"
43+
44+
# Create data directory for mounting
45+
RUN mkdir -p /data
46+
47+
# Health check (optional - can be used by orchestrators)
48+
# This verifies the server can start and respond
49+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
50+
CMD python -c "import focus_mcp_server; print('OK')" || exit 1
51+
52+
# Expose stdio for MCP communication
53+
# MCP servers typically communicate via stdio, not HTTP ports
54+
# Run as non-root user for security
55+
RUN useradd -m -u 1000 mcp && \
56+
chown -R mcp:mcp /app /data
57+
USER mcp
58+
59+
# Set the entry point to the MCP server
60+
# Users can override environment variables at runtime
61+
ENTRYPOINT ["python", "focus_mcp_server.py"]

README.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,144 @@ The AWS credential chain automatically finds credentials from:
223223

224224
Note: AWS_PROFILE is a standard AWS environment variable that the credential chain respects.
225225

226+
## Docker Usage
227+
228+
### Using Pre-built Images
229+
230+
The server is available as a Docker image on GitHub Container Registry:
231+
232+
```bash
233+
# Pull the latest image
234+
docker pull ghcr.io/glassity/focus-mcp:latest
235+
236+
# Or use a specific version
237+
docker pull ghcr.io/glassity/focus-mcp:v0.1.1
238+
```
239+
240+
### Running with Docker
241+
242+
#### Local FOCUS Data
243+
244+
```bash
245+
# Run with local data mounted
246+
docker run -i --rm \
247+
-v "/path/to/your/focus/data:/data:ro" \
248+
-e FOCUS_DATA_LOCATION=/data \
249+
-e FOCUS_VERSION=1.0 \
250+
ghcr.io/glassity/focus-mcp:latest
251+
```
252+
253+
#### S3 FOCUS Data
254+
255+
**Using AWS credentials from environment:**
256+
257+
```bash
258+
docker run -i --rm \
259+
-e FOCUS_DATA_LOCATION="s3://your-bucket/focus-exports" \
260+
-e AWS_REGION="us-west-2" \
261+
-e AWS_ACCESS_KEY_ID="your-access-key" \
262+
-e AWS_SECRET_ACCESS_KEY="your-secret-key" \
263+
ghcr.io/glassity/focus-mcp:latest
264+
```
265+
266+
**Using AWS profile (recommended for multiple profiles):**
267+
268+
```bash
269+
docker run -i --rm \
270+
-v "$HOME/.aws:/home/mcp/.aws:ro" \
271+
-e FOCUS_DATA_LOCATION="s3://your-bucket/focus-exports" \
272+
-e AWS_REGION="us-west-2" \
273+
-e AWS_PROFILE="billing-reader" \
274+
ghcr.io/glassity/focus-mcp:latest
275+
```
276+
277+
### Using with Claude Desktop
278+
279+
Configure Claude Desktop to use the Docker image in `claude_desktop_config.json`:
280+
281+
```json
282+
{
283+
"mcpServers": {
284+
"focus": {
285+
"command": "docker",
286+
"args": [
287+
"run",
288+
"-i",
289+
"--rm",
290+
"-v",
291+
"/path/to/your/focus/data:/data:ro",
292+
"-e",
293+
"FOCUS_DATA_LOCATION=/data",
294+
"-e",
295+
"FOCUS_VERSION=1.0",
296+
"ghcr.io/glassity/focus-mcp:latest"
297+
]
298+
}
299+
}
300+
}
301+
```
302+
303+
**For S3 data with environment variables:**
304+
305+
```json
306+
{
307+
"mcpServers": {
308+
"focus": {
309+
"command": "docker",
310+
"args": [
311+
"run",
312+
"-i",
313+
"--rm",
314+
"-e", "FOCUS_DATA_LOCATION=s3://your-bucket/focus-exports",
315+
"-e", "AWS_REGION=us-west-2",
316+
"-e", "AWS_ACCESS_KEY_ID=your-access-key",
317+
"-e", "AWS_SECRET_ACCESS_KEY=your-secret-key",
318+
"ghcr.io/glassity/focus-mcp:latest"
319+
]
320+
}
321+
}
322+
}
323+
```
324+
325+
**For S3 data with AWS profile:**
326+
327+
```json
328+
{
329+
"mcpServers": {
330+
"focus": {
331+
"command": "docker",
332+
"args": [
333+
"run",
334+
"-i",
335+
"--rm",
336+
"-v", "/Users/YOUR_USERNAME/.aws:/home/mcp/.aws:ro",
337+
"-e", "FOCUS_DATA_LOCATION=s3://your-bucket/focus-exports",
338+
"-e", "AWS_REGION=us-west-2",
339+
"-e", "AWS_PROFILE=billing-reader",
340+
"ghcr.io/glassity/focus-mcp:latest"
341+
]
342+
}
343+
}
344+
}
345+
```
346+
347+
### Building Your Own Image
348+
349+
```bash
350+
# Clone the repository
351+
git clone https://github.com/glassity/focus-mcp.git
352+
cd focus-mcp
353+
354+
# Build the image
355+
docker build -t focus-mcp:custom .
356+
357+
# Run your custom image
358+
docker run -i --rm \
359+
-v "/path/to/your/focus/data:/data:ro" \
360+
-e FOCUS_DATA_LOCATION=/data \
361+
focus-mcp:custom
362+
```
363+
226364
## Development
227365

228366
```bash

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ dependencies = [
77
"duckdb>=1.3.2",
88
"mcp[cli]>=1.13.1",
99
"pandas>=2.3.2",
10+
"pyyaml>=6.0.0",
11+
"packaging>=24.0",
1012
]
1113

1214
[project.scripts]

0 commit comments

Comments
 (0)