Docker 23 🐳 Minimal Base Images Comparison: alpine, debian-slim, scratch, and distroless
The base image is the first decision in any Dockerfile, and it is the one that is hardest to reverse. It determines the C library, the package manager, the shell, the attack surface, and the fundamental compatibility of everything that follows. A choice made casually in a FROM line ripples through every build, every scan, and every deployment. The four images compared here—alpine, debian-slim, scratch, and distroless—represent four distinct philosophies about what an operating system should be in a container. None is universally correct. Each is the right answer for a specific set of constraints.
This chapter compares these four base images across size, libc, package management, shell availability, CVE surface, and operational trade-offs. The goal is not to declare a winner but to make the decision explicit: given a language, a compatibility requirement, and a security posture, which base image fits?
Key point: scratch is empty. distroless is almost empty—no shell, no package manager, just the runtime. alpine is a tiny complete distribution with musl libc. debian-slim is a conventional distribution with glibc, stripped of documentation and development files. The libc choice (musl vs glibc) is the most consequential difference between alpine and everything else.
Why the base image choice matters
The size problem. Image size affects pull time, which affects deployment speed and cold-start latency. A 5 MB Alpine image pulls in seconds; a 75 MB Debian image takes longer. In Kubernetes, where pods are rescheduled and scaled frequently, the difference compounds. Small images also consume less disk in the registry and on nodes. But size is a symptom, not the goal. The goal is a minimal set of responsibilities.
The compatibility problem. Alpine uses musl libc instead of glibc. Most software compiles and runs against musl without issue, but precompiled binaries linked against glibc will not run. Python wheels are the most common casualty: packages without musllinux builds fall back to compiling from source, turning a fast build into a slow one . Java, Node.js, Go, and Rust are generally unaffected. Python data stacks and anything linking against Oracle or proprietary C libraries should be tested before committing to Alpine.
The security problem. Every package in a base image is a potential vulnerability. Ubuntu ships around 100 packages; Alpine ships about 15; distroless static ships effectively three (CA certificates, timezone data, and base files) . CVE exposure tracks package count almost linearly. Distroless images have no shell and no package manager, which does not prevent the initial compromise but makes post-exploitation significantly harder: no /bin/sh to pivot into, no curl to download a second stage, no apt to install tools . Alpine has a shell and apk, which is useful for debugging but also available to an attacker.
The operational problem. Distroless images cannot be debugged interactively. There is no shell to docker exec into. Debugging requires either a :debug variant (which includes busybox) or a temporary sidecar container in the same namespace . Alpine can be shelled into and packages can be installed for troubleshooting. debian-slim is a conventional Debian system with apt and bash. The trade-off is between security and operability, and it is real.
The build problem. Some applications require build tools at runtime—Python packages that compile C extensions, applications that shell out to system utilities. scratch and distroless cannot satisfy these requirements. Alpine and debian-slim can. The base image choice determines what is possible at runtime, not just what is present at build time.
a. Alpine: small, complete, musl
Alpine Linux is a security-oriented distribution built on musl libc and busybox. The official Docker image is approximately 5 MB compressed, making it the smallest base image that still provides a shell and a package manager . It is a complete, if minimal, Linux distribution.
FROM alpine:3.22
RUN apk add --no-cache python3 py3-pip
COPY . /app
WORKDIR /app
CMD ["python3", "app.py"]
The apk package manager uses a different package database than apt or dnf. Packages are named differently and versions are pinned to Alpine releases. The --no-cache flag avoids storing the package index in the image layer, keeping the image small.
Alpine’s greatest strength is its size-to-completeness ratio. It is 5 MB with a shell, apk, and busybox utilities. For Go, Rust, and most Node.js workloads, this is the pragmatic default when you want a small image without sacrificing debuggability .
Its greatest weakness is musl. The musl resolver historically handled DNS search domains and TCP fallback differently than glibc, which manifested as flaky service discovery in Kubernetes. Most of this was fixed by musl 1.2.4 (Alpine 3.18+), but the reputation persists . Python packages with C extensions often need build-base installed, and precompiled wheels for glibc will not work. For JVM workloads, Alpine is generally fine but should be tested. For anything that links against proprietary or precompiled glibc libraries, Alpine is the wrong choice.
The security track record has one notable scar: CVE-2019-5021 (CVSS 9.8), which shipped a blank root password in /etc/shadow across Alpine images from 2015 to 2019 . It was fixed promptly, but it illustrates that a small security team can miss serious issues. Alpine’s CVE count is low because its package count is low, but zero-CVE scans measure patch velocity, not invulnerability .
b. Debian-slim: conventional, compatible, larger
debian-slim is the Debian base image with documentation, man pages, and some development files removed. The compressed size is approximately 29–75 MB depending on the release and architecture . It uses glibc, which means every precompiled binary that targets Linux will run.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app
CMD ["python3", "app.py"]
The --no-install-recommends flag prevents apt from installing optional dependencies that are not strictly required. The rm -rf /var/lib/apt/lists/* removes the package index in the same layer as the install, keeping the image as small as possible . Without this cleanup, the index files persist in the layer and add tens of megabytes.
debian-slim is the correct choice when glibc compatibility is non-negotiable. Python data stacks with precompiled wheels, Java applications that rely on glibc-specific behavior, and any workload that needs to link against a system library that is only available for glibc will work on debian-slim and may not work on Alpine. The cost is size: the image is roughly ten times larger than Alpine and twenty-five times larger than distroless static.
The CVE surface is correspondingly larger. A scan of debian:trixie-slim typically finds 9 or more CVEs, compared to zero for Alpine and distroless at scan time . Most of these are low or medium severity, and many are dormant, but they represent patching work and scanner noise that Alpine and distroless avoid.
For teams that value compatibility and familiarity over minimal size, debian-slim is the safe, unremarkable default. It behaves like the Linux environments developers already know.
c. Scratch: empty, static-only, minimal
scratch is not an image in the conventional sense. It is the reserved, empty filesystem that Docker uses as a starting point when you want the next instruction to be the first layer. You cannot pull it, run it, or tag it. You can only reference it in a FROM instruction .
FROM golang:1.25 AS builder
WORKDIR /src
COPY . .
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
USER 65534:65534
ENTRYPOINT ["/server"]
The final image contains exactly what is copied into it: the binary, the CA certificates, and nothing else. The size is the binary size, typically 5–15 MB for a Go application .
The constraints are absolute. The binary must be statically linked. Go requires CGO_ENABLED=0. Rust requires the musl target. C/C++ requires -static. If the binary has any dynamic library dependency, it will fail to start with a cryptic error about a missing shared object. There is no shell to debug with, no ldd to diagnose linking problems, no /etc/passwd to resolve user names . The USER directive must use a numeric UID because name resolution has no user database.
The moment you need TLS to an external service, you must copy CA certificates into the image. The moment you need timezone data, you must copy /usr/share/zoneinfo. At that point, you are essentially building a distroless image by hand, without the maintained update path . For simple, fully static binaries, scratch is the theoretical optimum. For anything that needs a certificate bundle or timezone data, distroless static does the same thing with less manual effort.
d. Distroless: no shell, no package manager, runtime only
Distroless images contain only the application and its runtime dependencies. They have no shell, no package manager, and no coreutils. The smallest variant, static-debian12, is approximately 2 MB compressed—about 50% of Alpine’s size and less than 2% of full Debian .
FROM golang:1.25 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
The static variant is for fully static binaries. It includes CA certificates, timezone data, and a nonroot user. The base variant adds glibc and libssl for dynamically linked binaries. The cc variant adds libgcc for C/C++. Language-specific variants exist for Java, Python, and Node.js .
Distroless is the pragmatic default for production services. It is small, has a minimal attack surface, and requires no manual copying of certificate bundles. The :nonroot tag runs as a non-root user by default; using it is a one-word change that closes a common security gap .
The trade-off is debuggability. There is no shell to docker exec into. When something goes wrong at runtime, you cannot inspect the filesystem or run diagnostic commands. The :debug variants include busybox for troubleshooting, but they are larger and should not be used in production . The recommended pattern is to deploy distroless in production and use a debug variant or an ephemeral sidecar for troubleshooting.
Patch cadence is tied to Debian point releases. A CVE in glibc can sit visible-but-unfixed in scanners for longer than it would with Chainguard or Ubuntu. This is a real operational consideration: a zero-CVE scan on the day you build does not mean zero CVEs a month later .
Complete Example Session
# ============================================
# PART 1: ALPINE BASE
# ============================================
FROM alpine:3.22
RUN apk add --no-cache python3 py3-pip
COPY . /app
WORKDIR /app
CMD ["python3", "app.py"]
# Size: ~50 MB with Python installed
# ============================================
# PART 2: DEBIAN-SLIM BASE
# ============================================
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app
CMD ["python3", "app.py"]
# Size: ~120 MB with Python installed
# ============================================
# PART 3: SCRATCH BASE (GO)
# ============================================
FROM golang:1.25 AS builder
WORKDIR /src
COPY . .
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
USER 65534:65534
ENTRYPOINT ["/server"]
# Size: ~10 MB
# ============================================
# PART 4: DISTROLESS STATIC
# ============================================
FROM golang:1.25 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
# Size: ~2 MB base + ~8 MB binary
# ============================================
# PART 5: DISTROLESS BASE (GLIBC)
# ============================================
FROM gcr.io/distroless/base-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
# Size: ~20 MB base + binary
# ============================================
# PART 6: COMPATIBILITY TEST
# ============================================
# ldd /server on scratch/distorless static
# → not a dynamic executable
#
# ldd /server on distroless base
# → libc.so.6, libssl.so, etc.
# ============================================
# PART 7: ALPINE PYTHON WHEEL TEST
# ============================================
# pip install numpy on alpine
# → may build from source (slow)
# pip install numpy on debian-slim
# → uses precompiled glibc wheel (fast)
# ============================================
# PART 8: DISTROLESS DEBUG
# ============================================
FROM gcr.io/distroless/static-debian12:debug
# Includes busybox shell for troubleshooting
# Use for debugging only, not production
# ============================================
# PART 9: SIZE MEASUREMENT
# ============================================
# docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}'
# alpine:3.22 8 MB
# debian:trixie-slim 75 MB
# distroless/static-debian12 2 MB
# scratch (with binary) 10 MB
# ============================================
# PART 10: CVE SCAN COMPARISON
# ============================================
# trivy image --severity HIGH,CRITICAL alpine:3.22
# → 0-3 CVEs
#
# trivy image --severity HIGH,CRITICAL debian:trixie-slim
# → 9+ CVEs
#
# trivy image --severity HIGH,CRITICAL \
# gcr.io/distroless/static-debian12
# → 0 CVEs (scannable surface near zero)
The ten parts covered Alpine with Python, Debian-slim with Python, scratch with Go, distroless static, distroless base, compatibility testing, Python wheel behavior, debug variants, size measurement, and CVE scanning.
Quick Reference
Base Image Comparison
| Image | Size | Shell | Package Manager | libc | Typical CVEs |
|---|---|---|---|---|---|
scratch | 0 MB | No | No | N/A | 0 |
distroless/static | ~2 MB | No | No | N/A | 0 |
distroless/base | ~20 MB | No | No | glibc | 0–1 |
alpine | ~5–8 MB | Yes (busybox) | apk | musl | 0–3 |
debian-slim | ~30–75 MB | Yes (bash) | apt | glibc | 9+ |
libc Compatibility
| Workload | Alpine (musl) | Debian/Ubuntu (glibc) |
|---|---|---|
| Go static binary | Fine | Fine |
| Rust static binary | Fine | Fine |
| Node.js | Fine | Fine |
| Java | Generally fine | Fine |
| Python (pure) | Fine | Fine |
| Python (C extensions) | May compile from source | Precompiled wheels |
| Precompiled glibc binaries | Will not run | Fine |
| Oracle/ML libraries | Often fail | Fine |
When to Choose Each
| Constraint | Recommended Base |
|---|---|
| Fully static binary, no certs needed | scratch |
| Static binary, needs CA certs/tzdata | distroless/static |
| Dynamically linked binary, glibc | distroless/base |
| Small image, need shell/debugging | alpine |
| glibc compatibility, conventional distro | debian-slim |
| Maximum compatibility, unfamiliar workload | debian-slim or ubuntu |
Operational Trade-offs
| Image | Debuggable | Rebuild Cadence | Manual Setup |
|---|---|---|---|
scratch | No | Your responsibility | CA certs, tzdata |
distroless | :debug variant | Debian point releases | None |
alpine | Yes (shell) | Alpine releases | None |
debian-slim | Yes (shell) | Debian releases | Cleanup in RUN |
Best Practices
✅ Do This:
# Use distroless/static for Go/Rust services
FROM gcr.io/distroless/static-debian12:nonroot # ✅
# Use :nonroot tag when available
USER nonroot:nonroot # ✅
# Clean apt cache in the same layer
RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* # ✅
# Test Alpine compatibility before committing
RUN pip install --no-cache-dir -r requirements.txt # ✅
# Use alpine for debugging-friendly minimal images
FROM alpine:3.22 # ✅
# Rebuild on a schedule, not just on release
# (base patches matter more than app code) # ✅
❌ Don’t Do This:
# Don't use scratch without static linking
FROM scratch
COPY --from=builder /server /server # ❌ (won't run if dynamic)
# Don't forget CA certificates on scratch
FROM scratch
COPY --from=builder /server /server # ❌ (TLS fails)
# Don't use alpine for Python data stacks without testing
FROM alpine:3.22
RUN pip install numpy scipy pandas # ❌ (compiles from source)
# Don't use named users on scratch
USER appuser # ❌ (no /etc/passwd)
# Don't use distroless without a debug strategy
FROM gcr.io/distroless/static-debian12 # ❌ (no shell for debugging)
# Don't separate apt install and cleanup
RUN apt-get update
RUN apt-get install -y python3 # ❌ (index persists)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Binary won’t run on scratch | Dynamic linking | CGO_ENABLED=0 or musl target |
| TLS handshake fails | Missing CA certificates | Copy ca-certificates.crt |
| Python install slow on Alpine | musl, no precompiled wheel | Use debian-slim |
| Image larger than expected | apt cache not cleaned | Combine install and cleanup |
| Can’t debug distroless | No shell | Use :debug variant |
| DNS flaky on Alpine | musl resolver differences | Test, use Alpine 3.18+ |
| Scanner noise on debian-slim | Many packages, many CVEs | Use distroless or Alpine |
Real-World Examples
1. Go Service on Distroless Static
FROM golang:1.25 AS builder
RUN CGO_ENABLED=0 go build -o /server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
2. Rust Service on Scratch
FROM clux/muslrust:stable AS builder
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /volume/target/x86_64-unknown-linux-musl/release/app /app
ENTRYPOINT ["/app"]
3. Python on Debian-Slim
FROM python:3.12-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
4. Node.js on Alpine
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]
5. Java on Distroless
FROM maven AS build
RUN mvn package -DskipTests
FROM gcr.io/distroless/java21-debian12:nonroot
COPY --from=build /target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
6. Alpine for Debugging
FROM alpine:3.22
RUN apk add --no-cache curl bash
# Shell available for troubleshooting
7. Distroless Debug
FROM gcr.io/distroless/static-debian12:debug
# Busybox shell available
8. Multi-Arch Go on Distroless
FROM --platform=$BUILDPLATFORM golang:1.25 AS builder
ARG TARGETARCH
RUN CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
9. Python C Extensions on Debian-Slim
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
10. Alpine with Compatibility Caveats
FROM alpine:3.22
RUN apk add --no-cache python3 py3-pip build-base
# Build-base for C extension compilation
Visual
Size and Capability Spectrum
┌─────────────────────────────────────────────────────────────┐
│ BASE IMAGE SPECTRUM │
│ │
│ scratch distroless alpine debian-slim│
│ 0 MB 2 MB 8 MB 75 MB │
│ │
│ ┌────┐ ┌────┐ ┌────┐ ┌──────┐ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ CA │ │apk │ │ apt │ │
│ │ │ │ tz │ │sh │ │ bash │ │
│ │bin │ │bin │ │musl│ │glibc │ │
│ │ │ │ │ │ │ │ │ │
│ └────┘ └────┘ └────┘ └──────┘ │
│ │
│ ◀── Less to attack, less to use ── More to attack, more to use ──▶
│ │
│ No shell No shell Shell Shell │
│ No pkg mgr No pkg mgr apk apt │
│ Static only Static/glibc musl glibc │
│ │
└─────────────────────────────────────────────────────────────┘
libc Decision Tree
┌─────────────────────────────────────────────────────────────┐
│ CHOOSING BETWEEN MUSL AND GLIBC │
│ │
│ Does the workload ship precompiled binaries? │
│ │ │
│ ├── YES ──▶ Need glibc ──▶ debian-slim or distroless/base│
│ │ │
│ └── NO │
│ │ │
│ ├── Is it a static binary (Go/Rust)? │
│ │ ├── YES ──▶ scratch or distroless/static │
│ │ └── NO │
│ │ │
│ ├── Does it compile from source at build time? │
│ │ ├── YES ──▶ Alpine is fine │
│ │ └── NO │
│ │ │
│ └── Python data stack? │
│ ├── YES ──▶ debian-slim (wheels) │
│ └── NO ──▶ Alpine is fine │
│ │
└─────────────────────────────────────────────────────────────┘
Attack Surface Comparison
┌─────────────────────────────────────────────────────────────┐
│ POST-EXPLOITATION CAPABILITIES │
│ │
│ scratch / distroless: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Attacker has code execution. │ │
│ │ Can: run the application's own code │ │
│ │ Cannot: spawn shell, download tools, inspect fs │ │
│ │ Pivot difficulty: HIGH │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ alpine: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Attacker has code execution. │ │
│ │ Can: spawn /bin/sh, use busybox, apk install │ │
│ │ Pivot difficulty: MEDIUM │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ debian-slim: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Attacker has code execution. │ │
│ │ Can: spawn bash, use coreutils, apt install │ │
│ │ Pivot difficulty: LOW │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
CVE Surface vs Package Count
┌─────────────────────────────────────────────────────────────┐
│ PACKAGE COUNT AND CVE EXPOSURE │
│ │
│ ubuntu:24.04 ~100 packages 15-40 CVEs │
│ debian:trixie-slim ~50 packages 9+ CVEs │
│ alpine:3.22 ~15 packages 0-3 CVEs │
│ distroless/static 3 packages 0 CVEs │
│ │
│ CVE exposure tracks package count almost linearly. │
│ Fewer packages = fewer places for a base-layer CVE. │
│ │
│ But: zero CVEs on scan day ≠ zero CVEs next month. │
│ Rebuild cadence matters more than the initial scan. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
scratch size | 0 MB |
distroless/static size | ~2 MB |
distroless/base size | ~20 MB |
alpine size | ~5–8 MB |
debian-slim size | ~30–75 MB |
scratch shell | None |
distroless shell | None (:debug variant has busybox) |
alpine shell | busybox |
debian-slim shell | bash |
alpine libc | musl |
debian-slim libc | glibc |
distroless/base libc | glibc |
scratch use case | Fully static binaries |
distroless use case | Production services |
alpine use case | Small images with debugging |
debian-slim use case | glibc compatibility |
Key takeaways:
scratchis empty, not minimal. It contains nothing. The binary must be fully static, and anything the binary needs at runtime—CA certificates, timezone data—must be copied in manually. The moment you need more than a binary,distroless/staticdoes the same thing with a maintained update path.- Distroless is the pragmatic production default. The
staticvariant is ~2 MB, includes CA certificates, timezone data, and a nonroot user, and has no shell or package manager. It is small enough to matter and complete enough to work without manual assembly. - Alpine is the smallest complete distribution. At ~5–8 MB, it has a shell, a package manager, and enough utilities to debug. Its musl libc is the primary compatibility risk. Go, Rust, and Node.js are generally fine; Python C extensions and precompiled glibc binaries are not.
debian-slimis the compatibility default. It uses glibc, hasaptandbash, and runs everything. The cost is size—roughly ten times Alpine—and a larger CVE surface. It is the right choice when glibc is non-negotiable.- The libc choice is the most consequential difference. musl vs glibc affects DNS resolution, precompiled wheel availability, and binary compatibility. Test before committing to Alpine for anything that ships precompiled artifacts.
- CVE counts track package count. Ubuntu has ~100 packages and 15–40 CVEs. Alpine has ~15 packages and 0–3 CVEs. Distroless static has three packages and zero. Fewer packages means fewer places for a base-layer CVE.
- A zero-CVE scan is not immunity. It measures the absence of known vulnerabilities on the day of the scan. Rebuilding on a schedule—weekly, not just on release—is what keeps an image patched. A perfect base image pinned for six months is a bad base image .
- Debuggability is a real trade-off. Distroless and scratch cannot be shelled into. Debug variants exist, but they are larger and should not be deployed. Plan a debug strategy before committing to a shell-less base image.
Remember: The base image is not a neutral choice. It determines the libc, the package manager, the shell, the attack surface, and the compatibility envelope of everything built on top of it. scratch is for static binaries that need nothing. distroless is for production services that need a runtime but not an operating system. alpine is for small images that need a shell. debian-slim is for workloads that need glibc and the conventional Linux environment. There is no universal winner; there is only the right constraint for the right context. Choose deliberately, test the compatibility of the language stack, and rebuild on a schedule. The base image is the foundation, and foundations are expensive to change.
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!