Static vs. Dynamic Binaries: Choosing the Right Container Runtime

Key Takeaways
- Static and dynamic binaries have fundamentally different runtime requirements — a dynamically linked binary needs shared libraries present in the container filesystem at startup; a statically linked binary carries everything it needs. Mismatching binary type to base image is one of the most common causes of containers that build successfully and fail immediately at runtime.
- Base image choice determines runtime security posture — before any runtime security control is applied, the base image has already set the attack surface. A distroless or minimal base image with a statically linked binary has a categorically smaller exposure than a full OS image running a dynamically linked application with a shell and package manager present.
- Container runtime security operates at two layers — the high-level runtime (containerd, CRI-O) manages the container lifecycle; the low-level OCI runtime (runc, gVisor, Kata Containers) enforces isolation. Choosing the right combination depends on your workload's binary type, performance requirements, and compliance posture.
What a Container Runtime Is and the Two Layers That Make It Work
"Container runtime" is a term that gets used loosely to mean several different things — the daemon that manages containers, the process that runs them, and the isolation mechanism that separates them from the host. Understanding the two distinct layers that make up a container runtime environment matters because they make different security and compatibility decisions.
The high-level runtime sits between the container orchestrator (Kubernetes, for example) and the actual container execution layer. containerd and CRI-O are the most widely deployed high-level runtimes in production Kubernetes environments. Their job is lifecycle management: pulling images, managing storage and networking, and handing off container execution to the low-level runtime via the OCI runtime specification. The high-level runtime is what your CI/CD pipeline and orchestration layer interact with.
The low-level OCI runtime is what actually creates and runs the container process. runc is the reference implementation and the default in most environments — it creates Linux namespaces and cgroups, sets up the container filesystem from the image layers, and launches the container process. Alternative low-level runtimes like gVisor (runsc) and Kata Containers intercept syscalls or run workloads inside lightweight VMs to provide stronger isolation than runc's namespace-based model.
The OCI (Open Container Initiative) specification defines the interface between these two layers — the image format, the runtime configuration, and the lifecycle hooks — which is why containers built to the OCI specification are portable across runtimes. But portability at the specification level doesn't mean all containers work correctly in all runtime configurations. Binary type, base image contents, and runtime isolation model all interact in ways that can produce silent failures or security gaps if the combination isn't chosen deliberately.
Static vs. Dynamic Binaries
Every executable binary is either statically or dynamically linked, and that distinction has direct consequences for what needs to be present in the container at runtime.
A dynamically linked binary is compiled against shared libraries — libc, libssl, libpthread — that are not included in the binary itself. At startup, the dynamic linker (ld.so on Linux) resolves those library references and loads them from the filesystem. If the required shared libraries aren't present at the expected paths in the container filesystem, the process fails immediately — typically with a not found error or a segfault, before any application code has executed.
Most binaries compiled from C, C++, and Rust (by default) are dynamically linked. Go binaries are statically linked by default when built with CGO_DISABLED=1, but enable dynamic linking when CGO is active — a distinction that matters significantly for container base image selection. Python, Ruby, and Node.js applications run on dynamically linked interpreters and inherit the interpreter's library dependencies in addition to any native extension dependencies of their own.
A statically linked binary includes all library dependencies compiled directly into the executable. At runtime, it needs no shared libraries from the container filesystem — just a kernel to run on. A statically linked Go binary or a musl-linked Rust binary can run in a container with no OS userspace at all. This is the property that makes scratch and distroless base images viable for certain workloads.
The practical consequences of getting this wrong are unforgiving. A statically linked binary in a full Ubuntu base image works, but carries the entire Ubuntu userspace as unnecessary attack surface. A dynamically linked binary in a scratch image builds without error and fails at the first docker run — the missing shared libraries produce a runtime error that has nothing to do with application logic and everything to do with the base image selection.
The error message itself is often misleading. exec format error and no such file or directory on a binary that clearly exists in the image are both symptoms of dynamic linking failures in minimal base images. Understanding that these are base image compatibility errors, not application bugs, is the starting point for debugging them correctly.
How Base Image Choice Locks In Runtime Security Posture Before the First Deploy
The container runtime enforces isolation between the container and the host — but it can only control what's inside the container it's given. By the time the runtime launches a container, the base image has already determined the attack surface: which binaries are present, which libraries are loaded, whether a shell exists, and whether a package manager could be used post-compromise to install additional tooling.
This matters for container runtime security because many runtime security controls — seccomp profiles, AppArmor policies, syscall filtering — are calibrated against the expected behavior of the workload. A container running a statically linked binary with no shell has a small, predictable syscall footprint. A container running a dynamically linked application in a full OS image makes a far broader range of syscalls, uses more kernel interfaces, and provides an attacker with more post-exploitation tools if the application layer is compromised.
Scratch images contain nothing but the binary you copy into them. No shell, no package manager, no shared libraries, no OS utilities. They're only viable for fully statically linked binaries — but when the binary type matches, they represent the smallest possible attack surface a container runtime has to protect.
Distroless containers take a middle path: they include the minimum runtime dependencies for a specific language ecosystem (the JVM for Java workloads, the Python interpreter for Python workloads, libc and libssl for dynamically linked binaries) without including a shell, package manager, or general-purpose OS utilities. A distroless container can run dynamically linked binaries while still dramatically reducing the toolset available to an attacker who achieves code execution inside the container. For a broader look at how base image choice affects the inherited risk profile of containerized workloads, our post on hidden risks in Docker base images covers the full spectrum.
Hardened OS-based images — built on minimal distributions like Alpine, hardened UBI, or Echo's hardened base images — retain a more complete OS userspace but remove unnecessary packages, apply STIG or CIS benchmark configurations, and ship with patched libraries. They're the right choice for dynamically linked workloads that need OS-level tooling at runtime or that require a specific libc implementation (musl vs. glibc compatibility is a frequent source of runtime failures when migrating between Alpine and Debian-based images).
The security gap between a full Ubuntu image with all default packages and a hardened minimal image running the same application can span hundreds of CVEs before application code is considered at all. Base image choice is a security decision, and it's one that's made before the runtime ever runs.
Echo's hardened container images are built on this principle — minimal footprint, continuously patched, and shipped CVE-free with signed provenance. They provide the runtime security baseline that lets other controls (seccomp profiles, admission policies, runtime monitoring) operate against a clean, known-good foundation rather than compensating for a vulnerable base layer.
How to Match Your Application's Runtime Requirements to the Right Container Configuration
Choosing the right base image and runtime combination is a three-variable problem: binary type, isolation requirements, and compliance posture. The following framework maps those variables to concrete choices.
Identify your binary type first. Before selecting a base image, determine whether your application binary is statically or dynamically linked and which shared libraries it requires. For Go binaries, check whether CGO is enabled in your build. For compiled languages, use ldd <binary> to list dynamic library dependencies. For interpreted languages, the interpreter itself is dynamically linked and determines the minimum base image requirements.
Match binary type to base image:
- Fully statically linked binary (Go with CGO_DISABLED, musl-linked Rust): scratch or distroless/static is viable and optimal. Smallest possible attack surface, no unnecessary OS components.
- Dynamically linked binary requiring glibc: distroless/base (which includes glibc) or a minimal glibc-based image. Alpine's musl libc is not a drop-in replacement — glibc binaries fail at runtime on Alpine without compatibility shims.
- Dynamically linked binary requiring musl libc: Alpine-based images or musl-based minimal images.
- JVM, Python, or Node.js workloads: language-specific distroless images (distroless/java, distroless/python) or hardened language runtime images that include only the interpreter and its dependencies.
- Workloads requiring OS-level tooling at runtime: hardened minimal OS images (hardened UBI, Alpine with unnecessary packages removed, Echo hardened base images). Full Ubuntu or Debian images are rarely justified for production workloads and should require explicit sign-off.
Set isolation requirements based on workload sensitivity. For most workloads, runc with a properly configured seccomp profile and AppArmor policy provides adequate container isolation. For workloads handling sensitive data, multi-tenant environments, or compliance-regulated processing, alternative OCI runtimes provide stronger isolation guarantees:
- gVisor (runsc) interposes on syscalls in userspace, providing a significant reduction in kernel attack surface. It adds latency overhead — typically 10–30% on I/O-heavy workloads — and has syscall compatibility limitations that can affect applications making unusual kernel calls.
- Kata Containers runs each container in a lightweight VM, providing hardware-enforced isolation rather than namespace-based separation. It's the appropriate choice when the strongest possible isolation boundary is required and when the performance overhead of VM startup is acceptable for the workload's deployment pattern.
Apply compliance requirements as a filter. FIPS-validated workloads need base images with FIPS 140-3 validated cryptographic modules — which rules out most public Docker images and requires either a purpose-built FIPS image or a provider like Echo that ships FIPS-validated base images as a standard offering. STIG-hardened workloads require base images aligned with the relevant STIG benchmark, which similarly narrows the base image options to purpose-built hardened images.
For a detailed guide to hardening techniques that complement base image selection, our post on container hardening techniques covers the control layer above the base image.
Frequently Asked Questions
Why do dynamic binaries fail in static base images? Dynamic binaries rely on the dynamic linker (ld.so) and shared libraries (libc, libssl, etc.) being present in the container filesystem at startup. Scratch and fully static base images contain neither — the dynamic linker has nothing to resolve against, and the process fails before executing any application code. The error typically surfaces as no such file or directory or exec format error on a binary that visibly exists in the image, which makes the root cause non-obvious without understanding the dynamic linking dependency.
Is gVisor or Kata more appropriate for regulated workloads? Both provide stronger isolation than runc, but they suit different requirements. gVisor intercepts syscalls in userspace, reducing kernel attack surface without full VM overhead — it's well-suited for multi-tenant environments and workloads where syscall surface reduction is the primary goal. Kata Containers provides hardware-enforced VM isolation, which is the appropriate choice when compliance frameworks require workload isolation equivalent to separate VMs. For most FedRAMP or HIPAA workloads, gVisor is sufficient; DoD classified environments typically require Kata-level isolation.
Does the container runtime affect vulnerability scanning results? The runtime itself doesn't affect what a scanner finds in an image — image scanning is a static analysis of image layers, performed before the runtime is involved. However, runtime selection affects which vulnerabilities are exploitable: a gVisor runtime significantly reduces the exploitability of kernel-level CVEs because the application never interacts with the host kernel directly. Scanners will still report those CVEs in the image, but their practical risk is lower in a gVisor environment than in a runc environment.
Does container runtime choice affect Kubernetes scheduling performance? Yes, meaningfully for some runtimes. runc container startup is measured in milliseconds. gVisor adds startup overhead in the range of hundreds of milliseconds due to sandbox initialization. Kata Containers adds VM boot time — typically one to three seconds per container — which is significant for workloads with high pod churn or aggressive autoscaling. For performance-sensitive workloads, runtime selection should be validated against realistic scheduling patterns before production deployment, and alternative runtimes should be applied selectively to workloads that require them rather than cluster-wide.



.avif)
.avif)