--- title: "Building Dynamically Linked Bifrost Binary" description: "Learn how to build a dynamically linked Bifrost binary required for custom plugin support" icon: "hammer" --- ## Why Dynamic Linking? Go's plugin system requires **dynamic linking** to load `.so` files at runtime. By default, Bifrost builds are **statically linked** for maximum portability across Linux distributions - they bundle all dependencies including the C standard library (libc). However, statically linked binaries **cannot load Go plugins**. To use custom plugins with Bifrost, you must build a dynamically linked binary that links against the system's libc at runtime. Dynamic plugins only work on **Linux** and **macOS** (Darwin). Windows is not supported by Go's plugin system. ## Static vs Dynamic Builds ### Static Builds (Default) Bifrost's default build configuration creates statically linked binaries: ```bash go build \ -ldflags="-w -s -extldflags '-static' -X main.Version=v1.3.30" \ -tags "sqlite_static" \ -o bifrost-http ``` **Characteristics:** - ✅ Portable across all Linux distributions (musl, glibc, etc.) - ✅ No external dependencies required at runtime - ✅ Smaller deployment surface area - ❌ **Cannot load Go plugins** **Use static builds when:** You don't need custom plugins and want maximum portability. ### Dynamic Builds (For Plugins) To enable plugin support, build without static linking flags: ```bash go build \ -ldflags="-w -s -X main.Version=v1.3.30" \ -o bifrost-http ``` **Characteristics:** - ✅ **Can load Go plugins** (`.so` files) - ✅ Slightly faster compilation - ⚠️ Must match the target system's libc (musl vs glibc) - ⚠️ Less portable across different Linux distributions **Use dynamic builds when:** You need custom plugin support. ## Building with Makefile The easiest way to build a dynamic binary is using the `DYNAMIC=1` flag with the Makefile: ### Local Build ```bash # Build dynamically linked binary for your current platform make build DYNAMIC=1 # With version tag make build DYNAMIC=1 VERSION=1.3.30 ``` This creates `tmp/bifrost-http` as a dynamically linked binary. ### Cross-Compilation ```bash # Build for Linux AMD64 (uses Docker if cross-compiling) make build DYNAMIC=1 GOOS=linux GOARCH=amd64 # Build for Linux ARM64 make build DYNAMIC=1 GOOS=linux GOARCH=arm64 ``` ### How It Works The `DYNAMIC=1` flag automatically: - ✅ Removes `-extldflags "-static"` from ldflags - ✅ Removes `-tags "sqlite_static"` build tag - ✅ Keeps `CGO_ENABLED=1` (required for SQLite and plugins) - ✅ Uses Docker for cross-compilation when needed ## Building with Docker For containerized deployments, you'll need to modify the Dockerfile. Here are two complete examples based on your target environment's libc. ### Option A: Alpine Linux (musl libc) Use this for Alpine-based deployments or when you want minimal image size. ```dockerfile # --- UI Build Stage: Build the React + Vite frontend --- FROM node:25-alpine3.23 AS ui-builder WORKDIR /app # Copy UI package files and install dependencies COPY ui/package*.json ./ RUN npm ci # Copy UI source code COPY ui/ ./ # Build UI (skip the copy-build step) RUN npm run build-enterprise # --- Go Build Stage: Compile the Go binary --- FROM golang:1.26.1-alpine3.23 AS builder WORKDIR /app # Install dependencies including gcc for CGO and sqlite RUN apk add --no-cache gcc musl-dev sqlite-dev # Set environment for CGO-enabled build (required for go-sqlite3 and plugins) ENV CGO_ENABLED=1 GOOS=linux COPY transports/go.mod transports/go.sum ./ RUN go mod download # Copy source code and dependencies COPY transports/ ./ COPY --from=ui-builder /app/out ./bifrost-http/ui # Build the binary with CGO enabled for DYNAMIC LINKING ENV GOWORK=off ARG VERSION=unknown RUN go build \ -ldflags="-w -s -X main.Version=v${VERSION}" \ -a -trimpath \ -o /app/main \ ./bifrost-http # Verify build succeeded RUN test -f /app/main || (echo "Build failed" && exit 1) # --- Runtime Stage: Minimal runtime image --- FROM alpine:3.23 WORKDIR /app # Install runtime dependencies for CGO-enabled dynamic binary # musl: C standard library (required for CGO binaries) # libgcc: GCC runtime library # ca-certificates: For HTTPS connections # wget: For healthcheck RUN apk add --no-cache musl libgcc ca-certificates wget # Create data directory and set up user COPY --from=builder /app/main . COPY --from=builder /app/docker-entrypoint.sh . # Getting arguments ARG ARG_APP_PORT=8080 ARG ARG_APP_HOST=0.0.0.0 ARG ARG_LOG_LEVEL=info ARG ARG_LOG_STYLE=json ARG ARG_APP_DIR=/app/data # Environment variables with defaults (can be overridden at runtime) ENV APP_PORT=$ARG_APP_PORT \ APP_HOST=$ARG_APP_HOST \ LOG_LEVEL=$ARG_LOG_LEVEL \ LOG_STYLE=$ARG_LOG_STYLE \ APP_DIR=$ARG_APP_DIR RUN mkdir -p $APP_DIR/logs && \ adduser -D -s /bin/sh appuser && \ chown -R appuser:appuser /app && \ chmod +x /app/docker-entrypoint.sh USER appuser # Declare volume for data persistence VOLUME ["/app/data"] EXPOSE $APP_PORT # Health check for container status monitoring HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:${APP_PORT}/metrics || exit 1 # Use entrypoint script that handles volume permissions and argument processing ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["/app/main"] ``` **Key changes from static build:** - Line 40-44: Removed `-extldflags '-static'` and `-tags "sqlite_static"` - Removed UPX compression step (optional, but simpler) - Runtime uses musl libc from Alpine base image **Build and run:** ```bash # Build the image docker build -f transports/Dockerfile -t bifrost:dynamic-alpine . # Run the container docker run -p 8080:8080 -v ./plugins:/app/data/plugins bifrost:dynamic-alpine ``` ### Option B: Debian (glibc) Use this for Debian/Ubuntu-based deployments or when deploying to glibc-based systems. ```dockerfile # --- UI Build Stage: Build the React + Vite frontend --- FROM node:25-bookworm AS ui-builder WORKDIR /app # Copy UI package files and install dependencies COPY ui/package*.json ./ RUN npm ci # Copy UI source code COPY ui/ ./ # Build UI RUN npm run build-enterprise # --- Go Build Stage: Compile the Go binary --- FROM golang:1.26.1-bookworm AS builder WORKDIR /app # Install dependencies including gcc for CGO and sqlite RUN apt-get update && apt-get install -y \ gcc \ libc6-dev \ libsqlite3-dev \ && rm -rf /var/lib/apt/lists/* # Set environment for CGO-enabled build (required for go-sqlite3 and plugins) ENV CGO_ENABLED=1 GOOS=linux COPY transports/go.mod transports/go.sum ./ RUN go mod download # Copy source code and dependencies COPY transports/ ./ COPY --from=ui-builder /app/out ./bifrost-http/ui # Build the binary with CGO enabled for DYNAMIC LINKING ENV GOWORK=off ARG VERSION=unknown RUN go build \ -ldflags="-w -s -X main.Version=v${VERSION}" \ -a -trimpath \ -o /app/main \ ./bifrost-http # Verify build succeeded RUN test -f /app/main || (echo "Build failed" && exit 1) # --- Runtime Stage: Minimal runtime image --- FROM debian:bookworm-slim WORKDIR /app # Install runtime dependencies for CGO-enabled dynamic binary # libc6: GNU C Library (required for glibc-linked binaries) # ca-certificates: For HTTPS connections RUN apt-get update && apt-get install -y \ libc6 \ ca-certificates \ wget \ && rm -rf /var/lib/apt/lists/* # Create data directory and set up user COPY --from=builder /app/main . COPY --from=builder /app/docker-entrypoint.sh . # Getting arguments ARG ARG_APP_PORT=8080 ARG ARG_APP_HOST=0.0.0.0 ARG ARG_LOG_LEVEL=info ARG ARG_LOG_STYLE=json ARG ARG_APP_DIR=/app/data # Environment variables with defaults (can be overridden at runtime) ENV APP_PORT=$ARG_APP_PORT \ APP_HOST=$ARG_APP_HOST \ LOG_LEVEL=$ARG_LOG_LEVEL \ LOG_STYLE=$ARG_LOG_STYLE \ APP_DIR=$ARG_APP_DIR RUN mkdir -p $APP_DIR/logs && \ useradd -m -s /bin/sh appuser && \ chown -R appuser:appuser /app && \ chmod +x /app/docker-entrypoint.sh USER appuser # Declare volume for data persistence VOLUME ["/app/data"] EXPOSE $APP_PORT # Health check for container status monitoring HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:${APP_PORT}/metrics || exit 1 # Use entrypoint script that handles volume permissions and argument processing ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["/app/main"] ``` **Key differences from Alpine version:** - Uses `bookworm` (Debian 12) base images instead of Alpine - Installs `apt` packages instead of `apk` - Runtime uses glibc (libc6) instead of musl - Uses `useradd` instead of `adduser` for user creation **Build and run:** ```bash # Build the image docker build -f transports/Dockerfile.debian -t bifrost:dynamic-debian . # Run the container docker run -p 8080:8080 -v ./plugins:/app/data/plugins bifrost:dynamic-debian ``` ## libc Compatibility Understanding libc (C standard library) compatibility is **critical** when building dynamic binaries and plugins. ### musl vs glibc Linux distributions use one of two main C standard libraries: | libc Type | Used By | Characteristics | |-----------|---------|-----------------| | **musl** | Alpine Linux | Lightweight, minimal, security-focused | | **glibc** | Debian, Ubuntu, RHEL, CentOS, Fedora, Amazon Linux | Standard GNU C Library, feature-rich | ### The Golden Rule - **A binary built with musl will NOT run on glibc systems.** - **A binary built with glibc will NOT run on musl systems.** - **Plugins and Bifrost MUST use the same libc.** ### Why This Matters When you build a dynamic binary: ```bash # Built on Alpine (musl) $ ldd bifrost-http linux-vdso.so.1 libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 # Built on Debian (glibc) $ ldd bifrost-http linux-vdso.so.1 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 ``` The binary is linked to a **specific** libc implementation. If you try to run it on a system with a different libc, you'll get errors like: ``` error while loading shared libraries: libc.musl-x86_64.so.1: cannot open shared object file ``` ### Choosing Your Build Environment **Decision Matrix:** | Target Deployment | Build With | Dockerfile Base | |-------------------|------------|-----------------| | Alpine containers | musl | `golang:1.26.1-alpine3.23` | | Debian/Ubuntu containers | glibc | `golang:1.26.1-bookworm` | | Ubuntu/Debian servers | glibc | `golang:1.26.1-bookworm` | | RHEL/CentOS servers | glibc | Native build or glibc container | | Kubernetes (Alpine) | musl | `golang:1.26.1-alpine3.23` | | Kubernetes (Debian) | glibc | `golang:1.26.1-bookworm` | **Simple rule:** Build with the same base OS family as your deployment target. ### Building Plugins Plugins **must** be built with the **exact same environment** as your Bifrost binary: ```bash # If Bifrost was built with Alpine/musl docker run --rm \ -v "$PWD:/work" \ -w /work \ golang:1.26.1-alpine3.23 \ sh -c "apk add --no-cache gcc musl-dev && \ go build -buildmode=plugin -o myplugin.so main.go" # If Bifrost was built with Debian/glibc docker run --rm \ -v "$PWD:/work" \ -w /work \ golang:1.26.1-bookworm \ sh -c "apt-get update && apt-get install -y gcc && \ go build -buildmode=plugin -o myplugin.so main.go" ``` See the [hello-world plugin Makefile](https://github.com/maximhq/bifrost/blob/main/examples/plugins/hello-world/Makefile) for a complete example. ## Verification ### Verify Dynamic Linking After building, check that your binary is dynamically linked: ```bash # Check binary dependencies ldd tmp/bifrost-http # Expected output (musl): linux-vdso.so.1 libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 # Expected output (glibc): linux-vdso.so.1 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 ``` If you see `statically linked`, the binary **will not load plugins**. ### Verify Plugin Compatibility Test that your plugin loads successfully: ```bash # Start Bifrost with your plugin configured ./tmp/bifrost-http -config config.json # Check logs for plugin initialization # Should see: "Plugin loaded successfully: your-plugin-name" ``` ## Go Version and Package Compatibility ### Go Version Requirement Bifrost is built with **Go 1.26.1**. Your plugin **must** be compiled with the exact same Go version to ensure compatibility. ```bash # Check your Go version go version # Should output: go version go1.26.1 ... # If you need to install Go 1.26.1 # Visit: https://go.dev/dl/ ``` ### Key Package Versions Bifrost uses the following key packages across its three main modules that may affect plugin development: #### Transport Layer (`transports/go.mod`) | Package | Version | Purpose | |---------|---------|---------| | `github.com/bytedance/sonic` | v1.14.1 | High-performance JSON serialization | | `github.com/valyala/fasthttp` | v1.67.0 | Fast HTTP server/client | | `github.com/fasthttp/router` | v1.5.4 | HTTP router for fasthttp | | `github.com/fasthttp/websocket` | v1.5.12 | WebSocket support | | `github.com/prometheus/client_golang` | v1.23.0 | Prometheus metrics | | `gorm.io/gorm` | v1.31.1 | Database ORM | #### Core Layer (`core/go.mod`) | Package | Version | Purpose | |---------|---------|---------| | `github.com/bytedance/sonic` | v1.14.1 | High-performance JSON serialization | | `github.com/valyala/fasthttp` | v1.67.0 | Fast HTTP client for providers | | `github.com/google/uuid` | v1.6.0 | UUID generation | | `github.com/rs/zerolog` | v1.34.0 | Zero-allocation JSON logger | | `github.com/mark3labs/mcp-go` | v0.41.1 | Model Context Protocol support | | `golang.org/x/oauth2` | v0.32.0 | OAuth2 client | #### Framework Layer (`framework/go.mod`) | Package | Version | Purpose | |---------|---------|---------| | `github.com/redis/go-redis/v9` | v9.14.0 | Redis client for caching | | `github.com/weaviate/weaviate-go-client/v5` | v5.5.0 | Weaviate vector store client | | `github.com/mattn/go-sqlite3` | v1.14.32 | SQLite3 driver (requires CGO) | | `gorm.io/gorm` | v1.31.1 | Database ORM | | `gorm.io/driver/sqlite` | v1.6.0 | GORM SQLite driver | | `gorm.io/driver/postgres` | v1.6.0 | GORM PostgreSQL driver | | `golang.org/x/crypto` | v0.43.0 | Cryptographic functions | If your plugin imports any of these packages, use compatible versions to avoid runtime issues. Check `transports/go.mod`, `core/go.mod`, and `framework/go.mod` for complete dependency lists. ### Checking Bifrost's Dependencies To see all dependencies used by Bifrost across its three main modules: ```bash # View transport layer dependencies cat transports/go.mod # View core dependencies cat core/go.mod # View framework dependencies cat framework/go.mod # Or list all dependencies for a specific module cd transports && go list -m all cd ../core && go list -m all cd ../framework && go list -m all ``` ### Plugin go.mod Example When creating a plugin, your `go.mod` should match Bifrost's Go version: ```go module github.com/example/my-plugin go 1.26.1 require ( github.com/maximhq/bifrost/core v1.2.38 // Optional: Add framework for advanced features // github.com/maximhq/bifrost/framework v1.1.48 // Add other dependencies as needed, matching versions from Bifrost's go.mod files // github.com/bytedance/sonic v1.14.1 // github.com/rs/zerolog v1.34.0 ) ``` Import only the Bifrost modules you need. Most plugins only require `core`. Use `framework` if you need access to config stores, vector stores, or other framework features. ## Troubleshooting ### Common Errors #### 1. Cannot load plugin - Go version mismatch ``` cannot load plugin: plugin was built with a different version of package runtime/internal/sys ``` **Cause:** Plugin and Bifrost were built with different Go versions. **Solution:** Use the exact same Go version (Go 1.26.1) for both: ```bash # Check Go version used for Bifrost ./tmp/bifrost-http -version # Verify your Go version matches go version # Should output: go version go1.26.1 # See full compatibility requirements ``` Refer to [Go Version and Package Compatibility](#go-version-and-package-compatibility) for details. #### 2. Shared library not found ``` error while loading shared libraries: libc.musl-x86_64.so.1: cannot open shared object file ``` **Cause:** Binary built with musl trying to run on glibc system (or vice versa). **Solution:** Rebuild with the correct libc for your target system. #### 3. Plugin architecture mismatch ``` plugin was built with a different version of package internal/cpu ``` **Cause:** Plugin and Bifrost built for different architectures (amd64 vs arm64). **Solution:** Ensure `GOARCH` matches for both builds: ```bash # Check architecture uname -m # x86_64 = amd64, aarch64 = arm64 # Build with explicit architecture GOARCH=amd64 go build ... ``` #### 4. Plugin file not found ``` plugin.Open("myplugin.so"): realpath failed: no such file or directory ``` **Cause:** Plugin file path is incorrect in config. **Solution:** Use absolute paths or verify relative paths: ```json { "plugins": [ { "path": "/app/data/plugins/myplugin.so", "config": {} } ] } ``` ## Best Practices ### 1. Document Your Build Environment Create a `BUILD.md` file documenting: - Go version used - Base image (Alpine vs Debian) - Build commands - Target deployment platform ### 2. Use Consistent Tooling Match Bifrost's exact Go version and key dependencies (see [Go Version and Package Compatibility](#go-version-and-package-compatibility)): ```bash # Pin Go version in Dockerfile FROM golang:1.26.1-alpine3.23 AS builder # Pin Go version in Makefile/CI GO_VERSION=1.26.1 ``` ### 3. Test Plugin Loading Locally Before deploying, test plugin loading: ```bash # Build both Bifrost and plugin make build DYNAMIC=1 cd examples/plugins/hello-world && make build # Test loading ./tmp/bifrost-http -config examples/plugins/hello-world/config.json ``` ### 4. Version Your Plugins Tag plugin builds with version and build info: ```bash go build -buildmode=plugin \ -ldflags="-X main.Version=v1.0.0 -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o myplugin-v1.0.0.so ``` ### 5. Multi-Stage Dockerfiles for Plugins Build plugins in the same Dockerfile as Bifrost: ```dockerfile # Build plugin FROM golang:1.26.1-alpine3.23 AS plugin-builder WORKDIR /plugin COPY plugins/myplugin/ . RUN apk add --no-cache gcc musl-dev && \ go build -buildmode=plugin -o myplugin.so main.go # Build Bifrost FROM golang:1.26.1-alpine3.23 AS bifrost-builder # ... (bifrost build steps) # Runtime FROM alpine:3.23 COPY --from=bifrost-builder /app/main . COPY --from=plugin-builder /plugin/myplugin.so /app/plugins/ ``` This ensures plugins and Bifrost use identical build environments. ## Next Steps Now that you have a dynamically linked Bifrost binary: 1. **[Write your first plugin](./writing-plugin)** - Learn the plugin API and create custom functionality 2. **[Deploy with plugins](../deployment-guides)** - Best practices for production deployments 3. **[Example plugins](https://github.com/maximhq/bifrost/tree/main/examples/plugins)** - Study working examples For questions or issues with dynamic builds and plugins, visit our [GitHub Discussions](https://github.com/maximhq/bifrost/discussions) or [Discord community](https://discord.gg/exN5KAydbU).