| |

Docker 22 🐳 Multi-Stage Pattern: Building Standalone Go, Rust, or Node.js Binaries for Distroless/Scratch

A compiled binary is a promise. It says: everything this program needs is inside this file. No interpreter, no package manager, no shared library. Just instructions the kernel can execute directly. Go and Rust make this promise readily—with the right build flags. Node.js can approximate it with newer tooling that embeds the runtime. Docker’s multi-stage builds turn the promise into an image that contains nothing else: a single binary on a filesystem so empty it has no shell.

This chapter covers the pattern for building standalone binaries and shipping them in scratch or distroless images. You will see the build flags that make a binary static, the base image choices and their trade-offs, and the specific requirements for each language. The result is an image measured in single-digit megabytes that starts in milliseconds and presents almost nothing to an attacker who gains a foothold.

Key point: scratch is an empty filesystem. It contains no shell, no libc, no certificate store, no timezone data, nothing. A binary shipped on scratch must be completely static—no dynamic library dependencies at all. Distroless images are not empty; they include CA certificates, timezone data, and a non-root user, but no shell or package manager.


Why standalone binaries exist

The runtime dependency problem. Most language runtimes—Node.js, Python, Java—require an interpreter to execute. The interpreter must be present in the image, along with its standard library and system dependencies. A Node.js image is at minimum 100–200 MB even before application code is added . Go and Rust compile to machine code, eliminating the interpreter entirely. The resulting binary is the program.

The dynamic linking problem. A binary that links against glibc or libstdc++ at runtime requires those libraries to be present in the image. A Debian base with glibc is around 74 MB. Alpine with musl is around 7 MB, but still larger than empty . Static linking removes the dependency. The binary carries the code it needs, and the image can be empty.

The attack surface problem. Every binary in an image is a potential vulnerability. A shell allows an attacker who gains code execution to run commands, inspect the filesystem, and pivot. curl allows data exfiltration. ls reveals what is present. Removing them does not prevent the initial compromise, but it makes exploitation dramatically harder. Distroless images strip the shell, package manager, and all system utilities .

The performance problem. Image size affects pull time, which affects deployment speed. A 5 MB image pulls in seconds; a 1 GB image can take minutes on cold nodes. In Kubernetes, where pods are rescheduled frequently, the difference compounds. Small images also consume less disk and memory in the registry and on nodes.

The reproducibility problem. A build that runs in a full SDK image and copies artifacts to a minimal image is reproducible in a way that a build on a developer’s laptop is not. The SDK version is pinned in the Dockerfile. The build flags are explicit. The runtime image is declared. The result is deterministic.


a. Building static Go binaries

Go’s standard library includes pure-Go implementations of DNS resolution and user lookup. When CGO_ENABLED=0 is set, the compiler uses these pure-Go paths instead of calling into libc . The resulting binary is fully static—it depends on nothing at runtime except the Linux kernel.

FROM golang:1.25-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o /server ./cmd/server

The build flags do specific work. CGO_ENABLED=0 forces static linking. GOOS=linux ensures a Linux binary even when building on macOS or Windows. -ldflags="-s -w" strips the symbol table (-s) and DWARF debug information (-w), reducing binary size by 20–30% . -trimpath removes absolute paths from the binary for reproducibility .

For the runtime stage, scratch produces the smallest possible image:

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /server /server
USER 65534:65534
ENTRYPOINT ["/server"]

The CA certificates are required for outbound HTTPS. Without them, TLS handshakes fail with opaque errors. The timezone data is required for time.LoadLocation. Both are copied from the build stage because scratch contains neither. The USER directive uses a numeric UID because scratch has no /etc/passwd for name resolution .

Distroless static-debian12 is the pragmatic alternative:

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

The distroless static image includes CA certificates, timezone data, and a nonroot user by default. It is approximately 2 MB compressed . The image is larger than a bare scratch image but requires no manual file copying and provides a named non-root user.

A Go binary built this way lands between 5 and 15 MB depending on the application and dependencies . The golang:1.26 base image is hundreds of megabytes; the final image is a fraction of that.


b. Building static Rust binaries

Rust’s standard build links against glibc on Linux. A glibc-linked binary will not run on scratch or distroless/static because those images have no libc. The solution is musl, a lightweight libc designed for static linking.

The clux/muslrust image provides a build environment with musl-gcc and the necessary tooling pre-installed . A typical build:

FROM clux/muslrust:stable AS builder
WORKDIR /volume
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
    cargo build --release --target x86_64-unknown-linux-musl && \
    rm -rf src
COPY src ./src
RUN cargo build --release --target x86_64-unknown-linux-musl && \
    strip target/x86_64-unknown-linux-musl/release/app

The dummy source file builds dependencies before the real source is copied. This caches the dependency compilation layer; when only application code changes, dependencies are not recompiled . The strip command removes debug symbols from the binary, reducing size by up to 47% .

The runtime stage is the same as Go:

FROM scratch
COPY --from=builder /volume/target/x86_64-unknown-linux-musl/release/app /app
USER 65534:65534
ENTRYPOINT ["/app"]

Rust binaries are larger than Go binaries by default. A simple Rust HTTP service might be 15 MB unstripped, 8 MB stripped . Adding [profile.release] settings to Cargo.toml—lto = true, codegen-units = 1, panic = "abort", opt-level = "z"—reduces size further at the cost of longer build times .

The muslrust image includes some C libraries compiled against musl-gcc, such as SQLite and zlib, enabling static builds when those libraries are used . OpenSSL, curl, and PostgreSQL have been removed from recent versions; projects needing them should use blackdex/rust-musl or similar alternatives .

The muslrust project explicitly recommends distroless static or chainguard static over scratch for production, because those images include non-root users and SSL certificates without manual copying .


c. Building standalone Node.js binaries

Node.js cannot be statically linked in the traditional sense. The runtime depends on libc, libstdc++, and several shared libraries. A fully static Node.js binary is possible to build but not commonly distributed . The modern approach is Node.js Single Executable Applications (SEA), stabilized in recent Node.js versions.

SEA bundles your JavaScript code and its dependencies into a single file using esbuild, then injects that blob into a copy of the Node.js executable using postject . The result is a binary that contains both the runtime and your application. It is not static—it still depends on the host’s libc and libstdc++—but it is self-contained in the sense that no node_modules directory or source files are needed at runtime.

The fossilize tool automates the SEA build process across platforms . A Dockerfile using Node SEA:

FROM node:22 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
RUN npx fossilize --output /app/server

FROM gcr.io/distroless/nodejs22-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

The final image uses distroless Node.js rather than scratch or distroless/static. The Node.js binary requires libc and libstdc++, which distroless/static does not provide. The nodejs22-debian12 distroless image includes the Node.js runtime dependencies but no shell or package manager . The image is approximately 130 MB—larger than Go or Rust images, but smaller than a full node:22 image and with a reduced attack surface .

A Node SEA binary can run on scratch only if it is built against musl and statically linked. The caxa tool supports bundling a musl-linked Node.js binary for Alpine environments, but the process requires downloading a precompiled musl Node.js tarball and ensuring all shared library dependencies are satisfied . For most production use cases, distroless Node.js is the practical choice.

The trade-off is clear. Go and Rust produce binaries that run on empty images because they have no runtime dependencies. Node.js carries its runtime with it, and that runtime has dependencies. SEA reduces the image by eliminating node_modules and source code, but it cannot eliminate libc.


Complete Example Session

# ============================================
# PART 1: GO STATIC BINARY ON SCRATCH
# ============================================
FROM golang:1.25-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o /server ./cmd/server

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /server /server
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/server"]
# ============================================
# PART 2: GO ON DISTROLESS
# ============================================
FROM golang:1.25 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
# ============================================
# PART 3: RUST STATIC BINARY ON SCRATCH
# ============================================
FROM clux/muslrust:stable AS builder
WORKDIR /volume
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
    cargo build --release --target x86_64-unknown-linux-musl && \
    rm -rf src
COPY src ./src
RUN cargo build --release --target x86_64-unknown-linux-musl && \
    strip target/x86_64-unknown-linux-musl/release/app

FROM scratch
COPY --from=builder /volume/target/x86_64-unknown-linux-musl/release/app /app
USER 65534:65534
ENTRYPOINT ["/app"]
# ============================================
# PART 4: RUST WITH DEPENDENCY CACHING
# ============================================
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    mkdir src && echo "fn main() {}" > src/main.rs && \
    cargo build --release --target x86_64-unknown-linux-musl && \
    rm -rf src
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release --target x86_64-unknown-linux-musl && \
    strip target/x86_64-unknown-linux-musl/release/app

FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app
USER 1000:1000
ENTRYPOINT ["/app"]
# ============================================
# PART 5: NODE SEA ON DISTROLESS
# ============================================
FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npx fossilize --output /app/server

FROM gcr.io/distroless/nodejs22-debian12
COPY --from=builder /app/server /server
USER nonroot
ENTRYPOINT ["/server"]
# ============================================
# PART 6: SIZE COMPARISON
# ============================================
# Single-stage Go:        ~800 MB
# Multi-stage Go scratch:  ~10 MB
# Multi-stage Rust scratch: ~8 MB
# Node SEA distroless:     ~130 MB
# Full node:22:            ~1.1 GB
# ============================================
# PART 7: VERIFYING STATIC LINKING
# ============================================
# ldd target/x86_64-unknown-linux-musl/release/app
#         not a dynamic executable
#
# file server
# server: ELF 64-bit LSB executable, statically linked
# ============================================
# PART 8: BUILDING AND MEASURING
# ============================================
# docker build -t myapp:go .
# docker images myapp:go --format '{{.Size}}'
# → 9.2MB
#
# docker build -t myapp:rust .
# docker images myapp:rust --format '{{.Size}}'
# → 7.8MB
# ============================================
# PART 9: NON-ROOT USER IN SCRATCH
# ============================================
FROM scratch
COPY --from=builder /server /server
USER 65534:65534
ENTRYPOINT ["/server"]
# No /etc/passwd, so numeric UID required
# 65534 is the traditional "nobody" UID
# ============================================
# PART 10: MULTI-ARCH BUILD
# ============================================
# docker buildx build --platform linux/amd64,linux/arm64 \
#   -t myapp:multi .

The ten parts covered Go static builds, Go on distroless, Rust static builds, Rust with caching, Node SEA, size comparison, verifying static linking, measuring, non-root in scratch, and multi-arch builds.


Quick Reference

Language Build Flags

LanguageStatic Build FlagSize Reduction
GoCGO_ENABLED=0Required for scratch
Go-ldflags="-s -w"20–30% smaller
Go-trimpathReproducibility
Rust--target x86_64-unknown-linux-muslRequired for scratch
Ruststrip47% smaller
Nodefossilize or SEASelf-contained, not static

Base Image Selection

LanguageRecommended BaseSizeReason
Goscratch0 MBFully static
Godistroless/static~2 MBCA certs, nonroot
Rustscratch0 MBFully static
Rustdistroless/static~2 MBCA certs, nonroot
Nodedistroless/nodejs~130 MBRuntime required
NodescratchN/ANot viable without static Node

Required Files on Scratch

FilePurposeSource
BinaryThe applicationBuild stage
ca-certificates.crtOutbound HTTPSBuild stage
zoneinfo/Timezone dataBuild stage
/etc/passwdUser lookupNot needed (use numeric UID)

Distroless Image Sizes

ImageSizeUse Case
distroless/static~2 MBGo, Rust static
distroless/base~20 MBCGO, glibc
distroless/cc~25 MBC/C++
distroless/nodejs22~130 MBNode.js
distroless/python3~50 MBPython

Best Practices

✅ Do This:

# Force static linking for Go
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server      # ✅

# Strip Rust binaries
RUN cargo build --release && strip target/release/app        # ✅

# Copy CA certificates to scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ # ✅

# Use numeric UID on scratch
USER 65534:65534                                              # ✅

# Use distroless for Node.js
FROM gcr.io/distroless/nodejs22-debian12                      # ✅

# Cache dependencies before copying source
COPY go.mod go.sum ./
RUN go mod download                                           # ✅

# Verify static linking
RUN ldd /server && echo "not static" || echo "static"         # ✅

❌ Don’t Do This:

# Don't use scratch without CGO_ENABLED=0 for Go
FROM scratch                                                  # ❌ (binary won't run)

# Don't forget CA certificates for HTTPS
FROM scratch
COPY --from=builder /server /server                           # ❌ (TLS fails)

# Don't use scratch for Node.js
FROM scratch                                                  # ❌ (libc required)

# Don't skip stripping
RUN go build -o /server                                       # ❌ (larger binary)

# Don't use named users on scratch
USER appuser                                                  # ❌ (no /etc/passwd)

# Don't copy entire build stage
COPY --from=builder /app /app                                 # ❌ (toolchain included)

Common Pitfalls

PitfallWhy It HappensFix
Binary not foundscratch has no shellUse exec-form ENTRYPOINT
TLS handshake failsMissing CA certificatesCopy ca-certificates.crt
Timezone errorsMissing zoneinfoCopy /usr/share/zoneinfo
User lookup failsNo /etc/passwdUse numeric UID
Rust binary won’t runLinked against glibcBuild with musl target
Go binary won’t runCGO enabledCGO_ENABLED=0
Node SEA won’t startMissing libcUse distroless Node.js
Image still largeCopied build stageCopy only binary

Real-World Examples

1. Go HTTP Server on Scratch

FROM golang:1.25 AS builder
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /server /server
ENTRYPOINT ["/server"]

2. Go on Distroless

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

3. Rust on Scratch

FROM clux/muslrust:stable AS builder
RUN cargo build --release --target x86_64-unknown-linux-musl
RUN strip target/x86_64-unknown-linux-musl/release/app
FROM scratch
COPY --from=builder /volume/target/x86_64-unknown-linux-musl/release/app /app
ENTRYPOINT ["/app"]

4. Rust with LTO

[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true

5. Node SEA

RUN npx fossilize --output /app/server
FROM gcr.io/distroless/nodejs22-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

6. Multi-Arch Go

ARG TARGETARCH
RUN CGO_ENABLED=0 GOARCH=${TARGETARCH} go build -o /server

7. Debug Distroless

FROM gcr.io/distroless/static-debian12:debug
# Includes busybox shell for troubleshooting

8. Go with Build Tags

RUN CGO_ENABLED=0 go build -tags netgo,osusergo -o /server

9. Rust Dependency Caching

RUN --mount=type=cache,target=/usr/local/cargo/registry \
    cargo build --release

10. Verify Static

ldd target/release/app
# not a dynamic executable

Visual

Image Size Progression

┌─────────────────────────────────────────────────────────────┐
│  GO IMAGE SIZE PROGRESSION                                  │
│                                                             │
│  FROM golang:1.25                    ~800 MB                │
│  ├── Go toolchain                                           │
│  ├── Standard library source                                │
│  ├── Debian base                                            │
│  └── Application binary                                     │
│                                                             │
│  Multi-stage + alpine                ~15 MB                 │
│  ├── Alpine base (5 MB)                                     │
│  ├── CA certificates                                        │
│  └── Application binary                                     │
│                                                             │
│  Multi-stage + distroless/static     ~2 MB base + binary    │
│  ├── CA certificates                                        │
│  ├── Timezone data                                          │
│  ├── Nonroot user                                           │
│  └── Application binary                                     │
│                                                             │
│  Multi-stage + scratch               binary only            │
│  └── Application binary                                     │
│                                                             │
│  Binary size (stripped): 5–15 MB                            │
│  Final image: 5–15 MB                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

CGO_ENABLED=0 Effect

┌─────────────────────────────────────────────────────────────┐
│  CGO_ENABLED=1 (DEFAULT)                                    │
│                                                             │
│  net.Lookup* ──▶ getaddrinfo (libc)                         │
│  os/user     ──▶ getpwuid (libc)                            │
│                                                             │
│  Binary: dynamically linked                                 │
│  Requires: libc at runtime                                  │
│  Runs on: glibc images (Debian, Ubuntu)                     │
│  Does NOT run on: scratch, distroless/static                │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CGO_ENABLED=0                                              │
│                                                             │
│  net.Lookup* ──▶ Pure-Go DNS resolver                       │
│  os/user     ──▶ Parses /etc/passwd                         │
│                                                             │
│  Binary: statically linked                                  │
│  Requires: nothing at runtime                               │
│  Runs on: scratch, distroless/static, any Linux             │
│                                                             │
│  Reads /etc/resolv.conf for DNS configuration.              │
│  For Kubernetes pods, this is the cluster DNS.              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Scratch Requirements

┌─────────────────────────────────────────────────────────────┐
│  WHAT SCRATCH CONTAINS                                      │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  (empty)                                            │    │
│  │                                                     │    │
│  │  No /bin/sh                                         │    │
│  │  No /bin/ls                                         │    │
│  │  No /etc/passwd                                     │    │
│  │  No libc                                            │    │
│  │  No CA certificates                                 │    │
│  │  No timezone data                                   │    │
│  │                                                     │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  WHAT YOU MUST ADD                                          │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  /server               (the binary)                 │    │
│  │  /etc/ssl/certs/       (if HTTPS outbound)          │    │
│  │  /usr/share/zoneinfo/  (if time.LoadLocation)       │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  USER 65534:65534  (numeric, no /etc/passwd)                │
│  ENTRYPOINT ["/server"]  (exec form, no shell)              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Distroless vs Scratch

┌─────────────────────────────────────────────────────────────┐
│  SCRATCH                        DISTROLESS STATIC           │
│                                                             │
│  ┌─────────────────────┐       ┌─────────────────────┐      │
│  │ Empty filesystem    │       │ Debian base         │      │
│  │                     │       │                     │      │
│  │ + binary            │       │ + CA certs          │      │
│  │ + CA certs (manual) │       │ + tzdata            │      │
│  │ + tzdata (manual)   │       │ + nonroot user      │      │
│  │                     │       │ + binary            │      │
│  │ No shell            │       │                     │      │
│  │ No package manager  │       │ No shell            │      │
│  │ No users            │       │ No package manager  │      │
│  └─────────────────────┘       └─────────────────────┘      │
│                                                             │
│  Size: binary only              Size: ~2 MB + binary        │
│  Setup: manual                  Setup: copy binary          │
│  Debug: impossible              Debug: use :debug tag       │
│                                                             │
│  distroless is the pragmatic default for most services.     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Go static flagCGO_ENABLED=0
Go strip flag-ldflags="-s -w"
Rust static targetx86_64-unknown-linux-musl
Rust stripstrip command or Cargo.toml
Node standaloneNode SEA / fossilize
Scratch baseEmpty filesystem
Distroless static~2 MB, CA certs, nonroot
Distroless Node~130 MB, runtime included
Required on scratchCA certs, tzdata
Non-root on scratchNumeric UID
Go image size5–15 MB
Rust image size5–10 MB
Node image size~130 MB

Key takeaways:

  • Go and Rust compile to static binaries with the right flags. CGO_ENABLED=0 for Go forces pure-Go implementations of DNS and user lookup, eliminating libc. Rust uses the musl target to link statically. Both produce binaries that run on scratch.
  • scratch is an empty filesystem. The only things in the final image are what you COPY into it. CA certificates and timezone data must be added manually if the binary needs them.
  • Distroless is the pragmatic default. The static-debian12:nonroot image includes CA certificates, timezone data, and a nonroot user. It is 2 MB compressed and eliminates the manual copying that scratch requires.
  • Node.js cannot be fully static. The runtime depends on libc. Node SEA bundles the application with the runtime into a single binary, but the binary still needs libc and libstdc++. Distroless Node.js provides these without a shell.
  • Strip your binaries. -ldflags="-s -w" for Go and strip for Rust remove debug symbols, reducing size by 20–47%. The trade-off is losing symbol-rich debugging against production binaries.
  • Numeric UIDs work on scratch. Since there is no /etc/passwd, named users cannot be resolved. USER 65534:65534 runs as the traditional “nobody” user without any user database.
  • CGO affects more than linking. When CGO_ENABLED=0, Go uses its pure-Go DNS resolver, which reads /etc/resolv.conf directly. This is the correct behavior in Kubernetes, where cluster DNS configuration lands in that file.
  • The size reduction is dramatic. A Go image drops from ~800 MB to 5–15 MB. A Rust image lands in the same range. A Node SEA image is ~130 MB—larger, but still a fraction of a full node:22 image and with no shell or package manager.

Remember: A standalone binary is the ideal container artifact. It has no runtime dependencies, no package manager, no shell, and no utilities. The image is the binary, plus whatever data files it genuinely needs. Go and Rust achieve this with build flags. Node.js approximates it with SEA, though the runtime dependency on libc means distroless rather than scratch. The pattern is always the same: build in a full SDK image, copy only the artifact to a minimal base. The final image starts in milliseconds, pulls in seconds, and presents almost nothing to an attacker. The size is a symptom; the reduction in attack surface is the point.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!