Go Building and Deployment¶
Building Go MCP Servers for Production¶
This guide covers building, optimizing, and deploying Go MCP servers with proper cross-compilation, optimization, and containerization.
Build Configuration¶
Basic Build Commands¶
# Development build
go build -o mcp-server ./cmd/server
# Production build with optimizations
go build -ldflags "-w -s" -trimpath -o mcp-server ./cmd/server
# Build with version information
VERSION=$(git describe --tags --always --dirty)
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
go build -ldflags "-w -s -X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME" -trimpath -o mcp-server ./cmd/server
Advanced Build Configuration¶
// cmd/server/version.go
package main
import (
"fmt"
"runtime"
)
var (
Version = "dev"
BuildTime = "unknown"
GitCommit = "unknown"
)
func printVersion() {
fmt.Printf("MCP Server %s\n", Version)
fmt.Printf("Build time: %s\n", BuildTime)
fmt.Printf("Git commit: %s\n", GitCommit)
fmt.Printf("Go version: %s\n", runtime.Version())
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
Cross-Platform Compilation¶
Multi-Platform Makefile¶
# Build configuration
BINARY_NAME=mcp-server
MAIN_PATH=./cmd/server
VERSION ?= $(shell git describe --tags --always --dirty)
BUILD_TIME ?= $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
GIT_COMMIT ?= $(shell git rev-parse HEAD)
# Build flags
LDFLAGS=-ldflags "-w -s -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)"
BUILD_FLAGS=-trimpath $(LDFLAGS)
# Supported platforms
PLATFORMS := windows/amd64 linux/amd64 linux/arm64 darwin/amd64 darwin/arm64
# Default build
build:
go build $(BUILD_FLAGS) -o $(BINARY_NAME) $(MAIN_PATH)
# Cross-compile for all platforms
build-all: $(PLATFORMS)
$(PLATFORMS):
$(eval GOOS := $(word 1,$(subst /, ,$@)))
$(eval GOARCH := $(word 2,$(subst /, ,$@)))
$(eval BINARY := $(BINARY_NAME)-$(GOOS)-$(GOARCH))
$(eval BINARY := $(if $(filter windows,$(GOOS)),$(BINARY).exe,$(BINARY)))
@echo "Building $(BINARY)..."
GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(BUILD_FLAGS) -o dist/$(BINARY) $(MAIN_PATH)
# Build for current platform with race detection (development)
build-dev:
go build -race -o $(BINARY_NAME)-dev $(MAIN_PATH)
# Static binary for containers
build-static:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(BUILD_FLAGS) -a -installsuffix cgo -o $(BINARY_NAME)-static $(MAIN_PATH)
# Clean build artifacts
clean:
rm -f $(BINARY_NAME)*
rm -rf dist/
.PHONY: build build-all build-dev build-static clean $(PLATFORMS)
GitHub Actions Build Pipeline¶
# .github/workflows/build.yml
name: Build and Release
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run tests
run: |
go test -race -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
- name: Upload coverage
uses: codecov/codecov-action@v3
build:
needs: test
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, windows, darwin]
goarch: [amd64, arm64]
exclude:
- goos: windows
goarch: arm64
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Build binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
VERSION=$(git describe --tags --always --dirty)
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
GIT_COMMIT=$(git rev-parse HEAD)
BINARY_NAME=mcp-server-${{ matrix.goos }}-${{ matrix.goarch }}
if [ "${{ matrix.goos }}" = "windows" ]; then
BINARY_NAME="${BINARY_NAME}.exe"
fi
CGO_ENABLED=0 go build \
-ldflags "-w -s -X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME -X main.GitCommit=$GIT_COMMIT" \
-trimpath \
-o $BINARY_NAME \
./cmd/server
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: binaries
path: mcp-server-*
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: binaries
- name: Create release
uses: softprops/action-gh-release@v1
with:
files: mcp-server-*
generate_release_notes: true
Container Building¶
Multi-Stage Dockerfile¶
# Build stage
FROM golang:1.21-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
# Set working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build arguments
ARG VERSION=dev
ARG BUILD_TIME
ARG GIT_COMMIT
# Build the binary
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-w -s -X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME -X main.GitCommit=$GIT_COMMIT" \
-trimpath \
-o mcp-server \
./cmd/server
# Final stage
FROM scratch
# Copy CA certificates
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copy the binary
COPY --from=builder /app/mcp-server /mcp-server
# Create non-root user (numeric for scratch image)
USER 10001:10001
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/mcp-server", "health"]
# Run the binary
ENTRYPOINT ["/mcp-server"]
Optimized Dockerfile with UPX¶
# Build stage with UPX compression
FROM golang:1.21-alpine AS builder
# Install dependencies including UPX
RUN apk add --no-cache git ca-certificates tzdata upx
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG VERSION=dev
ARG BUILD_TIME
ARG GIT_COMMIT
# Build with static linking
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-w -s -X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME -X main.GitCommit=$GIT_COMMIT" \
-trimpath \
-a -installsuffix cgo \
-o mcp-server \
./cmd/server
# Compress binary with UPX
RUN upx --best --lzma mcp-server
# Verify the compressed binary works
RUN ./mcp-server version
# Final stage
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/mcp-server /mcp-server
USER 10001:10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/mcp-server", "health"]
ENTRYPOINT ["/mcp-server"]
Docker Compose for Development¶
# docker-compose.yml
version: '3.8'
services:
mcp-server:
build:
context: .
dockerfile: Dockerfile.dev
args:
VERSION: dev
ports:
- "8000:8000"
environment:
- MCP_LOG_LEVEL=debug
- MCP_TRANSPORT=http
- MCP_HTTP_ADDR=:8000
volumes:
- .:/app
- go-mod-cache:/go/pkg/mod
depends_on:
- postgres
- redis
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: mcp_dev
POSTGRES_USER: mcp
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
volumes:
go-mod-cache:
postgres_data:
redis_data:
Development Dockerfile¶
# Dockerfile.dev - for development with hot reload
FROM golang:1.21-alpine
RUN apk add --no-cache git ca-certificates tzdata
# Install air for hot reloading
RUN go install github.com/cosmtrek/air@latest
WORKDIR /app
# Copy go mod files and download deps
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Expose port
EXPOSE 8000
# Command for development
CMD ["air", "-c", ".air.toml"]
Build Optimization¶
Compiler Optimizations¶
# Size optimization
go build -ldflags "-w -s" -trimpath
# Performance optimization
go build -ldflags "-w -s" -trimpath -gcflags "-B"
# Link-time optimization (experimental)
go build -ldflags "-w -s" -trimpath -buildmode=pie
# Profile-guided optimization (Go 1.21+)
go build -pgo=auto -ldflags "-w -s" -trimpath
Build Caching¶
# Enable build cache
export GOCACHE=$(go env GOCACHE)
# Module cache
export GOMODCACHE=$(go env GOMODCACHE)
# Docker build cache
docker build --build-arg BUILDKIT_INLINE_CACHE=1 .
Binary Analysis¶
Size Analysis Script¶
#!/bin/bash
# scripts/analyze-binary.sh
BINARY=${1:-mcp-server}
if [ ! -f "$BINARY" ]; then
echo "Binary $BINARY not found"
exit 1
fi
echo "=== Binary Analysis for $BINARY ==="
echo
echo "File size:"
ls -lh "$BINARY" | awk '{print $5}'
echo
echo "Symbols (top 20):"
go tool nm "$BINARY" | grep -E ' T | D | B ' | sort -k3 -nr | head -20
echo
echo "Build info:"
go version -m "$BINARY"
echo
echo "Dependencies:"
go tool nm "$BINARY" | grep -o 'github.com/[^/]*/[^/]*' | sort | uniq -c | sort -nr | head -10
Security Scanning¶
#!/bin/bash
# scripts/security-scan.sh
echo "=== Security Analysis ==="
# Check for hardcoded secrets
echo "Checking for hardcoded secrets..."
truffleHog --regex --entropy=False .
# Scan dependencies for vulnerabilities
echo "Scanning dependencies..."
go list -json -m all | nancy sleuth
# Static analysis
echo "Running static analysis..."
gosec ./...
# Container security scan (if image built)
if [ "$1" = "container" ]; then
echo "Scanning container image..."
trivy image mcp-server:latest
fi
Performance Optimization¶
Build-Time Profiling¶
// build_profile.go - build with profiling enabled
//go:build profile
package main
import (
_ "net/http/pprof"
"net/http"
"log"
)
func init() {
go func() {
log.Println("Starting profile server on :6060")
log.Println(http.ListenAndServe(":6060", nil))
}()
}
Memory Optimization¶
# Build with memory optimization
go build -gcflags "-l=4" -ldflags "-w -s" -trimpath
# Runtime memory tuning
export GOGC=100
export GOMEMLIMIT=1GiB
export GOMAXPROCS=4
Deployment Scripts¶
Systemd Service¶
# /etc/systemd/system/mcp-server.service
[Unit]
Description=MCP Server
After=network.target
Wants=network.target
[Service]
Type=simple
User=mcp
Group=mcp
WorkingDirectory=/opt/mcp-server
ExecStart=/opt/mcp-server/mcp-server
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mcp-server
# Security settings
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/mcp-server
# Environment
Environment=MCP_LOG_LEVEL=info
Environment=MCP_TRANSPORT=http
Environment=MCP_HTTP_ADDR=:8000
[Install]
WantedBy=multi-user.target
Installation Script¶
#!/bin/bash
# scripts/install.sh
set -e
# Configuration
USER="mcp"
GROUP="mcp"
INSTALL_DIR="/opt/mcp-server"
SERVICE_NAME="mcp-server"
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# Create user and group
if ! id "$USER" &>/dev/null; then
useradd -r -s /bin/false -d "$INSTALL_DIR" "$USER"
fi
# Create installation directory
mkdir -p "$INSTALL_DIR"
mkdir -p "/var/log/$SERVICE_NAME"
# Copy binary
cp mcp-server "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR/mcp-server"
# Set ownership
chown -R "$USER:$GROUP" "$INSTALL_DIR"
chown -R "$USER:$GROUP" "/var/log/$SERVICE_NAME"
# Install systemd service
cp scripts/mcp-server.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
echo "Installation complete!"
echo "Start the service with: systemctl start $SERVICE_NAME"
echo "Check status with: systemctl status $SERVICE_NAME"
echo "View logs with: journalctl -u $SERVICE_NAME -f"
Best Practices¶
Build Performance¶
- Module caching: Use persistent module cache in CI/CD
- Layer caching: Optimize Docker layer caching
- Parallel builds: Use
GOMAXPROCS
for faster builds - Incremental builds: Structure code to minimize rebuild scope
Security¶
- Static linking: Use
CGO_ENABLED=0
for static binaries - Strip symbols: Use
-ldflags "-w -s"
to reduce size - Minimal containers: Use scratch or distroless base images
- Vulnerability scanning: Regularly scan dependencies and containers
Optimization¶
- Profile-guided optimization: Use PGO for performance-critical code
- Binary compression: Consider UPX for size-constrained deployments
- Runtime tuning: Configure Go runtime parameters for deployment environment
- Health checks: Include health check endpoints in production builds
This comprehensive building guide ensures your Go MCP server is optimized, secure, and ready for production deployment.