612 lines
16 KiB
Markdown
612 lines
16 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```dockerfile
|
|
# 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:
|
|
|
|
```dockerfile
|
|
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:
|
|
|
|
```bash
|
|
./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:**
|
|
|
|
```dockerfile
|
|
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):
|
|
|
|
```dockerfile
|
|
RUN gcc -static -g -O0 -DDEBUG \
|
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
|
# ... rest of flags
|
|
```
|
|
|
|
### 4. Multi-Architecture Support
|
|
|
|
Build for different architectures:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
#!/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:
|
|
|
|
```bash
|
|
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:
|
|
```bash
|
|
-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:
|
|
```bash
|
|
./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**:
|
|
```bash
|
|
sudo usermod -aG docker $USER
|
|
newgrp docker # Or logout and login
|
|
```
|
|
|
|
### Issue 4: Binary Won't Run on Target System
|
|
|
|
**Checks**:
|
|
```bash
|
|
# 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:
|
|
|
|
```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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```ini
|
|
[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)
|
|
|
|
```dockerfile
|
|
FROM scratch
|
|
COPY my_app_static_x86_64 /app
|
|
ENTRYPOINT ["/app"]
|
|
```
|
|
|
|
Build and run:
|
|
```bash
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
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)
|
|
```bash
|
|
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
|
|
|
|
- [MUSL libc](https://musl.libc.org/)
|
|
- [Alpine Linux](https://alpinelinux.org/)
|
|
- [nostr_core_lib](https://github.com/chebizarro/nostr_core_lib)
|
|
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
|
|
- [c-relay Implementation](./docs/musl_static_build.md)
|
|
|
|
## Example: Minimal Nostr Client
|
|
|
|
Here's a complete example of building a minimal Nostr client:
|
|
|
|
```c
|
|
// 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:**
|
|
```dockerfile
|
|
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:**
|
|
```bash
|
|
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! |