<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Data – Serverküche</title><link>https://serverkueche.de/en/tags/data/</link><description>Data – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Thu, 06 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/data/index.xml" rel="self" type="application/rss+xml"/><item><title>Docker volumes vs. bind mounts: where your data really lives</title><link>https://serverkueche.de/en/tutorials/docker-volumes-vs-bind-mounts/</link><pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/docker-volumes-vs-bind-mounts/</guid><description>Named volume or bind mount? This foundational guide explains the difference, shows both with real examples, and when to use which.</description><content:encoded><![CDATA[<p>A container is ephemeral: delete it, and its data is gone. For your database, your Nextcloud files or your configuration to survive a <code>docker compose down</code>, you need volumes. This guide clears up the often confusing difference between <strong>named volumes</strong> and <strong>bind mounts</strong>.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>No new service this time, but a solid foundation: by the end you understand why container data normally disappears, know the two ways to store it permanently, and know for each service which to use. All examples are tested with <strong>Docker 29</strong> on Debian 13 and work with any current Docker version. You need this knowledge for every app tutorial – there, a <code>volumes:</code> block appears in almost every <code>compose.yaml</code>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A server with <a href="/en/tutorials/install-docker/">Docker installed</a></li>
<li>Basic understanding of <a href="/en/tutorials/docker-compose-basics/">Docker Compose</a> (services, <code>compose.yaml</code>)</li>
<li>Terminal access to the server – a pure command-line exercise, no web interface</li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-why-container-data-disappears">Step 1: Why container data disappears</h3>
<p>Everything a container writes into its own filesystem only lives as long as the container. That&rsquo;s intended – containers are meant to be replaceable. For anything that should persist (databases, uploads, configuration), you have to tell Docker explicitly where it lands <em>outside</em> the container. That&rsquo;s exactly what the two tools are for: <strong>named volumes</strong> and <strong>bind mounts</strong>.</p>
<h3 id="step-2-named-volumes--managed-by-docker">Step 2: Named volumes – managed by Docker</h3>
<p>A named volume is a storage area that <strong>Docker itself</strong> manages. You only give it a name; Docker determines the location. Create one:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker volume create sk-demo-vol</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">sk-demo-vol</span></span></code></pre></div>
</div>
<p>Now we mount the volume into a throwaway container (<code>--rm</code> deletes it afterwards) at <code>/data</code> and write a file into it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v sk-demo-vol:/data alpine:3 sh -c <span class="s2">&#34;echo hallo-von-serverkueche &gt; /data/notiz.txt &amp;&amp; ls -l /data&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">total 4
</span></span><span class="line"><span class="cl">-rw-r--r--    1 root     root            23 Jul 20 22:31 notiz.txt</span></span></code></pre></div>
</div>
<p>The container is long gone – the data isn&rsquo;t. Where does it live? Ask Docker:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker volume inspect sk-demo-vol --format <span class="s2">&#34;{{ .Mountpoint }}&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/var/lib/docker/volumes/sk-demo-vol/_data</span></span></code></pre></div>
</div>
<p>This path belongs to Docker. You <em>can</em> view it as root, but shouldn&rsquo;t edit it directly:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /var/lib/docker/volumes/sk-demo-vol/_data/notiz.txt</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">hallo-von-serverkueche</span></span></code></pre></div>
</div>
<p>The proof of persistence: a completely new container sees the same data:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v sk-demo-vol:/data alpine:3 cat /data/notiz.txt</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">hallo-von-serverkueche</span></span></code></pre></div>
</div>
<p><strong>Remember:</strong> named volumes are the default for application data – databases, uploads, everything the app manages itself. Docker takes care of location and permissions, and the volume survives <code>docker compose down</code> (only <code>down -v</code> deletes it too).</p>
<h3 id="step-3-bind-mounts--a-folder-from-the-host">Step 3: Bind mounts – a folder from the host</h3>
<p>With a bind mount you mount a <strong>specific folder from your server</strong> into the container. You determine the path, and changes are immediately visible on both sides:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/sk-demo-bind
</span></span><span class="line"><span class="cl">docker run --rm -v /opt/sk-demo-bind:/data alpine:3 sh -c <span class="s2">&#34;echo aus-dem-container &gt; /data/host.txt&#34;</span>
</span></span><span class="line"><span class="cl">ls -l /opt/sk-demo-bind</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">total 4
</span></span><span class="line"><span class="cl">-rw-r--r-- 1 root root 18 Jul 21 00:31 host.txt</span></span></code></pre></div>
</div>
<p>The container wrote the file, and it lives directly in your host folder – without the detour through <code>/var/lib/docker</code>. That&rsquo;s the point of bind mounts: <strong>files you want to edit yourself.</strong> Classic cases are configuration files (<code>traefik.yml</code>, <code>nginx.conf</code>) or a <code>compose.yaml</code> that reads in a config.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tip
  </p>
  <div class="prose-kitchen text-sm">Rule of thumb: <strong>named volume for data the app manages</strong> (database, uploads). <strong>Bind mount for files you manage</strong> (configuration). When in doubt, named volume – it causes the fewest permission problems.</div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm">Never blindly mount sensitive host paths into a container: not <code>/</code>, not <code>/etc</code>, no home directories – and especially not <code>/var/run/docker.sock</code>. Anyone who gets the Docker socket mounted can start arbitrary containers and is thus effectively root on the entire host. Always mount only the one folder the service really needs.</div>
</div>
<h3 id="step-4-both-in-the-composeyaml">Step 4: Both in the <code>compose.yaml</code></h3>
<p>In Compose the difference looks like this. A named volume is declared at the bottom under <code>volumes:</code> and referenced by name above; a bind mount is simply a host path:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">beispiel/app:1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">app-daten:/var/lib/app               </span><span class="w"> </span><span class="c"># named volume (Docker-managed)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.yml:/etc/app/config.yml:ro  </span><span class="w"> </span><span class="c"># bind mount (your file, read-only)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">app-daten:</span></span></span></code></pre></div>
</div>
<p>The <code>:ro</code> at the end makes the bind mount <strong>read-only</strong> – the container can read the configuration but not change it. For mounted configs that&rsquo;s a good habit.</p>
<h3 id="step-5-the-trap--anonymous-volumes">Step 5: The trap – anonymous volumes</h3>
<p>If you leave out the name with <code>-v</code> (<code>-v /data</code> instead of <code>-v name:/data</code>), or an image brings a <code>VOLUME</code> instruction in its Dockerfile, an <strong>anonymous volume</strong> with a random ID is created. This is what the first case looks like:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --name demo -v /data alpine:3 sh -c <span class="s2">&#34;echo test &gt; /data/x.txt&#34;</span></span></span></code></pre></div>
</div>
<p>We deliberately run the container here without <code>--rm</code> – otherwise Docker would immediately delete the anonymous volume on exit. The everyday problem: such volumes stick around, pile up unnoticed, and you can no longer figure out later which data belongs where:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker volume ls</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">DRIVER    VOLUME NAME
</span></span><span class="line"><span class="cl">local     sk-demo-vol
</span></span><span class="line"><span class="cl">local     9f8c1a2b3c4d5e6f70819a0b1c2d3e4f5061a2b3c4d5e6f70819a0b1c2d3e4f50</span></span></code></pre></div>
</div>
<p>The cryptic line is an anonymous volume. You can now remove the throwaway container – the anonymous volume <strong>still</strong> stays behind:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker rm demo</span></span></code></pre></div>
</div>
<p><strong>Always give your volumes a name</strong> – then it stays clear what belongs to what.</p>
<h3 id="step-6-managing-volumes">Step 6: Managing volumes</h3>
<p>The most important everyday commands:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker volume ls           <span class="c1"># list all volumes</span>
</span></span><span class="line"><span class="cl">docker volume inspect NAME <span class="c1"># details, including the mountpoint</span>
</span></span><span class="line"><span class="cl">docker volume rm NAME      <span class="c1"># delete a volume (data gone!)</span></span></span></code></pre></div>
</div>
<p>We save cleaning up our demo until after the backup section – we still need <code>sk-demo-vol</code> there.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The container writes, but the bind-mount folder on the host stays empty.</strong> You used a relative
path that points somewhere other than intended, or Docker created the path anew as an empty folder.
with <code>docker run</code>, always give bind mounts an <strong>absolute path</strong> (<code>/opt/app/config</code>, not <code>config</code>).
If this host path doesn&rsquo;t exist yet, Docker silently creates it as an empty folder – so check with
<code>ls</code> that you really hit the right one. In the <code>compose.yaml</code>, <code>./</code> paths are perfectly fine, on the
other hand – they&rsquo;re relative to the <code>compose.yaml</code> and thus unambiguous. Note also: <code>docker run -v config:/data</code> (without <code>/</code> or <code>./</code> in front) is <strong>not</strong> a bind mount, but creates a named volume
called <code>config</code>.</p>
<p><strong><code>Permission denied</code> as soon as the container tries to write into the mount.</strong> The process in the
container runs under a different UID than the owner of the host folder – typical with bind mounts.
with named volumes this rarely happens (Docker sets the permissions). With bind mounts, give the
folder to the matching user (<code>chown -R 1000:1000 /opt/app/data</code>) or use the <code>user:</code> setting of the
Compose in the image.</p>
<p><strong>After <code>docker compose down</code> all data is gone.</strong> You used <code>docker compose down -v</code> – the <code>-v</code>
deletes the named volumes too. for a normal restart, work <strong>without</strong> <code>-v</code>. Use <code>-v</code> deliberately
only when you really want to start from scratch.</p>
<p><strong><code>docker system prune</code> deleted data.</strong> <code>docker volume prune</code> or <code>docker system prune --volumes</code>
removes volumes that no container is currently attached to. With <code>docker volume prune</code>, <strong>named</strong>
volumes are protected by default – only <strong>anonymous</strong> volumes are deleted; named ones only go with
<code>--all</code>/<code>-a</code>. (With <code>docker system prune</code>, on the other hand, <code>-a</code>/<code>--all</code> controls the images, not
the named volumes.) another strong argument for naming – an <code>sk-demo-vol</code> survives an accidental
<code>docker volume prune</code>, an anonymous volume doesn&rsquo;t. Still, only run <code>prune</code> with volumes when all
important stacks are active, or remove specifically with <code>docker volume rm</code>.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p>Volumes are not a backup – they live on the <strong>same</strong> disk as your server. If it fails, container <em>and</em> volume are gone. You back up a named volume by packing its contents into an archive via a throwaway container:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/backups
</span></span><span class="line"><span class="cl">docker run --rm -v sk-demo-vol:/data -v /opt/backups:/backup alpine:3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  tar czf /backup/sk-demo-vol.tar.gz -C /data .</span></span></code></pre></div>
</div>
<p>Restoring works the same way around – archive in, unpack into the volume. Create the target volume beforehand (<code>docker volume create sk-demo-vol</code>) and stop the associated container so nothing writes into the open volume:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v sk-demo-vol:/data -v /opt/backups:/backup alpine:3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  tar xzf /backup/sk-demo-vol.tar.gz -C /data</span></span></code></pre></div>
</div>
<p>This archive – and all bind-mount folders under <code>/opt</code> – belong in your <a href="/en/tutorials/restic-backups/">encrypted off-site backup with Restic</a>. That way your database survives even a total failure of the server.</p>
<p><strong>In everyday use</strong> it pays to stay tidy: name volumes descriptively (<code>nextcloud-db</code>, not <code>db</code>), clean up anonymous volumes occasionally, and check with <code>docker system df</code> how much space your volumes take up – with databases and media services in particular, this grows noticeably over time.</p>
<p>Finally, tear down the demo again – now that backup and all examples are done:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker volume rm sk-demo-vol <span class="o">&amp;&amp;</span> rm -rf /opt/sk-demo-bind /opt/backups</span></span></code></pre></div>
</div>
]]></content:encoded></item></channel></rss>