Skip to content

Commit 063f7ca

Browse files
committed
Add Dockerfile
1 parent a11c922 commit 063f7ca

File tree

10 files changed

+202
-13
lines changed

10 files changed

+202
-13
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

.github/workflows/style.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Set up Go
2525
uses: actions/setup-go@v5
2626
with:
27-
go-version: '1.24'
27+
go-version: '1.25'
2828

2929
- name: Check code formatting
3030
run: make fmt-check
@@ -33,3 +33,8 @@ jobs:
3333
uses: golangci/golangci-lint-action@v8
3434
with:
3535
version: v2.6
36+
37+
- name: Run hadolint
38+
uses: hadolint/[email protected]
39+
with:
40+
dockerfile: Dockerfile

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Set up Go
2525
uses: actions/setup-go@v5
2626
with:
27-
go-version: '1.24'
27+
go-version: '1.25'
2828

2929
- name: Download dependencies
3030
run: go mod download

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: "2"
22
run:
33
timeout: 240m
4-
go: "1.24"
4+
go: "1.25"
55
modules-download-mode: readonly
66
allow-parallel-runners: true
77
output:

Dockerfile

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

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ GOTEST=$(GOCMD) test
1414
GOFMT=$(GOCMD) fmt
1515
GOCLEAN=$(GOCMD) clean
1616

17+
# Set the container runtime command - prefer podman, fallback to docker
18+
DOCKER_CMD = $(shell command -v podman >/dev/null 2>&1 && echo podman || echo docker)
19+
1720
# Build flags
1821
LDFLAGS=-ldflags "-X github.com/stackrox/stackrox-mcp/internal/server.version=$(VERSION)"
1922

@@ -35,6 +38,14 @@ help: ## Display this help message
3538
build: ## Build the binary
3639
$(GOBUILD) $(LDFLAGS) -o $(BINARY_NAME) ./cmd/stackrox-mcp
3740

41+
.PHONY: image
42+
image: ## Build the docker image
43+
$(DOCKER_CMD) build -t quay.io/stackrox-io/stackrox-mcp:$(VERSION) .
44+
45+
.PHONY: dockerfile-lint
46+
dockerfile-lint: ## Run hadolint for Dockerfile
47+
$(DOCKER_CMD) run --rm -i docker.io/hadolint/hadolint < Dockerfile
48+
3849
.PHONY: test
3950
test: ## Run unit tests
4051
$(GOTEST) -v ./...

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+
VERSION=dev make image
183+
```
184+
185+
### Running the Container
186+
187+
Run with default settings:
188+
```bash
189+
docker run --publish 8080:8080 --env STACKROX_MCP__TOOLS__CONFIG_MANAGER__ENABLED=true --env STACKROX_MCP__CENTRAL__URL=<central host:port> quay.io/stackrox-io/stackrox-mcp:dev
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).

go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
module github.com/stackrox/stackrox-mcp
22

3-
go 1.24.0
4-
5-
toolchain go1.24.7
3+
go 1.25
64

75
require (
86
github.com/modelcontextprotocol/go-sdk v1.1.0

internal/server/server.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +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(
57-
func(*http.Request) *mcp.Server {
58-
return s.mcp
59-
},
60-
nil,
61-
)
56+
// Create a new ServeMux for routing.
57+
mux := http.NewServeMux()
58+
s.registerRouteHealth(mux)
59+
s.registerRouteDefault(mux)
6260

6361
addr := net.JoinHostPort(s.cfg.Server.Address, strconv.Itoa(s.cfg.Server.Port))
6462
httpServer := &http.Server{
6563
Addr: addr,
66-
Handler: handler,
64+
Handler: mux,
6765
ReadHeaderTimeout: readHeaderTimeout,
6866
}
6967

@@ -92,6 +90,29 @@ func (s *Server) Start(ctx context.Context) error {
9290
}
9391
}
9492

93+
func (s *Server) registerRouteHealth(mux *http.ServeMux) {
94+
mux.HandleFunc("/health", func(responseWriter http.ResponseWriter, _ *http.Request) {
95+
responseWriter.Header().Set("Content-Type", "application/json")
96+
responseWriter.WriteHeader(http.StatusOK)
97+
98+
_, err := responseWriter.Write([]byte(`{"status":"ok"}`))
99+
if err != nil {
100+
slog.Error("Failed to write health response", "error", err)
101+
}
102+
})
103+
}
104+
105+
func (s *Server) registerRouteDefault(mux *http.ServeMux) {
106+
mcpHandler := mcp.NewStreamableHTTPHandler(
107+
func(*http.Request) *mcp.Server {
108+
return s.mcp
109+
},
110+
nil,
111+
)
112+
113+
mux.Handle("/", mcpHandler)
114+
}
115+
95116
// registerTools registers all tools from the registry with the MCP server.
96117
func (s *Server) registerTools() {
97118
slog.Info("Registering MCP tools")

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)