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' go-version-file: 'go.mod'
cache: true cache: true
- name: Set up ko
uses: ko-build/setup-ko@v0.9
- name: Run all tests - name: Run all tests
run: make test-all run: make test-all

View file

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

View file

@ -41,15 +41,22 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker: ko-push:
name: Build and Push Image with ko
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: goreleaser needs: goreleaser
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Docker Buildx - name: Set up Go
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 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 - name: Log in to Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
@ -58,31 +65,23 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Get build timestamp - name: Get build timestamp and version
id: build-time id: build-info
run: echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT 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 - name: Build and push with ko
id: meta env:
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
with: VERSION: ${{ steps.build-info.outputs.version }}
images: ghcr.io/${{ github.repository }} GIT_COMMIT: ${{ github.sha }}
tags: | BUILD_TIME: ${{ steps.build-info.outputs.timestamp }}
type=semver,pattern={{version}} run: |
type=raw,value=latest # Build and push multi-platform image with version tag and latest
ko build ./cmd/registry \
- name: Build and push Docker image --bare \
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 --platform=linux/amd64,linux/arm64 \
with: --tags=${{ steps.build-info.outputs.version }},latest
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 }}

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 # Default target
help: ## Show this help message 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!" @echo "All checks passed!"
# Development targets # 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=$$(git rev-parse HEAD) \
GIT_COMMIT_SHORT=$$(git rev-parse --short HEAD) \
BUILD_TIME=$$(date -u +%Y-%m-%dT%H:%M:%SZ) \ 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 dev-down: ## Stop development environment
docker compose down docker compose down

View file

@ -32,7 +32,8 @@ Often (but not always) ideas flow through this pipeline:
#### Pre-requisites #### Pre-requisites
- **Docker** - **Docker**
- **Go 1.24.x** - **Go 1.24.x**
- **ko** - Container image builder for Go ([installation instructions](https://ko.build/install/))
- **golangci-lint v2.4.0** - **golangci-lint v2.4.0**
#### Running the server #### 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. 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`. 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. 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: services:
registry: registry:
image: modelcontextprotocol/registry:dev # Built using ko (see Makefile dev-compose target)
pull_policy: never # Image format: ko.local/<import-path>:<tag>
image: ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev
container_name: registry 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: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@ -30,6 +25,8 @@ services:
- MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=${MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION:-true} - MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=${MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION:-true}
ports: ports:
- 8080:8080 - 8080:8080
volumes:
- ./data:/data:ro
restart: "unless-stopped" restart: "unless-stopped"
postgres: postgres:

View file

@ -24,8 +24,19 @@ cleanup() {
go build -o ./bin/publisher ./cmd/publisher go build -o ./bin/publisher ./cmd/publisher
# Build the registry image with both tags, forcing a rebuild to ensure latest code # Build the registry image with ko, forcing a rebuild to ensure latest code
docker build --no-cache -t registry -t modelcontextprotocol/registry:dev . # 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 trap cleanup EXIT