Skip to content

Commit c804787

Browse files
committed
Add Dockerfile
1 parent a11c922 commit c804787

File tree

5 files changed

+169
-1
lines changed

5 files changed

+169
-1
lines changed

.dockerignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Git and jj files
2+
.git/
3+
.jj/
4+
.gitignore
5+
.gitattributes
6+
7+
# CI/CD
8+
.github/
9+
10+
# IDE and editor files
11+
.vscode/
12+
.idea/
13+
.DS_Store
14+
15+
# Test coverage output
16+
/*.out
17+
18+
# Build output
19+
/stackrox-mcp
20+
21+
# Lint output
22+
/report.xml
23+
24+
# Documentation
25+
*.md
26+
docs/
27+
LICENSE

Dockerfile

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Multi-stage Dockerfile for StackRox MCP Server
2+
# Stage 1: Builder - Build the Go binary
3+
FROM registry.access.redhat.com/ubi10/ubi:latest AS builder
4+
5+
# Build arguments for multi-arch support
6+
ARG TARGETOS=linux
7+
ARG TARGETARCH=amd64
8+
ARG VERSION=dev
9+
10+
# Install Go 1.24
11+
RUN dnf install -y golang && dnf clean all
12+
13+
# Set working directory
14+
WORKDIR /workspace
15+
16+
# Copy go module files first for better layer caching
17+
COPY go.mod go.sum ./
18+
19+
# Download dependencies (cached layer)
20+
RUN go mod download
21+
22+
# Copy source code
23+
COPY . .
24+
25+
# Build the binary with optimizations
26+
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
27+
go build \
28+
-ldflags="-w -s" \
29+
-trimpath \
30+
-o stackrox-mcp \
31+
./cmd/stackrox-mcp
32+
33+
# Stage 2: Runtime - Minimal runtime image
34+
FROM registry.access.redhat.com/ubi10/ubi-micro:latest
35+
36+
# Set default environment variables
37+
ENV LOG_LEVEL=INFO
38+
39+
# Set working directory
40+
WORKDIR /app
41+
42+
# Copy binary from builder
43+
COPY --from=builder /workspace/stackrox-mcp /app/stackrox-mcp
44+
45+
# Set ownership to non-root user
46+
RUN chown -R 4000:4000 /app
47+
48+
# Switch to non-root user
49+
USER 4000
50+
51+
# Expose port for MCP server
52+
EXPOSE 8080
53+
54+
# Run the application
55+
ENTRYPOINT ["/app/stackrox-mcp"]

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,34 @@ You: "Can you list all the clusters from StackRox?"
173173
Claude: [Uses list_clusters tool to retrieve cluster information]
174174
```
175175

176+
## Docker
177+
178+
### Building the Docker Image
179+
180+
Build the image locally:
181+
```bash
182+
docker build -t stackrox-mcp:test .
183+
```
184+
185+
### Running the Container
186+
187+
Run with default settings:
188+
```bash
189+
docker run -p 8080:8080 stackrox-mcp:test
190+
```
191+
192+
### Build Arguments
193+
194+
- `TARGETOS` - Target operating system (default: `linux`)
195+
- `TARGETARCH` - Target architecture (default: `amd64`)
196+
- `VERSION` - Application version (default: `dev`)
197+
198+
### Image Details
199+
200+
- **Base Image**: Red Hat UBI10-micro (minimal, secure)
201+
- **User**: Non-root user `mcp` (UID/GID 4000)
202+
- **Port**: 8080
203+
176204
## Development
177205

178206
For detailed development guidelines, testing standards, and contribution workflows, see [CONTRIBUTING.md](.github/CONTRIBUTING.md).

internal/server/server.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ func NewServer(cfg *config.Config, registry *toolsets.Registry) *Server {
5353
func (s *Server) Start(ctx context.Context) error {
5454
s.registerTools()
5555

56-
handler := mcp.NewStreamableHTTPHandler(
56+
mcpHandler := mcp.NewStreamableHTTPHandler(
5757
func(*http.Request) *mcp.Server {
5858
return s.mcp
5959
},
6060
nil,
6161
)
6262

63+
handler := s.withHealthEndpoint(mcpHandler)
64+
6365
addr := net.JoinHostPort(s.cfg.Server.Address, strconv.Itoa(s.cfg.Server.Port))
6466
httpServer := &http.Server{
6567
Addr: addr,
@@ -118,3 +120,18 @@ func (s *Server) registerTools() {
118120

119121
slog.Info("Tools registration complete")
120122
}
123+
124+
// withHealthEndpoint wraps an HTTP handler with a health check endpoint.
125+
func (s *Server) withHealthEndpoint(next http.Handler) http.Handler {
126+
return http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
127+
if request.URL.Path == "/health" {
128+
responseWriter.Header().Set("Content-Type", "application/json")
129+
responseWriter.WriteHeader(http.StatusOK)
130+
_, _ = responseWriter.Write([]byte(`{"status":"ok"}`))
131+
132+
return
133+
}
134+
135+
next.ServeHTTP(responseWriter, request)
136+
})
137+
}

internal/server/server_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,44 @@ func TestServer_Start(t *testing.T) {
166166
t.Fatal("Server did not shut down within timeout period")
167167
}
168168
}
169+
170+
func TestServer_HealthEndpoint(t *testing.T) {
171+
cfg := getDefaultConfig()
172+
cfg.Server.Port = testutil.GetPortForTest(t)
173+
174+
registry := toolsets.NewRegistry(cfg, []toolsets.Toolset{})
175+
srv := NewServer(cfg, registry)
176+
177+
ctx, cancel := context.WithCancel(context.Background())
178+
defer cancel()
179+
180+
errChan := make(chan error, 1)
181+
182+
go func() {
183+
errChan <- srv.Start(ctx)
184+
}()
185+
186+
serverURL := "http://" + net.JoinHostPort(cfg.Server.Address, strconv.Itoa(cfg.Server.Port))
187+
err := testutil.WaitForServerReady(serverURL, 3*time.Second)
188+
require.NoError(t, err, "Server should start within timeout")
189+
190+
// Test health endpoint.
191+
//nolint:noctx
192+
resp, err := http.Get(serverURL + "/health")
193+
require.NoError(t, err, "Health endpoint should be reachable")
194+
require.NoError(t, resp.Body.Close())
195+
196+
assert.Equal(t, http.StatusOK, resp.StatusCode, "Health endpoint should return 200 OK")
197+
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"), "Health endpoint should return JSON")
198+
199+
// Trigger shutdown.
200+
cancel()
201+
202+
// Wait for server to shut down.
203+
select {
204+
case <-errChan:
205+
// Server shut down successfully
206+
case <-time.After(ShutdownTimeout + time.Second):
207+
t.Fatal("Server did not shut down within timeout period")
208+
}
209+
}

0 commit comments

Comments
 (0)