Use ko in favour of docker build (#708)

<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
The following PR leverages ko instead of docker build for building the
container image.

The motivation for the move is that ko has been one of the widely
adopted ways to build Go applications, but overall its benefits are:
* Build time is much faster - around 70-80%
* The resulting image size is much smaller (also more secure) - around
50% smaller

The changes in this PR are:
* Updated the workflows for staging and release to prod to use it
* Updated the local development + the integration tests to use it (the
integration tests overwrite the base image from chainguards one to
alpine mostly because we rely on wget to check the health status and you
don't get that in the stripped down one)
 
From the user point of view everything should continue to work as it is.
 
## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

---------

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
This commit is contained in:
Radoslav Dimitrov 2025-10-29 17:16:11 +02:00 • committed by GitHub
commit b0a79c110c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 183 additions and 120 deletions

View file

@ -1 +0,0 @@
.db

View file

@ -54,6 +54,9 @@ jobs:
go-version-file: 'go.mod'
cache: true
- name: Set up ko
uses: ko-build/setup-ko@v0.9
- name: Run all tests
run: make test-all

View file

@ -14,8 +14,8 @@ env:
PULUMI_VERSION: "3.188.0"
jobs:
docker-push:
name: Build Docker Image
ko-push:
name: Build and Push Image with ko
runs-on: ubuntu-latest
permissions:
contents: read
@ -24,8 +24,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version-file: 'go.mod'
cache: true
- name: Set up ko
uses: ko-build/setup-ko@v0.9
- name: Log in to Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
@ -34,40 +40,31 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get build timestamp
id: build-time
run: echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
- name: Get build timestamp and tags
id: build-info
run: |
echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
echo "date=$(date -u +%Y%m%d)" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Extract metadata
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=main-{{date 'YYYYMMDD'}}-,enable={{is_default_branch}}
type=raw,value=main,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ steps.meta.outputs.version }}
GIT_COMMIT=${{ github.sha }}
BUILD_TIME=${{ steps.build-time.outputs.timestamp }}
- name: Build and push with ko
env:
KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
VERSION: main-${{ steps.build-info.outputs.date }}-${{ steps.build-info.outputs.short_sha }}
GIT_COMMIT: ${{ github.sha }}
BUILD_TIME: ${{ steps.build-info.outputs.timestamp }}
run: |
# Build and push multi-platform image
ko build ./cmd/registry \
--bare \
--platform=linux/amd64,linux/arm64 \
--tags=main-${{ steps.build-info.outputs.date }}-${{ steps.build-info.outputs.short_sha }},main
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
environment: staging
needs: docker-push
needs: ko-push
concurrency:
group: deploy-staging
cancel-in-progress: false

View file

@ -41,15 +41,22 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
ko-push:
name: Build and Push Image with ko
runs-on: ubuntu-latest
needs: goreleaser
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version-file: 'go.mod'
cache: true
- name: Set up ko
uses: ko-build/setup-ko@v0.9
- name: Log in to Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
@ -58,31 +65,23 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get build timestamp
id: build-time
run: echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
- name: Get build timestamp and version
id: build-info
run: |
echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
# Extract version from tag (e.g., refs/tags/v1.2.3 -> v1.2.3)
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Extract metadata
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ steps.meta.outputs.version }}
GIT_COMMIT=${{ github.sha }}
BUILD_TIME=${{ steps.build-time.outputs.timestamp }}
- name: Build and push with ko
env:
KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
VERSION: ${{ steps.build-info.outputs.version }}
GIT_COMMIT: ${{ github.sha }}
BUILD_TIME: ${{ steps.build-info.outputs.timestamp }}
run: |
# Build and push multi-platform image with version tag and latest
ko build ./cmd/registry \
--bare \
--platform=linux/amd64,linux/arm64 \
--tags=${{ steps.build-info.outputs.version }},latest

52
.ko.dockerignore Normal file
View file

@ -0,0 +1,52 @@
# Build artifacts
bin/
*.exe
*.dll
*.so
*.dylib
# Test and coverage files
coverage.out
coverage.html
*.test
# Database files
*.db
.db/
# Deployment files
deploy/
# Documentation
docs/
# Tests
tests/
# Git
.git/
.gitignore
# GitHub Actions
.github/
# Docker files (no longer used)
Dockerfile
.dockerignore
docker-compose.yml
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# Temporary files
tmp/
temp/
*.tmp
# OS files
.DS_Store
Thumbs.db

32
.ko.yaml Normal file
View file

@ -0,0 +1,32 @@
# ko configuration for MCP Registry
# Documentation: https://ko.build/configuration/
# Default base image for all builds
# Using Chainguard's static image for minimal, secure containers (~2MB)
defaultBaseImage: cgr.dev/chainguard/static:latest
# Default platforms for multi-architecture builds
defaultPlatforms:
- linux/amd64
- linux/arm64
# Build configuration
builds:
- id: registry
# Main package to build
main: ./cmd/registry
# Inject version information at build time via ldflags
ldflags:
- -s -w
- -X main.Version={{ .Env.VERSION }}
- -X main.GitCommit={{ .Env.GIT_COMMIT }}
- -X main.BuildTime={{ .Env.BUILD_TIME }}
env:
- CGO_ENABLED=0
# SBOM (Software Bill of Materials) configuration
sbom: spdx
# Base import path handling
# Set to false to preserve full import paths in image names
baseImportPaths: false

View file

@ -1,42 +0,0 @@
FROM golang:1.24-alpine AS builder
WORKDIR /app
# Copy go mod files first and download dependencies
# This creates a separate layer that only invalidates when dependencies change
COPY go.mod go.sum ./
RUN go mod download
# Copy the rest of the source code
COPY . .
ARG GO_BUILD_TAGS
ARG VERSION=dev
ARG GIT_COMMIT=unknown
ARG BUILD_TIME=unknown
RUN go build \
${GO_BUILD_TAGS:+-tags="$GO_BUILD_TAGS"} \
-ldflags="-X main.Version=${VERSION} -X main.GitCommit=${GIT_COMMIT} -X main.BuildTime=${BUILD_TIME}" \
-o /build/registry ./cmd/registry
FROM alpine:latest
WORKDIR /app
COPY --from=builder /build/registry .
COPY --from=builder /app/data/seed.json /app/data/seed.json
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
USER appuser
EXPOSE 8080
ENTRYPOINT ["./registry"]

View file

@ -1,4 +1,4 @@
.PHONY: help build test test-unit test-integration test-endpoints test-publish test-all lint lint-fix validate validate-schemas validate-examples check dev-compose clean publisher generate-schema check-schema
.PHONY: help build test test-unit test-integration test-endpoints test-publish test-all lint lint-fix validate validate-schemas validate-examples check ko-build ko-rebuild dev-compose dev-down clean publisher generate-schema check-schema
# Default target
help: ## Show this help message
@ -83,11 +83,23 @@ check: dev-down lint validate test-all ## Run all checks (lint, validate, unit t
@echo "All checks passed!"
# Development targets
dev-compose: ## Start development environment with Docker Compose (builds image automatically)
ko-build: ## Build registry image using ko (loads into local docker daemon)
@echo "Building registry with ko..."
VERSION=dev-$$(git rev-parse --short HEAD) \
GIT_COMMIT=$$(git rev-parse HEAD) \
GIT_COMMIT_SHORT=$$(git rev-parse --short HEAD) \
BUILD_TIME=$$(date -u +%Y-%m-%dT%H:%M:%SZ) \
docker compose up --build
KO_DOCKER_REPO=ko.local \
ko build --preserve-import-paths --tags=dev --sbom=none ./cmd/registry
@echo "Image built: ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev"
ko-rebuild: ## Rebuild with ko and restart registry container
@$(MAKE) ko-build
@echo "Restarting registry container..."
@docker compose restart registry
dev-compose: ko-build ## Start development environment with Docker Compose (builds with ko first)
@echo "Starting Docker Compose..."
docker compose up
dev-down: ## Stop development environment
docker compose down

View file

@ -33,6 +33,7 @@ Often (but not always) ideas flow through this pipeline:
- **Docker**
- **Go 1.24.x**
- **ko** - Container image builder for Go ([installation instructions](https://ko.build/install/))
- **golangci-lint v2.4.0**
#### Running the server
@ -44,6 +45,8 @@ make dev-compose
This starts the registry at [`localhost:8080`](http://localhost:8080) with PostgreSQL. The database uses ephemeral storage and is reset each time you restart the containers, ensuring a clean state for development and testing.
**Note:** The registry uses [ko](https://ko.build) to build container images. The `make dev-compose` command automatically builds the registry image with ko and loads it into your local Docker daemon before starting the services.
By default, the registry seeds from the production API with a filtered subset of servers (to keep startup fast). This ensures your local environment mirrors production behavior and all seed data passes validation. For offline development you can seed from a file without validation with `MCP_REGISTRY_SEED_FROM=data/seed.json MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=false make dev-compose`.
The setup can be configured with environment variables in [docker-compose.yml](./docker-compose.yml) - see [.env.example](./.env.example) for a reference.

View file

@ -1,14 +1,9 @@
services:
registry:
image: modelcontextprotocol/registry:dev
pull_policy: never
# Built using ko (see Makefile dev-compose target)
# Image format: ko.local/<import-path>:<tag>
image: ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev
container_name: registry
build:
dockerfile: Dockerfile
args:
- VERSION=dev-${GIT_COMMIT_SHORT:-unknown}
- GIT_COMMIT=${GIT_COMMIT:-unknown}
- BUILD_TIME=${BUILD_TIME:-unknown}
depends_on:
postgres:
condition: service_healthy
@ -30,6 +25,8 @@ services:
- MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=${MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION:-true}
ports:
- 8080:8080
volumes:
- ./data:/data:ro
restart: "unless-stopped"
postgres:

View file

@ -24,8 +24,19 @@ cleanup() {
go build -o ./bin/publisher ./cmd/publisher
# Build the registry image with both tags, forcing a rebuild to ensure latest code
docker build --no-cache -t registry -t modelcontextprotocol/registry:dev .
# Build the registry image with ko, forcing a rebuild to ensure latest code
# Use Alpine for integration tests because health checks need wget (production uses static image)
echo "Building registry image with ko..."
VERSION=dev-$(git rev-parse --short HEAD) \
GIT_COMMIT=$(git rev-parse HEAD) \
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
KO_DOCKER_REPO=ko.local \
KO_DEFAULTBASEIMAGE=alpine:latest \
ko build --preserve-import-paths --tags=dev --sbom=none ./cmd/registry
# Tag the ko.local image with the tags expected by docker-compose
docker tag ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev registry
docker tag ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev modelcontextprotocol/registry:dev
trap cleanup EXIT