<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bjørn T. Dahl</title>
    <description>The latest articles on DEV Community by Bjørn T. Dahl (@btdahl).</description>
    <link>https://dev.to/btdahl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3902798%2F5ad4f7d7-bbfa-41f3-b689-2b7538f1eebc.jpg</url>
      <title>DEV Community: Bjørn T. Dahl</title>
      <link>https://dev.to/btdahl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/btdahl"/>
    <language>en</language>
    <item>
      <title>Sending stdin into a container using nothing but kernel primitives</title>
      <dc:creator>Bjørn T. Dahl</dc:creator>
      <pubDate>Tue, 28 Apr 2026 17:04:38 +0000</pubDate>
      <link>https://dev.to/btdahl/sending-stdin-into-a-container-using-nothing-but-kernel-primitives-3mej</link>
      <guid>https://dev.to/btdahl/sending-stdin-into-a-container-using-nothing-but-kernel-primitives-3mej</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.apario.net/sending-stdin-into-a-container" rel="noopener noreferrer"&gt;blog.apario.net&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When a containerised process needs input via stdin, delivering it from outside the container is often done in a more complex way than necessary. This article describes a lightweight, robust approach using a host-created FIFO mounted into the container, providing a simple, near-zero-overhead, atomic, scriptable, multi-source stdin channel with no extra daemons, no &lt;code&gt;docker exec&lt;/code&gt;, and no PTY involvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Some processes use stdin as their primary input channel. When such a process runs inside a Docker container, the conventional options for sending input to it from the host are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker exec -i container command&lt;/code&gt;&lt;/strong&gt; — spawns a new process inside the container rather than reaching the existing one's stdin, introduces extra pipes and process overhead, and is unsuitable for automation when ordering or atomicity matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker attach&lt;/code&gt;&lt;/strong&gt;, or &lt;strong&gt;&lt;code&gt;docker run -i&lt;/code&gt;&lt;/strong&gt; with a held-open stdin — attaches a stdin stream to the container's primary process, but is fragile, blocks the caller, and is coupled to the lifetime of the writer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A multiplexer inside the container (&lt;code&gt;tmux&lt;/code&gt;, &lt;code&gt;screen&lt;/code&gt;)&lt;/strong&gt; — adds a persistent process and complexity inside the container, requires a separate attach mechanism, couples the container image to the tooling, and displaces the target process from PID 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;socat&lt;/code&gt;&lt;/strong&gt; — forks a fresh service process per connection in the &lt;code&gt;LISTEN,fork EXEC:&lt;/code&gt; pattern, ruling out stateful services. Without &lt;code&gt;fork&lt;/code&gt;, handles exactly one connection and exits. &lt;code&gt;socat&lt;/code&gt; itself occupies PID 1 rather than the service process, except in the &lt;code&gt;EXEC:...,nofork&lt;/code&gt; form, which &lt;code&gt;execvp&lt;/code&gt;s the service into PID 1 but is limited to a single connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom socket or network interface&lt;/strong&gt; — requires the process to support it, which many do not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are satisfactory when the goal is simple, reliable, scriptable input delivery to a process that already reads from stdin. The Linux kernel, however, already provides a cleaner path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach
&lt;/h2&gt;

&lt;p&gt;All it takes is a single, well-established Linux primitive: the &lt;em&gt;named pipe&lt;/em&gt; (FIFO), and a few lines of standard shell tooling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A named pipe (a FIFO in POSIX terminology) is a kernel-managed special file that provides a unidirectional byte stream for inter-process communication, allowing unrelated processes to exchange data using normal file I/O without storing it on disk. It is created with &lt;code&gt;mkfifo&lt;/code&gt; and exists in the filesystem. Unlike regular files, it does not support seeking and has no persistent contents, and unlike anonymous pipes it can be opened by unrelated processes. Because the kernel manages FIFO data entirely in memory, transfers are typically orders of magnitude faster than disk-based I/O and use far fewer resources. FIFOs are one of the 7 standard POSIX file types.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Linux FIFO created on the host can be mounted directly into a container as a bind mount. Inside the container, a minimal wrapper script opens the FIFO and redirects it to the target process's stdin. From that point, writing to the FIFO delivers input directly to the containerised process's stdin, with kernel-guaranteed atomicity.&lt;/p&gt;

&lt;p&gt;The additional processes and abstraction layers introduced by the conventional approaches are avoided entirely with this method. The full mechanism requires only three standard components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A FIFO created on the host before the container starts.&lt;/li&gt;
&lt;li&gt;The FIFO mounted into the container.&lt;/li&gt;
&lt;li&gt;A wrapper script inside the container that opens the FIFO and execs the target process with stdin redirected from it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This works because Docker on Linux runs containers as processes directly on the host kernel, isolated via namespaces and cgroups. A FIFO created on the host is therefore the same kernel object inside the container as outside. The bind mount simply makes it visible at a chosen path in the container's filesystem namespace, with no copying, translation, or intermediary layers involved. The in-memory nature of the FIFO makes this input path as efficient as it can practically be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create the FIFO on the host and set access permissions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkfifo&lt;/span&gt; /var/run/myservice/command_pipe
&lt;span class="nb"&gt;chmod &lt;/span&gt;0666 /var/run/myservice/command_pipe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The FIFO must exist before the container starts. The permissions shown are permissive for simplicity. Tighten the permissions and ownership to suit your environment, but ensure the process inside the container can still open the FIFO.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Mount the FIFO into the container
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"/var/run/myservice/command_pipe:/service/command_pipe"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  my-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container now sees the same FIFO file, and writes to it are immediately available inside the container. The kernel mediates the transfer entirely in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Wrapper script inside the container
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# FIFO-based stdin channel for containerised processes&lt;/span&gt;
&lt;span class="c"&gt;# See: https://blog.apario.net/sending-stdin-into-a-container&lt;/span&gt;

&lt;span class="nv"&gt;CMD_FIFO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/service/command_pipe"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD_FIFO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2 &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: FIFO &lt;/span&gt;&lt;span class="nv"&gt;$CMD_FIFO&lt;/span&gt;&lt;span class="s2"&gt; missing. Aborting."&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Open the FIFO on fd 3, read/write (&amp;lt;&amp;gt;) to prevent blocking.&lt;/span&gt;
&lt;span class="c"&gt;# This exec opens the file descriptor without touching the process.&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;3&amp;lt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD_FIFO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Exec the target process with stdin redirected from the FIFO.&lt;/span&gt;
&lt;span class="c"&gt;# This exec replaces this shell, so the target process becomes PID 1.&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt; /usr/bin/myservice &amp;lt;&amp;amp;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is the container's entrypoint. It is where the FIFO, the bind mount, and process handoff converge into a working stdin channel.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Sending input via the FIFO
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Single command&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reload config"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/run/myservice/command_pipe

&lt;span class="c"&gt;# Multiple commands from a file&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;commands.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/run/myservice/command_pipe

&lt;span class="c"&gt;# From another script&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"save&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;quit&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/run/myservice/command_pipe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No special tooling required. Standard shell redirection and piping work as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  FIFO opening and file descriptors
&lt;/h3&gt;

&lt;p&gt;Inside the container, the FIFO is opened read/write rather than read-only because a FIFO opened for reading blocks until a writer opens the other end, and vice versa. Opening the FIFO with &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; (read/write) on a single file descriptor sidesteps this: the open call returns immediately because the same fd satisfies both ends. The process then holds the FIFO open continuously, meaning subsequent host writes never block waiting for a reader.&lt;/p&gt;

&lt;p&gt;File descriptors 0, 1, and 2 are reserved for stdin, stdout, and stderr respectively. Any number from 3 up to the process's file descriptor limit is available for arbitrary use. fd 3 is chosen here by convention, but any unused descriptor would work.&lt;/p&gt;

&lt;p&gt;It is technically possible to open the FIFO directly on stdin with &lt;code&gt;exec 0&amp;lt;&amp;gt; "$CMD_FIFO"&lt;/code&gt;, but this modifies the shell's stdin before the target process takes over. In a simple wrapper this may work, but any startup logic between that line and the final &lt;code&gt;exec&lt;/code&gt;, e.g. sourcing files, running checks, or reading configuration, would have its stdin redirected from the FIFO. Using a custom fd as an intermediary keeps the shell's stdin untouched until the handoff.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why exec instead of a subshell
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;exec&lt;/code&gt; to replace the wrapper shell with the target process means the target process inherits PID 1 inside the container. This is correct behaviour for Docker: PID 1 receives signals directly, including &lt;code&gt;SIGTERM&lt;/code&gt; on &lt;code&gt;docker stop&lt;/code&gt;, allowing clean shutdown. A subshell sitting between the FIFO and the process would intercept or swallow signals.&lt;/p&gt;

&lt;p&gt;The shell's &lt;code&gt;exec&lt;/code&gt; is just a thin POSIX wrapper around the kernel's &lt;code&gt;execve&lt;/code&gt; system call; the process replacement itself is a kernel operation that leaves no intermediate process behind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Atomicity
&lt;/h3&gt;

&lt;p&gt;The Linux kernel guarantees that writes to a FIFO up to &lt;code&gt;PIPE_BUF&lt;/code&gt; bytes are atomic. On Linux, &lt;code&gt;PIPE_BUF&lt;/code&gt; is 4096 bytes. Multiple processes can write to the same FIFO concurrently without interleaving, as long as each write is within this limit. For line-oriented input, this is almost always satisfied.&lt;/p&gt;

&lt;p&gt;The FIFO buffer's total capacity typically defaults to 64KB on Linux, configurable up to a system-wide maximum (typically 1MB). If writers produce data faster than the target process can consume them and the buffer fills, writes will be blocked until space becomes available.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-way only
&lt;/h3&gt;

&lt;p&gt;This mechanism delivers input to the target process. Output is not returned through the FIFO. Use &lt;code&gt;docker logs&lt;/code&gt; or a separate log aggregation mechanism to observe the process's output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;This approach&lt;/th&gt;
&lt;th&gt;docker exec&lt;/th&gt;
&lt;th&gt;In-container multiplexer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scriptable from host&lt;/td&gt;
&lt;td&gt;Yes, native shell&lt;/td&gt;
&lt;td&gt;Yes, with overhead&lt;/td&gt;
&lt;td&gt;Requires attach tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra processes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;At least 2 per command&lt;/td&gt;
&lt;td&gt;Multiplexer server process with IPC layer + attach client + docker exec processes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service process is PID 1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Should be&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atomicity&lt;/td&gt;
&lt;td&gt;Kernel-guaranteed (per PIPE_BUF)&lt;/td&gt;
&lt;td&gt;Not guaranteed&lt;/td&gt;
&lt;td&gt;Depends on implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overhead&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;td&gt;Per-command process spawn cost&lt;/td&gt;
&lt;td&gt;Constant resource footprint plus per-interaction exec and spawn process cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Applicability
&lt;/h2&gt;

&lt;p&gt;This approach works for most, if not all, containerised processes that read from stdin; input can originate from the host, from within the same container, or from a different container (including sidecars, via a shared mount or volume), or any combination of these.&lt;/p&gt;

&lt;p&gt;It may not be suitable for processes that require a PTY (terminal emulation), though many processes that appear to require a PTY in interactive use will accept plain stdin input when running non-interactively.&lt;/p&gt;

&lt;p&gt;Host to container communication is not applicable on Windows or macOS. This approach relies on a shared POSIX kernel between host and container. On Windows and macOS, Docker Desktop runs Linux containers inside a lightweight Linux VM, isolating them from the host kernel. Windows native containers run directly on the Windows kernel, which does not expose a POSIX-compatible interface to containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A host FIFO mounted into a container, opened read/write to prevent blocking, and passed to a target process via &lt;code&gt;exec&lt;/code&gt; and stdin redirection provides a clean, low-overhead, kernel-mediated data channel into any containerised stdin-reading process. No extra daemons, no network sockets, no &lt;code&gt;docker exec&lt;/code&gt; overhead. Data can be written to it with standard tools from the host, from within a container, or from any other process with access. The kernel handles atomicity and buffering. Together these properties make it a practical, low-complexity solution for scriptable stdin control in containerised environments, with no moving parts outside the kernel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provenance and scope
&lt;/h2&gt;

&lt;p&gt;This approach emerged while working around stdin control limitations in a proprietary server binary. The original implementation is available in the &lt;a href="https://github.com/btdahl/bedfeather" rel="noopener noreferrer"&gt;Bedfeather&lt;/a&gt; project.&lt;/p&gt;

&lt;p&gt;Each component is a decades-old POSIX or Linux primitive. Using FIFOs in container data pipelines is not new, and the shell methods involved are well established. Their combination in this pattern, however, appears to be rarely documented.&lt;/p&gt;

&lt;p&gt;This article exists to distill the method into a clear, referenceable form. Security hardening, failure recovery, and behaviour under sustained, concurrent write load are deliberately outside this article's scope, but should be considered before production use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Licence
&lt;/h2&gt;

&lt;p&gt;Article text is licensed under &lt;a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener noreferrer"&gt;CC BY 4.0&lt;/a&gt; — share and adapt freely with attribution. Code samples are released under the &lt;a href="https://opensource.org/licenses/MIT" rel="noopener noreferrer"&gt;MIT licence&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Full licence text including copyright notice is available on the &lt;a href="https://blog.apario.net/sending-stdin-into-a-container" rel="noopener noreferrer"&gt;original post&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>containers</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
