Repository navigation
Add multi-stage Dockerfile and built-in healthcheck - #74
Conversation
The Dockerfile required a pre-built binary and the compose file referenced a nonexistent Dockerfile.dev. The health check ran `mcp serve --help` which always exits 0 regardless of server state. Replace with a multi-stage build (rust:alpine to scratch) that compiles from source and produces an 11.8MB image. Add `mcp healthcheck [url]` subcommand so scratch images can probe /health without external tools. Fixes: #65 Fixes: #66 Fixes: #73 Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR improves container usability by making the Docker image build reproducible from source and adding a real runtime health probe suitable for minimal (scratch/distroless) images.
Changes:
- Replaces the single-stage Dockerfile that required a prebuilt binary with a multi-stage build producing a minimal
scratchruntime image. - Adds a new
mcp healthcheck [url]CLI subcommand that probesGET /healthand returns an appropriate exit code. - Fixes
docker-compose.ymlto reference the correct Dockerfile and to use the new healthcheck command.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/main.rs | Adds mcp healthcheck [url] subcommand and usage entry for container health probing. |
| Dockerfile | Introduces multi-stage Rust build on Alpine and a scratch runtime stage. |
| docker-compose.yml | Switches build to Dockerfile and updates healthcheck to call mcp healthcheck. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Built-in HTTP health probe for container health checks (scratch/distroless | ||
| // images have no curl/wget). Exits 0 if the endpoint returns 2xx, 1 otherwise. | ||
| if args[0] == "healthcheck" { | ||
| let url = args | ||
| .get(1) | ||
| .map(|s| s.as_str()) | ||
| .unwrap_or("http://localhost:8080/health"); | ||
| let client = reqwest::Client::builder() |
There was a problem hiding this comment.
healthcheck runs after config::load_config()/reserved-name validation. In scratch/distroless images dirs::home_dir() can fail (no HOME or /etc/passwd), which would make mcp healthcheck exit 1 even if the HTTP server is healthy. Consider handling healthcheck (and --help) before loading config, and also add healthcheck to the reserved command-name list so server aliases can’t shadow this new subcommand silently.
| COPY --from=builder /app/target/release/mcp /usr/local/bin/mcp | ||
|
|
||
| EXPOSE 8080 | ||
| ENTRYPOINT ["mcp"] |
There was a problem hiding this comment.
For a scratch runtime, relying on ENTRYPOINT ["mcp"] can fail because PATH is typically unset. Use an absolute entrypoint (e.g. /usr/local/bin/mcp) or set ENV PATH=/usr/local/bin so both the entrypoint and docker-compose healthcheck can find the binary.
| ENTRYPOINT ["mcp"] | |
| ENTRYPOINT ["/usr/local/bin/mcp"] |
| cargo build --release && \ | ||
| rm -rf src | ||
|
|
||
| # Build the real binary | ||
| COPY src/ src/ | ||
| RUN touch src/main.rs && cargo build --release |
There was a problem hiding this comment.
The build stage uses cargo build --release without --locked. That allows dependency resolution to drift from Cargo.lock during image builds, reducing reproducibility. Use --locked (at least for the final build) to ensure the image is built from the checked-in lockfile.
| cargo build --release && \ | |
| rm -rf src | |
| # Build the real binary | |
| COPY src/ src/ | |
| RUN touch src/main.rs && cargo build --release | |
| cargo build --release --locked && \ | |
| rm -rf src | |
| # Build the real binary | |
| COPY src/ src/ | |
| RUN touch src/main.rs && cargo build --release --locked |
| # --- Build stage (Alpine = native musl, no cross-compile wrapper) --- | ||
| FROM rust:alpine AS builder | ||
|
|
||
| RUN apk add --no-cache musl-dev |
There was a problem hiding this comment.
The runtime stage copies /etc/ssl/certs/ca-certificates.crt from the builder, but the Dockerfile doesn’t explicitly install/update CA certificates in the builder. To avoid depending on base-image defaults, install ca-certificates (and run the update step if needed) in the builder so this copy is guaranteed to succeed and the cert bundle is current.
| RUN apk add --no-cache musl-dev | |
| RUN apk add --no-cache musl-dev ca-certificates && \ | |
| update-ca-certificates |
| restart: unless-stopped | ||
| healthcheck: | ||
| test: ["CMD", "mcp", "serve", "--help"] | ||
| test: ["CMD", "mcp", "healthcheck"] |
There was a problem hiding this comment.
healthcheck runs mcp via PATH. If the image moves to scratch without setting PATH (or uses an absolute ENTRYPOINT only), this healthcheck can fail with “executable not found”. Consider calling /usr/local/bin/mcp here or ensure the image sets PATH=/usr/local/bin.
| test: ["CMD", "mcp", "healthcheck"] | |
| test: ["CMD", "/usr/local/bin/mcp", "healthcheck"] |
The Dockerfile required a pre-built binary and the compose file referenced a nonexistent Dockerfile.dev. The health check ran
mcp serve --helpwhich always exits 0 regardless of server state.Replace with a multi-stage build (rust:alpine to scratch) that compiles from source and produces an 11.8MB image. Add
mcp healthcheck [url]subcommand so scratch images can probe /health without external tools.Fixes: #65
Fixes: #66
Fixes: #73