Files
c-relay/STATIC_MUSL_GUIDE.md
2025-11-11 17:01:39 -04:00

16 KiB

Static MUSL Build Guide for C Programs

Overview

This guide explains how to build truly portable static binaries using Alpine Linux and MUSL libc. These binaries have zero runtime dependencies and work on any Linux distribution without modification.

This guide is specifically tailored for C programs that use:

  • nostr_core_lib - Nostr protocol implementation
  • nostr_login_lite - Nostr authentication library
  • Common dependencies: libwebsockets, OpenSSL, SQLite, curl, secp256k1

Why MUSL Static Binaries?

Advantages Over glibc

Feature MUSL Static glibc Static glibc Dynamic
Portability ✓ Any Linux ⚠ glibc only ✗ Requires matching libs
Binary Size ~7-10 MB ~12-15 MB ~2-3 MB
Dependencies None NSS libs Many system libs
Deployment Single file Single file + NSS Binary + libraries
Compatibility Universal glibc version issues Library version hell

Key Benefits

  1. True Portability: Works on Alpine, Ubuntu, Debian, CentOS, Arch, etc.
  2. No Library Hell: No GLIBC_2.XX not found errors
  3. Simple Deployment: Just copy one file
  4. Reproducible Builds: Same Docker image = same binary
  5. Security: No dependency on system libraries with vulnerabilities

Quick Start

Prerequisites

  • Docker installed and running
  • Your C project with source code
  • Internet connection for downloading dependencies

Basic Build Process

# 1. Copy the Dockerfile template (see below)
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static

# 2. Customize for your project (see Customization section)
vim Dockerfile.static

# 3. Build the static binary
docker build --platform linux/amd64 -f Dockerfile.static -t my-app-builder .

# 4. Extract the binary
docker create --name temp-container my-app-builder
docker cp temp-container:/build/my_app_static ./my_app_static
docker rm temp-container

# 5. Verify it's static
ldd ./my_app_static  # Should show "not a dynamic executable"

Dockerfile Template

Here's a complete Dockerfile template you can customize for your project:

# Alpine-based MUSL static binary builder
# Produces truly portable binaries with zero runtime dependencies

FROM alpine:3.19 AS builder

# Install build dependencies
RUN apk add --no-cache \
    build-base \
    musl-dev \
    git \
    cmake \
    pkgconfig \
    autoconf \
    automake \
    libtool \
    openssl-dev \
    openssl-libs-static \
    zlib-dev \
    zlib-static \
    curl-dev \
    curl-static \
    sqlite-dev \
    sqlite-static \
    linux-headers \
    wget \
    bash

WORKDIR /build

# Build libsecp256k1 static (required for Nostr)
RUN cd /tmp && \
    git clone https://github.com/bitcoin-core/secp256k1.git && \
    cd secp256k1 && \
    ./autogen.sh && \
    ./configure --enable-static --disable-shared --prefix=/usr \
        CFLAGS="-fPIC" && \
    make -j$(nproc) && \
    make install && \
    rm -rf /tmp/secp256k1

# Build libwebsockets static (if needed for WebSocket support)
RUN cd /tmp && \
    git clone --depth 1 --branch v4.3.3 https://github.com/warmcat/libwebsockets.git && \
    cd libwebsockets && \
    mkdir build && cd build && \
    cmake .. \
        -DLWS_WITH_STATIC=ON \
        -DLWS_WITH_SHARED=OFF \
        -DLWS_WITH_SSL=ON \
        -DLWS_WITHOUT_TESTAPPS=ON \
        -DLWS_WITHOUT_TEST_SERVER=ON \
        -DLWS_WITHOUT_TEST_CLIENT=ON \
        -DLWS_WITHOUT_TEST_PING=ON \
        -DLWS_WITH_HTTP2=OFF \
        -DLWS_WITH_LIBUV=OFF \
        -DLWS_WITH_LIBEVENT=OFF \
        -DLWS_IPV6=ON \
        -DCMAKE_BUILD_TYPE=Release \
        -DCMAKE_INSTALL_PREFIX=/usr \
        -DCMAKE_C_FLAGS="-fPIC" && \
    make -j$(nproc) && \
    make install && \
    rm -rf /tmp/libwebsockets

# Copy git configuration for submodules
COPY .gitmodules /build/.gitmodules
COPY .git /build/.git

# Initialize submodules
RUN git submodule update --init --recursive

# Copy and build nostr_core_lib
COPY nostr_core_lib /build/nostr_core_lib/
RUN cd nostr_core_lib && \
    chmod +x build.sh && \
    sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
    rm -f *.o *.a 2>/dev/null || true && \
    ./build.sh --nips=1,6,13,17,19,44,59

# Copy and build nostr_login_lite (if used)
# COPY nostr_login_lite /build/nostr_login_lite/
# RUN cd nostr_login_lite && make static

# Copy your application source
COPY src/ /build/src/
COPY Makefile /build/Makefile

# Build your application with full static linking
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
    -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
    -I. -Inostr_core_lib -Inostr_core_lib/nostr_core \
    -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
    src/*.c \
    -o /build/my_app_static \
    nostr_core_lib/libnostr_core_x64.a \
    -lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
    -lcurl -lz -lpthread -lm -ldl && \
    strip /build/my_app_static

# Verify it's truly static
RUN echo "=== Binary Information ===" && \
    file /build/my_app_static && \
    ls -lh /build/my_app_static && \
    echo "=== Checking for dynamic dependencies ===" && \
    (ldd /build/my_app_static 2>&1 || echo "Binary is static")

# Output stage - just the binary
FROM scratch AS output
COPY --from=builder /build/my_app_static /my_app_static

Customization Guide

1. Adjust Dependencies

Add dependencies by modifying the apk add section:

RUN apk add --no-cache \
    build-base \
    musl-dev \
    # Add your dependencies here:
    libpng-dev \
    libpng-static \
    libjpeg-turbo-dev \
    libjpeg-turbo-static

Remove unused dependencies to speed up builds:

  • Remove libwebsockets section if you don't need WebSocket support
  • Remove sqlite if you don't use databases
  • Remove curl if you don't make HTTP requests

2. Configure nostr_core_lib NIPs

Specify which NIPs your application needs:

./build.sh --nips=1,6,19  # Minimal: Basic protocol, keys, bech32
./build.sh --nips=1,6,13,17,19,44,59  # Full: All common NIPs
./build.sh --nips=all  # Everything available

Common NIP combinations:

  • Basic client: 1,6,19 (events, keys, bech32)
  • With encryption: 1,6,19,44 (add modern encryption)
  • With DMs: 1,6,17,19,44,59 (add private messages)
  • Relay/server: 1,6,13,17,19,42,44,59 (add PoW, auth)

3. Modify Compilation Flags

For your application:

RUN gcc -static -O2 -Wall -Wextra -std=c99 \
    -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \  # REQUIRED for MUSL
    -I. -Inostr_core_lib \                    # Include paths
    src/*.c \                                  # Your source files
    -o /build/my_app_static \                 # Output binary
    nostr_core_lib/libnostr_core_x64.a \     # Nostr library
    -lwebsockets -lssl -lcrypto \             # Link libraries
    -lsqlite3 -lsecp256k1 -lcurl \
    -lz -lpthread -lm -ldl

Debug build (with symbols, no optimization):

RUN gcc -static -g -O0 -DDEBUG \
    -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
    # ... rest of flags

4. Multi-Architecture Support

Build for different architectures:

# x86_64 (Intel/AMD)
docker build --platform linux/amd64 -f Dockerfile.static -t my-app-x86 .

# ARM64 (Apple Silicon, Raspberry Pi 4+)
docker build --platform linux/arm64 -f Dockerfile.static -t my-app-arm64 .

Build Script Template

Create a build_static.sh script for convenience:

#!/bin/bash
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
DOCKERFILE="$SCRIPT_DIR/Dockerfile.static"

# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
    x86_64)
        PLATFORM="linux/amd64"
        OUTPUT_NAME="my_app_static_x86_64"
        ;;
    aarch64|arm64)
        PLATFORM="linux/arm64"
        OUTPUT_NAME="my_app_static_arm64"
        ;;
    *)
        echo "Unknown architecture: $ARCH"
        exit 1
        ;;
esac

echo "Building for platform: $PLATFORM"
mkdir -p "$BUILD_DIR"

# Build Docker image
docker build \
    --platform "$PLATFORM" \
    -f "$DOCKERFILE" \
    -t my-app-builder:latest \
    --progress=plain \
    .

# Extract binary
CONTAINER_ID=$(docker create my-app-builder:latest)
docker cp "$CONTAINER_ID:/build/my_app_static" "$BUILD_DIR/$OUTPUT_NAME"
docker rm "$CONTAINER_ID"

chmod +x "$BUILD_DIR/$OUTPUT_NAME"

echo "✓ Build complete: $BUILD_DIR/$OUTPUT_NAME"
echo "✓ Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"

# Verify
if ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 | grep -q "not a dynamic executable"; then
    echo "✓ Binary is fully static"
else
    echo "⚠ Warning: Binary may have dynamic dependencies"
fi

Make it executable:

chmod +x build_static.sh
./build_static.sh

Common Issues and Solutions

Issue 1: Fortification Errors

Error:

undefined reference to '__snprintf_chk'
undefined reference to '__fprintf_chk'

Cause: GCC's -O2 enables fortification by default, which uses glibc-specific functions.

Solution: Add these flags to all compilation commands:

-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0

This must be applied to:

  1. nostr_core_lib build.sh
  2. Your application compilation
  3. Any other libraries you build

Issue 2: Missing Symbols from nostr_core_lib

Error:

undefined reference to 'nostr_create_event'
undefined reference to 'nostr_sign_event'

Cause: Required NIPs not included in nostr_core_lib build.

Solution: Add missing NIPs:

./build.sh --nips=1,6,19  # Add the NIPs you need

Issue 3: Docker Permission Denied

Error:

permission denied while trying to connect to the Docker daemon socket

Solution:

sudo usermod -aG docker $USER
newgrp docker  # Or logout and login

Issue 4: Binary Won't Run on Target System

Checks:

# 1. Verify it's static
ldd my_app_static  # Should show "not a dynamic executable"

# 2. Check architecture
file my_app_static  # Should match target system

# 3. Test on different distributions
docker run --rm -v $(pwd):/app alpine:latest /app/my_app_static --version
docker run --rm -v $(pwd):/app ubuntu:latest /app/my_app_static --version

Project Structure Example

Organize your project for easy static builds:

my-nostr-app/
├── src/
│   ├── main.c
│   ├── handlers.c
│   └── utils.c
├── nostr_core_lib/          # Git submodule
├── nostr_login_lite/        # Git submodule (if used)
├── Dockerfile.static        # Static build Dockerfile
├── build_static.sh          # Build script
├── Makefile                 # Regular build
└── README.md

Makefile Integration

Add static build targets to your Makefile:

# Regular dynamic build
all: my_app

my_app: src/*.c
	gcc -O2 src/*.c -o my_app \
		nostr_core_lib/libnostr_core_x64.a \
		-lssl -lcrypto -lsecp256k1 -lz -lpthread -lm

# Static MUSL build via Docker
static:
	./build_static.sh

# Clean
clean:
	rm -f my_app build/my_app_static_*

.PHONY: all static clean

Deployment

Single Binary Deployment

# Copy to server
scp build/my_app_static_x86_64 user@server:/opt/my-app/

# Run (no dependencies needed!)
ssh user@server
/opt/my-app/my_app_static_x86_64

SystemD Service

[Unit]
Description=My Nostr Application
After=network.target

[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/my-app
ExecStart=/opt/my-app/my_app_static_x86_64
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Docker Container (Minimal)

FROM scratch
COPY my_app_static_x86_64 /app
ENTRYPOINT ["/app"]

Build and run:

docker build -t my-app:latest .
docker run --rm my-app:latest --help

Reusing c-relay Files

You can directly copy these files from c-relay:

1. Dockerfile.alpine-musl

cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static

Then customize:

  • Change binary name (line 125)
  • Adjust source files (line 122-124)
  • Modify include paths (line 120-121)

2. build_static.sh

cp /path/to/c-relay/build_static.sh ./

Then customize:

  • Change OUTPUT_NAME variable (lines 66, 70)
  • Update Docker image name (line 98)
  • Modify verification commands (lines 180-184)

3. .dockerignore (Optional)

cp /path/to/c-relay/.dockerignore ./

Helps speed up Docker builds by excluding unnecessary files.

Best Practices

  1. Version Control: Commit your Dockerfile and build script
  2. Tag Builds: Include git commit hash in binary version
  3. Test Thoroughly: Verify on multiple distributions
  4. Document Dependencies: List required NIPs and libraries
  5. Automate: Use CI/CD to build on every commit
  6. Archive Binaries: Keep old versions for rollback

Performance Comparison

Metric MUSL Static glibc Dynamic
Binary Size 7-10 MB 2-3 MB + libs
Startup Time ~50ms ~40ms
Memory Usage Similar Similar
Portability ✓ Universal ✗ System-dependent
Deployment Single file Binary + libraries

References

Example: Minimal Nostr Client

Here's a complete example of building a minimal Nostr client:

// minimal_client.c
#include "nostr_core/nostr_core.h"
#include <stdio.h>

int main() {
    // Generate keypair
    char nsec[64], npub[64];
    nostr_generate_keypair(nsec, npub);
    
    printf("Generated keypair:\n");
    printf("Private key (nsec): %s\n", nsec);
    printf("Public key (npub): %s\n", npub);
    
    // Create event
    cJSON *event = nostr_create_event(1, "Hello, Nostr!", NULL);
    nostr_sign_event(event, nsec);
    
    char *json = cJSON_Print(event);
    printf("\nSigned event:\n%s\n", json);
    
    free(json);
    cJSON_Delete(event);
    return 0;
}

Dockerfile.static:

FROM alpine:3.19 AS builder
RUN apk add --no-cache build-base musl-dev git autoconf automake libtool \
    openssl-dev openssl-libs-static zlib-dev zlib-static

WORKDIR /build

# Build secp256k1
RUN cd /tmp && git clone https://github.com/bitcoin-core/secp256k1.git && \
    cd secp256k1 && ./autogen.sh && \
    ./configure --enable-static --disable-shared --prefix=/usr CFLAGS="-fPIC" && \
    make -j$(nproc) && make install

# Copy and build nostr_core_lib
COPY nostr_core_lib /build/nostr_core_lib/
RUN cd nostr_core_lib && \
    sed -i 's/CFLAGS="-Wall/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall/' build.sh && \
    ./build.sh --nips=1,6,19

# Build application
COPY minimal_client.c /build/
RUN gcc -static -O2 -Wall -std=c99 \
    -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
    -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson \
    minimal_client.c -o /build/minimal_client_static \
    nostr_core_lib/libnostr_core_x64.a \
    -lssl -lcrypto -lsecp256k1 -lz -lpthread -lm -ldl && \
    strip /build/minimal_client_static

FROM scratch
COPY --from=builder /build/minimal_client_static /minimal_client_static

Build and run:

docker build -f Dockerfile.static -t minimal-client .
docker create --name temp minimal-client
docker cp temp:/minimal_client_static ./
docker rm temp

./minimal_client_static

Conclusion

Static MUSL binaries provide the best portability for C applications. While they're slightly larger than dynamic binaries, the benefits of zero dependencies and universal compatibility make them ideal for:

  • Server deployments across different Linux distributions
  • Embedded systems and IoT devices
  • Docker containers (FROM scratch)
  • Distribution to users without dependency management
  • Long-term archival and reproducibility

Follow this guide to create portable, self-contained binaries for your Nostr applications!