Skip to content

Commit 4cf0506

Browse files
mcollinaclaude
andcommitted
Add multi-arch Docker build system for DockerHub publishing
- Add GitHub Actions workflow for building and publishing to DockerHub - Support three variants: bookworm, slim (multi-stage), and alpine - Build for both amd64 and arm64 using native runners - Automatic version tagging from Node.js binary - Update README with DockerHub pull instructions and badges Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4062637 commit 4cf0506

File tree

5 files changed

+491
-0
lines changed

5 files changed

+491
-0
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Multi-architecture Docker build and publish workflow
2+
# Builds Node.js with pointer compression for amd64 and arm64
3+
# Supports three variants: bookworm (Debian), slim (minimal), alpine
4+
5+
name: Build and Publish Docker Images
6+
7+
on:
8+
push:
9+
branches: [main]
10+
tags: ['v*']
11+
pull_request:
12+
branches: [main]
13+
workflow_dispatch:
14+
15+
env:
16+
REGISTRY: docker.io
17+
IMAGE_NAME: platformatic/node-pointer-compression
18+
19+
jobs:
20+
# =============================================================================
21+
# Phase 1: Build images for each variant/architecture combination
22+
# =============================================================================
23+
build:
24+
runs-on: ${{ matrix.runner }}
25+
strategy:
26+
fail-fast: false
27+
matrix:
28+
include:
29+
# Bookworm builds
30+
- variant: bookworm
31+
arch: amd64
32+
runner: ubuntu-latest
33+
- variant: bookworm
34+
arch: arm64
35+
runner: ubuntu-24.04-arm
36+
# Slim builds
37+
- variant: slim
38+
arch: amd64
39+
runner: ubuntu-latest
40+
- variant: slim
41+
arch: arm64
42+
runner: ubuntu-24.04-arm
43+
# Alpine builds
44+
- variant: alpine
45+
arch: amd64
46+
runner: ubuntu-latest
47+
- variant: alpine
48+
arch: arm64
49+
runner: ubuntu-24.04-arm
50+
51+
steps:
52+
- name: Checkout repository
53+
uses: actions/checkout@v4
54+
55+
- name: Set up Docker Buildx
56+
uses: docker/setup-buildx-action@v3
57+
58+
- name: Log in to Docker Hub
59+
if: github.event_name != 'pull_request'
60+
uses: docker/login-action@v3
61+
with:
62+
registry: ${{ env.REGISTRY }}
63+
username: ${{ secrets.DOCKERHUB_USERNAME }}
64+
password: ${{ secrets.DOCKERHUB_TOKEN }}
65+
66+
- name: Build image
67+
id: build
68+
uses: docker/build-push-action@v6
69+
with:
70+
context: .
71+
file: docker/${{ matrix.variant }}/Dockerfile
72+
platforms: linux/${{ matrix.arch }}
73+
push: ${{ github.event_name != 'pull_request' }}
74+
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
75+
cache-from: type=gha,scope=${{ matrix.variant }}-${{ matrix.arch }}
76+
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}-${{ matrix.arch }}
77+
78+
- name: Export digest
79+
if: github.event_name != 'pull_request'
80+
run: |
81+
mkdir -p /tmp/digests/${{ matrix.variant }}
82+
digest="${{ steps.build.outputs.digest }}"
83+
touch "/tmp/digests/${{ matrix.variant }}/${digest#sha256:}"
84+
85+
- name: Upload digest
86+
if: github.event_name != 'pull_request'
87+
uses: actions/upload-artifact@v4
88+
with:
89+
name: digests-${{ matrix.variant }}-${{ matrix.arch }}
90+
path: /tmp/digests/${{ matrix.variant }}/*
91+
if-no-files-found: error
92+
retention-days: 1
93+
94+
# =============================================================================
95+
# Phase 2: Create multi-arch manifests for each variant
96+
# =============================================================================
97+
merge:
98+
runs-on: ubuntu-latest
99+
if: github.event_name != 'pull_request'
100+
needs: build
101+
strategy:
102+
matrix:
103+
variant: [bookworm, slim, alpine]
104+
105+
steps:
106+
- name: Checkout repository
107+
uses: actions/checkout@v4
108+
109+
- name: Download amd64 digest
110+
uses: actions/download-artifact@v4
111+
with:
112+
name: digests-${{ matrix.variant }}-amd64
113+
path: /tmp/digests/${{ matrix.variant }}
114+
115+
- name: Download arm64 digest
116+
uses: actions/download-artifact@v4
117+
with:
118+
name: digests-${{ matrix.variant }}-arm64
119+
path: /tmp/digests/${{ matrix.variant }}
120+
121+
- name: Set up Docker Buildx
122+
uses: docker/setup-buildx-action@v3
123+
124+
- name: Log in to Docker Hub
125+
uses: docker/login-action@v3
126+
with:
127+
registry: ${{ env.REGISTRY }}
128+
username: ${{ secrets.DOCKERHUB_USERNAME }}
129+
password: ${{ secrets.DOCKERHUB_TOKEN }}
130+
131+
- name: Extract Node.js version
132+
id: node-version
133+
run: |
134+
# Get one of the digests to pull and extract version
135+
digest=$(ls /tmp/digests/${{ matrix.variant }}/ | head -1)
136+
137+
# Pull the image by digest
138+
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:${digest}
139+
140+
# Extract Node.js version from the image
141+
NODE_VERSION=$(docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:${digest} node --version | sed 's/^v//')
142+
echo "version=${NODE_VERSION}" >> $GITHUB_OUTPUT
143+
echo "Node.js version: ${NODE_VERSION}"
144+
145+
- name: Create manifest list and push
146+
working-directory: /tmp/digests/${{ matrix.variant }}
147+
run: |
148+
NODE_VERSION="${{ steps.node-version.outputs.version }}"
149+
VARIANT="${{ matrix.variant }}"
150+
151+
# Build the list of image references from digests
152+
DIGESTS=$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
153+
154+
# Tags to create
155+
TAGS=""
156+
157+
# Always add version-variant tag (e.g., 25.6.1-bookworm)
158+
TAGS="$TAGS -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${NODE_VERSION}-${VARIANT}"
159+
160+
# Always add variant-only tag (e.g., bookworm, slim, alpine)
161+
TAGS="$TAGS -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VARIANT}"
162+
163+
# For bookworm (default variant), also add version-only and latest tags
164+
if [ "$VARIANT" = "bookworm" ]; then
165+
TAGS="$TAGS -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${NODE_VERSION}"
166+
TAGS="$TAGS -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
167+
fi
168+
169+
# Create and push the manifest
170+
docker buildx imagetools create $TAGS $DIGESTS
171+
172+
- name: Inspect manifest
173+
run: |
174+
NODE_VERSION="${{ steps.node-version.outputs.version }}"
175+
echo "Inspecting manifest for ${{ matrix.variant }}..."
176+
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.variant }}
177+
178+
# =============================================================================
179+
# Phase 3: Test the published images
180+
# =============================================================================
181+
test:
182+
runs-on: ${{ matrix.runner }}
183+
if: github.event_name != 'pull_request'
184+
needs: merge
185+
strategy:
186+
matrix:
187+
include:
188+
# Bookworm tests
189+
- variant: bookworm
190+
arch: amd64
191+
runner: ubuntu-latest
192+
- variant: bookworm
193+
arch: arm64
194+
runner: ubuntu-24.04-arm
195+
# Slim tests
196+
- variant: slim
197+
arch: amd64
198+
runner: ubuntu-latest
199+
- variant: slim
200+
arch: arm64
201+
runner: ubuntu-24.04-arm
202+
# Alpine tests
203+
- variant: alpine
204+
arch: amd64
205+
runner: ubuntu-latest
206+
- variant: alpine
207+
arch: arm64
208+
runner: ubuntu-24.04-arm
209+
210+
steps:
211+
- name: Checkout repository
212+
uses: actions/checkout@v4
213+
214+
- name: Pull and test image
215+
run: |
216+
echo "Testing ${{ matrix.variant }} on ${{ matrix.arch }}..."
217+
218+
# Pull the image
219+
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.variant }}
220+
221+
# Verify Node.js version
222+
docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.variant }} node --version
223+
224+
# Run pointer compression verification test
225+
docker run --rm \
226+
-v ${{ github.workspace }}/tests:/tests:ro \
227+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.variant }} \
228+
node /tests/verify-pointer-compression.js

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,46 @@
11
# Node.js Pointer Compression Experiments
22

3+
[![Build and Publish Docker Images](https://github.com/platformatic/pointer-compression-experiments/actions/workflows/build-publish.yml/badge.svg)](https://github.com/platformatic/pointer-compression-experiments/actions/workflows/build-publish.yml)
4+
[![Docker Pulls](https://img.shields.io/docker/pulls/platformatic/node-pointer-compression)](https://hub.docker.com/r/platformatic/node-pointer-compression)
5+
36
This repository contains experiments for building Node.js with V8 pointer compression enabled. Pointer compression is a V8 optimization that reduces memory usage by using 32-bit compressed pointers instead of full 64-bit pointers.
47

8+
## Docker Images
9+
10+
Pre-built multi-architecture images (amd64/arm64) are available on DockerHub:
11+
12+
```bash
13+
# Pull the latest image (Debian bookworm, recommended)
14+
docker pull platformatic/node-pointer-compression:latest
15+
16+
# Or use a specific variant
17+
docker pull platformatic/node-pointer-compression:bookworm # Full Debian
18+
docker pull platformatic/node-pointer-compression:slim # Minimal Debian
19+
docker pull platformatic/node-pointer-compression:alpine # Alpine Linux (experimental)
20+
21+
# Pin to a specific Node.js version
22+
docker pull platformatic/node-pointer-compression:25.6.1
23+
docker pull platformatic/node-pointer-compression:25.6.1-slim
24+
```
25+
26+
### Available Tags
27+
28+
| Tag | Description |
29+
|-----|-------------|
30+
| `latest`, `bookworm` | Latest build on Debian bookworm (recommended) |
31+
| `slim` | Minimal Debian bookworm-slim runtime (~100MB smaller) |
32+
| `alpine` | Alpine Linux with musl libc (smallest, experimental) |
33+
| `{version}` | Specific Node.js version on bookworm (e.g., `25.6.1`) |
34+
| `{version}-{variant}` | Specific version and variant (e.g., `25.6.1-alpine`) |
35+
36+
### Variant Comparison
37+
38+
| Variant | Base Image | Size | Compatibility |
39+
|---------|-----------|------|---------------|
40+
| `bookworm` | debian:bookworm | ~250MB | Full glibc, best compatibility |
41+
| `slim` | debian:bookworm-slim | ~150MB | Minimal glibc runtime |
42+
| `alpine` | alpine:3.21 | ~100MB | musl libc, experimental |
43+
544
## Quick Start
645

746
Build the Docker image:
@@ -44,3 +83,40 @@ The Dockerfile builds Node.js from the v25.x branch with the `--experimental-ena
4483

4584
- `tests/verify-pointer-compression.js` - Verifies pointer compression is enabled by checking heap limits
4685
- `tests/memory-benchmark.js` - Benchmarks memory usage with pointer-heavy data structures
86+
87+
## Building Locally
88+
89+
For local development, use the root Dockerfile:
90+
91+
```bash
92+
# Build for local architecture
93+
docker build --network=host -t node-pointer-compression .
94+
95+
# Run interactively
96+
docker run -it node-pointer-compression
97+
98+
# Run a script
99+
docker run -v $(pwd):/app node-pointer-compression node /app/your-script.js
100+
```
101+
102+
To build a specific variant locally:
103+
104+
```bash
105+
# Build bookworm variant
106+
docker build -f docker/bookworm/Dockerfile -t node-pointer-compression:bookworm .
107+
108+
# Build slim variant
109+
docker build -f docker/slim/Dockerfile -t node-pointer-compression:slim .
110+
111+
# Build alpine variant
112+
docker build -f docker/alpine/Dockerfile -t node-pointer-compression:alpine .
113+
```
114+
115+
## CI/CD
116+
117+
The GitHub Actions workflow automatically builds and publishes multi-architecture images on:
118+
- Push to `main` branch
119+
- Tagged releases (`v*`)
120+
- Manual workflow dispatch
121+
122+
Images are built natively on both amd64 and arm64 runners for optimal build performance.

docker/alpine/Dockerfile

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Dockerfile for Node.js with pointer compression - Alpine variant
2+
# Uses musl libc instead of glibc - smallest image but experimental
3+
#
4+
# WARNING: This is an experimental build. The combination of musl libc
5+
# and V8 pointer compression is not extensively tested.
6+
7+
FROM alpine:3.21 AS builder
8+
9+
# Install build dependencies
10+
# Alpine uses musl libc and different package names
11+
RUN apk add --no-cache \
12+
build-base \
13+
git \
14+
python3 \
15+
curl \
16+
linux-headers \
17+
openssl-dev \
18+
ccache \
19+
bash \
20+
procps
21+
22+
# Set working directory for build
23+
WORKDIR /build
24+
25+
# Clone Node.js repository (shallow clone for faster build)
26+
RUN git clone --depth 1 --branch v25.x https://github.com/nodejs/node.git .
27+
28+
# Configure with pointer compression enabled
29+
# Note: Some configure options may differ on Alpine/musl
30+
RUN ./configure \
31+
--experimental-enable-pointer-compression \
32+
--prefix=/usr/local
33+
34+
# Build Node.js using all available cores
35+
# V8 may have warnings with musl, so we allow them
36+
RUN make -j$(nproc)
37+
38+
# Install to a clean directory for copying
39+
RUN make install DESTDIR=/node-install
40+
41+
# =============================================================================
42+
# Stage 2: Runtime image
43+
# =============================================================================
44+
FROM alpine:3.21
45+
46+
# Install only runtime dependencies
47+
RUN apk add --no-cache \
48+
ca-certificates \
49+
libssl3 \
50+
libstdc++ \
51+
libgcc
52+
53+
# Copy Node.js installation from builder
54+
COPY --from=builder /node-install/usr/local /usr/local
55+
56+
# Verify installation
57+
RUN node --version && npm --version
58+
59+
# Set working directory for running applications
60+
WORKDIR /app
61+
62+
# Default command
63+
CMD ["node"]

0 commit comments

Comments
 (0)