<?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: serverkueche.de</title>
    <description>The latest articles on DEV Community by serverkueche.de (@serverkueche).</description>
    <link>https://dev.to/serverkueche</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4026385%2Fbcade448-c7e2-49fc-8e0f-176232f13294.jpg</url>
      <title>DEV Community: serverkueche.de</title>
      <link>https://dev.to/serverkueche</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/serverkueche"/>
    <language>en</language>
    <item>
      <title>Self-hosting Nextcloud: your own cloud behind Traefik</title>
      <dc:creator>serverkueche.de</dc:creator>
      <pubDate>Sat, 25 Jul 2026 12:16:37 +0000</pubDate>
      <link>https://dev.to/serverkueche/self-hosting-nextcloud-your-own-cloud-behind-traefik-9hb</link>
      <guid>https://dev.to/serverkueche/self-hosting-nextcloud-your-own-cloud-behind-traefik-9hb</guid>
      <description>&lt;p&gt;Your files, your calendar, your contacts – but on your server instead of at a cloud corporation. &lt;strong&gt;Nextcloud&lt;/strong&gt; is the flagship of self-hosting: a full-featured cloud you run yourself. In this recipe we set it up cleanly behind Traefik, with its own database, caching and automatic HTTPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we building?
&lt;/h2&gt;

&lt;p&gt;By the end, &lt;strong&gt;Nextcloud 34&lt;/strong&gt; runs behind your Traefik proxy, reachable at &lt;code&gt;https://cloud.YOUR_DOMAIN&lt;/code&gt; with a valid Let's Encrypt certificate. Nextcloud is your private cloud: sync files (like Dropbox), calendar and contacts across all devices, photos, notes, office documents. You connect the official &lt;strong&gt;desktop sync client&lt;/strong&gt; and the &lt;strong&gt;phone apps&lt;/strong&gt; with your server – the data lives exclusively with you.&lt;/p&gt;

&lt;p&gt;We deliberately use the &lt;strong&gt;classic &lt;code&gt;nextcloud&lt;/code&gt; image&lt;/strong&gt; (not "Nextcloud All-in-One") plus &lt;strong&gt;MariaDB&lt;/strong&gt; as the database and &lt;strong&gt;Redis&lt;/strong&gt; for caching and file locking. This combination is stable, well documented and fits exactly into the Traefik pattern from the Serverküche.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Why not Nextcloud AIO?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nextcloud also offers an "All-in-One" package (AIO). But it brings its &lt;strong&gt;own TLS and own ports&lt;/strong&gt; and wants to handle the reverse-proxy part itself – that clashes with an existing Traefik. For our setup, the classic image is the right, controllable choice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A running &lt;strong&gt;Traefik reverse proxy&lt;/strong&gt; with the shared &lt;code&gt;proxy&lt;/code&gt; network and the Let's Encrypt resolver &lt;code&gt;le&lt;/code&gt; – set up as in the tutorial &lt;a href="https://serverkueche.de/en/tutorials/traefik-reverse-proxy/" rel="noopener noreferrer"&gt;reverse proxy with Traefik&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A subdomain &lt;code&gt;cloud.YOUR_DOMAIN&lt;/code&gt; whose DNS record (A/AAAA) points to your server IP – see &lt;a href="https://serverkueche.de/en/tutorials/connect-domain-to-server/" rel="noopener noreferrer"&gt;connecting a domain to your server&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A working &lt;strong&gt;backup&lt;/strong&gt;. A cloud is the place where data loss hurts most – first set up &lt;a href="https://serverkueche.de/en/tutorials/restic-backups/" rel="noopener noreferrer"&gt;backups with Restic&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 How big does the server need to be?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nextcloud is more frugal than its reputation: for private use or a small family (1–5 users), &lt;strong&gt;2 vCPU and 4 GB RAM&lt;/strong&gt; are quite enough – the tested VPS 1000 fits. The bottleneck is memory: with too little RAM the server starts swapping and gets sluggish. Redis (we build it in shortly) relieves this noticeably. For Nextcloud Office/Collabora or many parallel users you should rather plan for 8 GB.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;How big your server should be for your user count is estimated by the &lt;a href="https://serverkueche.de/en/serverempfehlung/" rel="noopener noreferrer"&gt;server calculator&lt;/a&gt; in a few clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create the DNS record
&lt;/h3&gt;

&lt;p&gt;At your DNS provider, create an entry &lt;code&gt;cloud.YOUR_DOMAIN&lt;/code&gt; that points to your server IP (A record for IPv4, AAAA for IPv6). Check that it resolves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short cloud.YOUR_DOMAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see your server IP. Only once the record is set can Traefik fetch the HTTPS certificate later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Determine the proxy subnet
&lt;/h3&gt;

&lt;p&gt;Nextcloud sits behind Traefik. So that Nextcloud's brute-force protection recognizes the &lt;strong&gt;real&lt;/strong&gt; visitor IP (and doesn't ban the internal Traefik IP), we have to enter Traefik as a "trusted proxy". For that you need the subnet of your &lt;code&gt;proxy&lt;/code&gt; network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network inspect proxy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{(index .IPAM.Config 0).Subnet}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get something like &lt;code&gt;172.18.0.0/16&lt;/code&gt;. &lt;strong&gt;Remember this value&lt;/strong&gt; – it goes into the configuration as &lt;code&gt;TRUSTED_PROXIES&lt;/code&gt; shortly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create the compose.yaml
&lt;/h3&gt;

&lt;p&gt;Create a folder and change into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/nextcloud &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/nextcloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the &lt;code&gt;compose.yaml&lt;/code&gt;. Replace &lt;code&gt;cloud.YOUR_DOMAIN&lt;/code&gt;, all passwords and the &lt;code&gt;TRUSTED_PROXIES&lt;/code&gt; subnet from step 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nc-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mariadb:11.4&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-db&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MARIADB_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A_STRONG_ROOT_PASSWORD&lt;/span&gt;
      &lt;span class="na"&gt;MARIADB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
      &lt;span class="na"&gt;MARIADB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
      &lt;span class="na"&gt;MARIADB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A_STRONG_DB_PASSWORD&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nc_db:/var/lib/mysql&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthcheck.sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--connect"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--innodb_initialized"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;nc-redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:8-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-redis&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --requirepass A_STRONG_REDIS_PASSWORD&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;nc-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud:34-apache&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-app&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nc-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;nc-redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-db&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A_STRONG_DB_PASSWORD&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-redis&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_HOST_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A_STRONG_REDIS_PASSWORD&lt;/span&gt;
      &lt;span class="na"&gt;NEXTCLOUD_TRUSTED_DOMAINS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloud.YOUR_DOMAIN&lt;/span&gt;
      &lt;span class="na"&gt;OVERWRITEPROTOCOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;OVERWRITECLIURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://cloud.YOUR_DOMAIN&lt;/span&gt;
      &lt;span class="na"&gt;TRUSTED_PROXIES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;172.18.0.0/16&lt;/span&gt;
      &lt;span class="na"&gt;PHP_MEMORY_LIMIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1024M&lt;/span&gt;
      &lt;span class="na"&gt;PHP_UPLOAD_LIMIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10G&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nc_html:/var/www/html&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nc.rule=Host(`cloud.YOUR_DOMAIN`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nc.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nc.tls.certresolver=le"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nc.middlewares=nc-dav"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.nc.loadbalancer.server.port=80"&lt;/span&gt;
      &lt;span class="c1"&gt;# .well-known redirect for CalDAV/CardDAV (see below)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nc-dav.redirectregex.regex=https://(.*)/.well-known/(?:card|cal)dav"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nc-dav.redirectregex.replacement=https://cloud.YOUR_DOMAIN/remote.php/dav/"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nc-dav.redirectregex.permanent=true"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nc_html&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nc_db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three containers:&lt;/strong&gt; &lt;code&gt;nc-app&lt;/code&gt; (Nextcloud), &lt;code&gt;nc-db&lt;/code&gt; (MariaDB), &lt;code&gt;nc-redis&lt;/code&gt;. Only &lt;code&gt;nc-app&lt;/code&gt; is on the &lt;strong&gt;&lt;code&gt;proxy&lt;/code&gt; network&lt;/strong&gt; (for Traefik) &lt;em&gt;and&lt;/em&gt; on the internal &lt;code&gt;default&lt;/code&gt; network. Database and Redis stay exclusively internal – they have &lt;strong&gt;no &lt;code&gt;ports:&lt;/code&gt;&lt;/strong&gt; and are not reachable from outside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two named volumes:&lt;/strong&gt; &lt;code&gt;nc_html&lt;/code&gt; (program code, &lt;code&gt;config.php&lt;/code&gt;, user data) and &lt;code&gt;nc_db&lt;/code&gt; (the MariaDB data). The &lt;code&gt;nc_db&lt;/code&gt; volume is mandatory: without the entry, the database lands in an &lt;strong&gt;anonymous&lt;/strong&gt; volume, and a &lt;code&gt;docker compose down&lt;/code&gt; followed by &lt;code&gt;up&lt;/code&gt; would &lt;strong&gt;lose it irrecoverably&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;healthcheck&lt;/code&gt; + &lt;code&gt;depends_on: service_healthy&lt;/code&gt;:&lt;/strong&gt; Nextcloud starts faster than the database initializes. Without this healthcheck the initial install fails with a "Connection refused". This way the app container waits until MariaDB is really ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NEXTCLOUD_TRUSTED_DOMAINS&lt;/code&gt;:&lt;/strong&gt; without your own domain here, Nextcloud greets you with "Access through untrusted domain".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OVERWRITEPROTOCOL: https&lt;/code&gt;&lt;/strong&gt; tells Nextcloud it runs behind HTTPS – otherwise it builds internal links as &lt;code&gt;http://&lt;/code&gt; and logins/redirects break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TRUSTED_PROXIES&lt;/code&gt;:&lt;/strong&gt; the subnet from step 2. This lets Nextcloud recognize the real client IP; otherwise the brute-force protection bans the Traefik IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PHP_MEMORY_LIMIT&lt;/code&gt; / &lt;code&gt;PHP_UPLOAD_LIMIT&lt;/code&gt;:&lt;/strong&gt; the defaults (512 MB) are too small for large uploads. Set generously here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;loadbalancer.server.port=80&lt;/code&gt;:&lt;/strong&gt; the Apache image listens on port 80 in the container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;nc-dav&lt;/code&gt; middleware&lt;/strong&gt; solves a classic Nextcloud problem – more on that shortly.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ CalDAV/CardDAV: the .well-known redirect&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nextcloud wants to send calendar/contact clients via &lt;code&gt;/.well-known/caldav&lt;/code&gt; and &lt;code&gt;/.well-known/carddav&lt;/code&gt; to &lt;code&gt;/remote.php/dav/&lt;/code&gt;. Behind a reverse proxy, Nextcloud's own redirect doesn't work reliably – this later leads to a warning in the security check and to problems with calendar setup. The &lt;code&gt;nc-dav&lt;/code&gt; middleware above does the redirect directly in Traefik. Important: the pattern must match the &lt;strong&gt;full &lt;code&gt;https://…&lt;/code&gt; URL&lt;/strong&gt; – &lt;code&gt;redirectregex&lt;/code&gt; always checks the complete URL, not just the path. A pure path pattern like &lt;code&gt;^/.well-known/…&lt;/code&gt; therefore never matches.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Start and run through the initial setup
&lt;/h3&gt;

&lt;p&gt;Start the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first start, MariaDB initializes the database and Nextcloud unpacks itself – that takes a minute or two. Follow it in the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; nc-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run the image unpacks Nextcloud into the volume; you see lines like &lt;code&gt;Initializing nextcloud 34.0.1.2 ...&lt;/code&gt; and finally &lt;code&gt;Initializing finished&lt;/code&gt;, followed by the Apache start. If &lt;code&gt;restarting&lt;/code&gt; appears repeatedly instead, take a look at the "When things go wrong" section. With &lt;code&gt;Ctrl+C&lt;/code&gt; you leave the log view again (the container keeps running).&lt;/p&gt;

&lt;p&gt;Then open &lt;code&gt;https://cloud.YOUR_DOMAIN&lt;/code&gt;. Traefik fetches the certificate on the first access (can take a few seconds). Because we already configured the database and Redis via environment variables, Nextcloud detects that automatically and only asks for an &lt;strong&gt;administration account&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhohw6ic9woh82gqzssb9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhohw6ic9woh82gqzssb9.png" alt="The Nextcloud initial setup detects the autoconfig and only asks for the name and password of the administration account" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set an admin name and a strong password and click &lt;strong&gt;Install&lt;/strong&gt;. Nextcloud sets up the instance – after that you land on the login page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faeq2l2g1ozvrntp9xeaq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faeq2l2g1ozvrntp9xeaq.png" alt="The Nextcloud login page under your own HTTPS domain" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the login, the &lt;strong&gt;dashboard&lt;/strong&gt; greets you, and under &lt;strong&gt;Files&lt;/strong&gt; you find your cloud with a few example files:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8afao4tjrb1oy6c4srb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8afao4tjrb1oy6c4srb.png" alt="The Nextcloud dashboard with the " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm22kdjrh3ms8q608amli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm22kdjrh3ms8q608amli.png" alt="The Files interface of Nextcloud with folders and example files" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Set up background jobs (cron)
&lt;/h3&gt;

&lt;p&gt;Nextcloud has to perform tasks regularly (cleanup, notifications, thumbnails). By default this happens via "AJAX" on every page load – which is unreliable. The recommended way is a &lt;strong&gt;cron sidecar&lt;/strong&gt;: a second container with the same image that only runs &lt;code&gt;cron.php&lt;/code&gt;. Add a service to the &lt;code&gt;compose.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;nc-cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud:34-apache&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-cron&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/cron.sh&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nc-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nc_html:/var/www/html&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sidecar shares the &lt;code&gt;nc_html&lt;/code&gt; volume with the app and runs the jobs every five minutes. Apply the change and set the mode in Nextcloud to "Cron":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nc-app php occ background:cron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;strong&gt;Administration → Basic settings&lt;/strong&gt;, "Cron" should now be active.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 occ – the command-line tool&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;occ&lt;/code&gt; is Nextcloud's admin tool. You always call it as the user &lt;code&gt;www-data&lt;/code&gt; in the app container: &lt;code&gt;docker exec -u www-data nc-app php occ &amp;lt;command&amp;gt;&lt;/code&gt;. Two useful cleanup commands right after installation:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nc-app php occ db:add-missing-indices
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nc-app php occ config:system:set default_phone_region &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DE
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 6: Connect the clients
&lt;/h3&gt;

&lt;p&gt;Now the real benefit. There are three ways to use your cloud:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Desktop sync client&lt;/strong&gt; (Windows/macOS/Linux): install the "Nextcloud Desktop" client, enter &lt;code&gt;https://cloud.YOUR_DOMAIN&lt;/code&gt; as the server address, log in and choose a local folder. Files are synced like with Dropbox. With &lt;strong&gt;virtual files&lt;/strong&gt; (Windows/macOS) they only take up space on the disk when you open them – handy when your cloud is larger than the local disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone app&lt;/strong&gt; (Android/iOS): the official Nextcloud app, same server address. Ideal for automatic &lt;strong&gt;photo upload&lt;/strong&gt; from the smartphone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calendar &amp;amp; contacts:&lt;/strong&gt; in your device's system settings, create a CalDAV/CardDAV account with the address &lt;code&gt;https://cloud.YOUR_DOMAIN&lt;/code&gt; – thanks to the &lt;code&gt;.well-known&lt;/code&gt; middleware from step 3, automatic detection works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Features like calendar, contacts or mail are separate &lt;strong&gt;apps&lt;/strong&gt; you install with one click under &lt;strong&gt;Administration → Apps&lt;/strong&gt; – so Nextcloud is extensible without you touching anything on the server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Don't work with the admin account&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The administration account from step 4 is meant for &lt;strong&gt;management&lt;/strong&gt;, not for everyday use. Create a normal user account under &lt;strong&gt;Administration → Accounts&lt;/strong&gt; and sync your files through it. That way your powerful admin account isn't permanently logged in on all devices – a simple but effective security gain. For family or team, create further accounts here; each gets its own encrypted area.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When things go wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Access through untrusted domain" instead of the login page.&lt;/strong&gt; Your domain isn't in &lt;code&gt;trusted_domains&lt;/code&gt;. Check &lt;code&gt;NEXTCLOUD_TRUSTED_DOMAINS&lt;/code&gt; in the Compose file. To set it afterwards, use occ: &lt;code&gt;docker exec -u www-data nc-app php occ config:system:set trusted_domains 1 --value=cloud.YOUR_DOMAIN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The security check reports "Your web server is not set up properly to resolve .well-known/caldav".&lt;/strong&gt; The CalDAV/CardDAV redirect isn't working. Check the &lt;code&gt;nc-dav&lt;/code&gt; middleware labels (step 3) and that the router includes them via &lt;code&gt;...routers.nc.middlewares=nc-dav&lt;/code&gt;. The regex must match the full &lt;code&gt;https://…&lt;/code&gt; URL – &lt;code&gt;redirectregex&lt;/code&gt; checks the complete URL, not just the path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning "The 'Strict-Transport-Security' HTTP header is not set".&lt;/strong&gt; The HSTS header is missing. Set it as a Traefik middleware and attach it to the router: &lt;code&gt;traefik.http.middlewares.nc-secure.headers.stsSeconds=15552000&lt;/code&gt;. Attach it additionally to the &lt;code&gt;nc-dav&lt;/code&gt; middleware on the router (&lt;code&gt;...routers.nc.middlewares=nc-dav,nc-secure&lt;/code&gt;). The header must come from the proxy, not from Nextcloud. In &lt;a href="https://serverkueche.de/en/tutorials/harden-optimize-nextcloud/" rel="noopener noreferrer"&gt;part 2&lt;/a&gt; we set up exactly this &lt;code&gt;nc-secure&lt;/code&gt; middleware fully (including &lt;code&gt;includeSubdomains&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large uploads break off or end with a timeout / "413".&lt;/strong&gt; The PHP limit is too small. Increase &lt;code&gt;PHP_UPLOAD_LIMIT&lt;/code&gt; and &lt;code&gt;PHP_MEMORY_LIMIT&lt;/code&gt; (step 3) and restart the container. In rare cases these variables don't take effect – then mount your own &lt;code&gt;php.ini&lt;/code&gt; snippet into the image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the first start the installation aborts with "MySQL server has gone away" or "Connection refused".&lt;/strong&gt; The app container was faster than the database. That's exactly what the &lt;code&gt;healthcheck&lt;/code&gt; with &lt;code&gt;depends_on: condition: service_healthy&lt;/code&gt; is for – check that both are present in your Compose file, and restart with &lt;code&gt;docker compose up -d&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance &amp;amp; backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backups are mandatory for a cloud.&lt;/strong&gt; A consistent backup needs &lt;strong&gt;three things&lt;/strong&gt;: the &lt;strong&gt;database&lt;/strong&gt; (MariaDB dump), the &lt;strong&gt;data directory&lt;/strong&gt; (your files) and the &lt;strong&gt;configuration&lt;/strong&gt; (&lt;code&gt;config.php&lt;/code&gt;). Put Nextcloud into maintenance mode briefly before the backup so the database and files match each other:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nc-app php occ maintenance:mode &lt;span class="nt"&gt;--on&lt;/span&gt;
  docker &lt;span class="nb"&gt;exec &lt;/span&gt;nc-db mariadb-dump &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-pA_STRONG_ROOT_PASSWORD&lt;/span&gt; nextcloud &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nextcloud-db.sql
  docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nc-app php occ maintenance:mode &lt;span class="nt"&gt;--off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back up the dump together with the &lt;code&gt;nc_html&lt;/code&gt; volume &lt;strong&gt;encrypted and off-site&lt;/strong&gt; with &lt;a href="https://serverkueche.de/en/tutorials/restic-backups/" rel="noopener noreferrer"&gt;Restic&lt;/a&gt; – thanks to the dump you don't need to additionally back up the raw &lt;code&gt;nc_db&lt;/code&gt; volume. The &lt;code&gt;nc_html&lt;/code&gt; volume contains both the program code and the &lt;code&gt;config.php&lt;/code&gt; as well as – under &lt;code&gt;data/&lt;/code&gt; – &lt;strong&gt;your actual user files&lt;/strong&gt;. If your cloud grows a lot, you can later move this &lt;code&gt;data/&lt;/code&gt; directory to a separate, larger volume; for now one volume for everything is enough.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Updates: only one major version per step.&lt;/strong&gt; From 34 to 35, never directly to 36. Bump the image tag (&lt;code&gt;nextcloud:35-apache&lt;/code&gt;), then &lt;code&gt;docker compose pull&lt;/code&gt; and &lt;code&gt;docker compose up -d&lt;/code&gt;. The container runs the necessary &lt;code&gt;occ upgrade&lt;/code&gt; &lt;strong&gt;automatically&lt;/strong&gt; on start. &lt;strong&gt;Always make a backup first.&lt;/strong&gt; Bump the cron sidecar to the same version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check security.&lt;/strong&gt; After setup, run the official &lt;a href="https://scan.nextcloud.com/" rel="noopener noreferrer"&gt;Nextcloud Security Scan&lt;/a&gt; against your domain (target: grade A) and work through the warnings from the admin overview. The overview under &lt;strong&gt;Administration → Overview&lt;/strong&gt; runs automatic checks and shows exactly what's still missing. Keep Nextcloud up to date – security holes are closed promptly, but only if you update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest about the effort:&lt;/strong&gt; a self-hosted cloud wants to be maintained. Reckon with an update pass roughly monthly and a regular look at the admin overview. In return, the data then really belongs to you.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://serverkueche.de/en/tutorials/self-host-nextcloud/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Self-hosting Immich: your private photo backup behind Traefik</title>
      <dc:creator>serverkueche.de</dc:creator>
      <pubDate>Sat, 25 Jul 2026 07:06:44 +0000</pubDate>
      <link>https://dev.to/serverkueche/self-hosting-immich-your-private-photo-backup-behind-traefik-48ma</link>
      <guid>https://dev.to/serverkueche/self-hosting-immich-your-private-photo-backup-behind-traefik-48ma</guid>
      <description>&lt;p&gt;Thousands of photos and videos from your phone – and all of them at Google or Apple. &lt;strong&gt;Immich&lt;/strong&gt; brings them back to your server: a self-hosted photo service that comes astonishingly close to Google Photos, including automatic phone upload, timeline, search and face recognition. In this recipe we set up Immich cleanly behind Traefik.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we building?
&lt;/h2&gt;

&lt;p&gt;By the end, &lt;strong&gt;Immich v3&lt;/strong&gt; runs behind your Traefik proxy, reachable at &lt;code&gt;https://photos.YOUR_DOMAIN&lt;/code&gt; with valid HTTPS. The official &lt;strong&gt;Immich phone app&lt;/strong&gt; then backs up your new photos and videos automatically to your server – like the cloud backup of Google Photos, except the images live exclusively with you. Via the web interface you browse your timeline, search by full text ("beach", "dog") and let Immich group faces.&lt;/p&gt;

&lt;p&gt;Immich consists of &lt;strong&gt;four containers&lt;/strong&gt;: the server (web + API), a machine-learning service (for search and face recognition), a PostgreSQL database with a vector extension, and a cache (Valkey). Sounds like a lot – but the official template takes most of it off your hands, and we only hang the server onto Traefik.&lt;/p&gt;

&lt;p&gt;The appeal over Google Photos: the images don't leave your server, there are no storage subscriptions and no automatic analysis of your shots by a corporation. In return, you bear responsibility for operation and – very importantly – for &lt;strong&gt;backups&lt;/strong&gt;: if the server dies and you have no backup, the photos are gone. That's exactly why the backup below is not optional but mandatory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Immich moves fast – pin the version&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Immich develops rapidly. Database migrations run automatically on update and are &lt;strong&gt;not backward-compatible&lt;/strong&gt; – a downgrade is no longer cleanly possible afterwards. So &lt;strong&gt;always pin a fixed version&lt;/strong&gt; (here &lt;code&gt;v3.0.3&lt;/code&gt;) instead of &lt;code&gt;release&lt;/code&gt; or &lt;code&gt;latest&lt;/code&gt;, and make a &lt;strong&gt;backup before every update&lt;/strong&gt;. That way you decide when to update.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A running &lt;strong&gt;Traefik reverse proxy&lt;/strong&gt; with the &lt;code&gt;proxy&lt;/code&gt; network and the Let's Encrypt resolver &lt;code&gt;le&lt;/code&gt; – see &lt;a href="https://serverkueche.de/en/tutorials/traefik-reverse-proxy/" rel="noopener noreferrer"&gt;reverse proxy with Traefik&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A subdomain &lt;code&gt;photos.YOUR_DOMAIN&lt;/code&gt; with a DNS record to your server IP – see &lt;a href="https://serverkueche.de/en/tutorials/connect-domain-to-server/" rel="noopener noreferrer"&gt;connecting a domain to your server&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;backup&lt;/strong&gt;. Your photo collection is irreplaceable – first set up &lt;a href="https://serverkueche.de/en/tutorials/restic-backups/" rel="noopener noreferrer"&gt;backups with Restic&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 How big does the server need to be?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Immich is the most resource-hungry service in the Serverküche. Officially, &lt;strong&gt;at least 6 GB RAM (8 recommended)&lt;/strong&gt; are required – the machine-learning service for face recognition and search in particular needs memory. The tested &lt;strong&gt;VPS 1000 with 8 GB RAM&lt;/strong&gt; just meets that but has little reserve. For a large library or several users, a server with &lt;strong&gt;16 GB RAM&lt;/strong&gt; (e.g. VPS 2000) is more relaxed. On a 4 GB server, Immich only runs with &lt;strong&gt;ML turned off&lt;/strong&gt; (see "When things go wrong"). And: photos need space – plan for enough disk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;How much RAM your photo library including AI search really needs is calculated by the &lt;a href="https://serverkueche.de/en/serverempfehlung/" rel="noopener noreferrer"&gt;server calculator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create the DNS record
&lt;/h3&gt;

&lt;p&gt;Create an entry &lt;code&gt;photos.YOUR_DOMAIN&lt;/code&gt; that points to your server IP, and check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short photos.YOUR_DOMAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server IP must come back – otherwise Traefik won't fetch a certificate later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Prepare the folder and .env
&lt;/h3&gt;

&lt;p&gt;Immich is configured via a &lt;code&gt;.env&lt;/code&gt; file. Create the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/immich &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/immich
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; with the core values (set a strong DB password – &lt;strong&gt;letters and digits only&lt;/strong&gt;, no special characters, Immich's DB init doesn't like those):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;UPLOAD_LOCATION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;./library&lt;/span&gt;
&lt;span class="py"&gt;DB_DATA_LOCATION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;./postgres&lt;/span&gt;
&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_DB_PASSWORD&lt;/span&gt;
&lt;span class="py"&gt;DB_USERNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;
&lt;span class="py"&gt;DB_DATABASE_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;immich&lt;/span&gt;
&lt;span class="py"&gt;IMMICH_VERSION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;v3.0.3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UPLOAD_LOCATION&lt;/code&gt;&lt;/strong&gt; – this is where your &lt;strong&gt;photos and videos&lt;/strong&gt; land. It's the most important directory for the backup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DB_DATA_LOCATION&lt;/code&gt;&lt;/strong&gt; – the PostgreSQL data. Must be on a &lt;strong&gt;local disk&lt;/strong&gt; (no network drive/NFS – the DB resents that).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IMMICH_VERSION&lt;/code&gt;&lt;/strong&gt; – the pinned version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Create the compose.yaml
&lt;/h3&gt;

&lt;p&gt;We adopt the official Immich template and only add the Traefik labels on the &lt;code&gt;immich-server&lt;/code&gt;. Create &lt;code&gt;compose.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;immich&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;immich-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/immich-app/immich-server:${IMMICH_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${UPLOAD_LOCATION}:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOSTNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_HOSTNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.immich.rule=Host(`photos.YOUR_DOMAIN`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.immich.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.immich.tls.certresolver=le"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.immich.loadbalancer.server.port=2283"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;immich-machine-learning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;model-cache:/cache&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/valkey/valkey:9&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_USERNAME}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_DATABASE_NAME}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${DB_DATA_LOCATION}:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${DB_USERNAME}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${DB_DATABASE_NAME}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;model-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only &lt;code&gt;immich-server&lt;/code&gt; is on the &lt;code&gt;proxy&lt;/code&gt; network&lt;/strong&gt; and carries Traefik labels. The ML service, Valkey and the database stay exclusively on the internal &lt;code&gt;default&lt;/code&gt; network – not reachable from outside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;loadbalancer.server.port=2283&lt;/code&gt;&lt;/strong&gt; – Immich listens on port 2283 in the container.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;database&lt;/strong&gt; is deliberately the prebuilt Immich Postgres image with the vector extension &lt;em&gt;VectorChord&lt;/em&gt; (for image search). Don't just use a standard &lt;code&gt;postgres&lt;/code&gt; – the extension is then missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Valkey&lt;/strong&gt; is the Redis successor; the service is still called &lt;code&gt;redis&lt;/code&gt; in the Immich template for compatibility reasons.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;healthcheck&lt;/strong&gt; on the database reports when Postgres is really ready. Via &lt;code&gt;depends_on: … condition: service_healthy&lt;/code&gt;, &lt;code&gt;immich-server&lt;/code&gt; only starts then – this prevents migration errors and the "database not reachable" start on the first boot.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Applying changes to the .env&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you later change something in the &lt;code&gt;.env&lt;/code&gt;, a &lt;code&gt;docker compose restart&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; enough – Docker only reads the environment variables when &lt;strong&gt;recreating&lt;/strong&gt; the containers. Use &lt;code&gt;docker compose up -d&lt;/code&gt; then; Compose detects the change and rebuilds the affected containers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Allow large uploads (Traefik timeout)
&lt;/h3&gt;

&lt;p&gt;This is &lt;strong&gt;the&lt;/strong&gt; stumbling block with Immich behind Traefik: by default Traefik aborts connections after &lt;strong&gt;60 seconds&lt;/strong&gt; (&lt;code&gt;readTimeout&lt;/code&gt;). When uploading large videos from the phone this leads to aborted uploads (error 502/499). Increase the timeout on the &lt;code&gt;websecure&lt;/code&gt; entrypoint in your &lt;strong&gt;Traefik configuration&lt;/strong&gt; (the &lt;code&gt;traefik&lt;/code&gt; service from the &lt;a href="https://serverkueche.de/en/tutorials/traefik-reverse-proxy/" rel="noopener noreferrer"&gt;Traefik tutorial&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ... your existing lines ...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.transport.respondingTimeouts.readTimeout=600s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Traefik afterwards (&lt;code&gt;docker compose up -d&lt;/code&gt; in the Traefik folder). Unlike nginx, Traefik has no fixed size limit for uploads – only this timeout has to be high.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Start and run through the initial setup
&lt;/h3&gt;

&lt;p&gt;Pull the images (several GB – this takes a while) and start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; immich-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The images are several gigabytes in size – the first &lt;code&gt;pull&lt;/code&gt; takes a few minutes depending on your connection. Then check that all four containers are running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;immich-server&lt;/code&gt;, &lt;code&gt;immich-machine-learning&lt;/code&gt;, &lt;code&gt;redis&lt;/code&gt; and &lt;code&gt;database&lt;/code&gt; with status &lt;code&gt;running&lt;/code&gt; (the database &lt;code&gt;healthy&lt;/code&gt;). Wait until &lt;code&gt;Immich Server is listening on http://[::1]:2283 [v3.0.3]&lt;/code&gt; appears in the log. Then open &lt;code&gt;https://photos.YOUR_DOMAIN&lt;/code&gt;. Traefik fetches the Let's Encrypt certificate on the first access (be patient for a moment). On the very first start, Immich greets you with &lt;strong&gt;"Welcome to Immich"&lt;/strong&gt; and the choice of whether you &lt;strong&gt;start fresh&lt;/strong&gt; or restore a backup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/immich-willkommen.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/immich-willkommen.png" title="First launch: choose \" alt="The welcome screen of Immich with the options " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Getting Started&lt;/strong&gt; (you only need Restore if you're importing a backup). Then you create the &lt;strong&gt;administrator account&lt;/strong&gt; – as the first user you automatically become admin:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6er9q1b9my6g7oe45xdg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6er9q1b9my6g7oe45xdg.png" alt="The Immich form " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the first login, a short &lt;strong&gt;setup wizard&lt;/strong&gt; guides you through theme, language and basic privacy settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn09i1ywxmofik6cstiwv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn09i1ywxmofik6cstiwv.png" alt="The Immich setup wizard greets the new administrator" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that you land on your (still empty) &lt;strong&gt;timeline&lt;/strong&gt; – the heart of Immich:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwv16v1wgxejy9sk3368y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwv16v1wgxejy9sk3368y.png" alt="The empty photo timeline of Immich with the prompt to upload the first photo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Connect the phone app
&lt;/h3&gt;

&lt;p&gt;The real benefit comes from the app. Install &lt;strong&gt;Immich&lt;/strong&gt; from the App Store or Play Store (or F-Droid). At launch it asks for the &lt;strong&gt;server address&lt;/strong&gt; – enter &lt;code&gt;https://photos.YOUR_DOMAIN&lt;/code&gt; and log in with your account. Then, in the app settings, enable the &lt;strong&gt;backup&lt;/strong&gt; and choose the albums to be uploaded.&lt;/p&gt;

&lt;p&gt;From now on your phone backs up new photos automatically to your server – in the background and over Wi-Fi. The first pass of a large library takes a while; after that only new shots are added.&lt;/p&gt;

&lt;p&gt;After the upload, Immich keeps working in the background: it generates thumbnails, reads the capture metadata (date, location) and lets the ML service &lt;strong&gt;recognize faces&lt;/strong&gt; and index the images for &lt;strong&gt;smart search&lt;/strong&gt;. These jobs run for some time after the first big import – so faces and search hits only appear gradually. You see the progress under &lt;strong&gt;Administration → Job queues&lt;/strong&gt;. In the sidebar you then find the typical photo features: &lt;strong&gt;Explore&lt;/strong&gt; (by people and places), the &lt;strong&gt;map&lt;/strong&gt; with geolocation, &lt;strong&gt;albums&lt;/strong&gt; for sharing and the &lt;strong&gt;memories&lt;/strong&gt; ("a year ago").&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Videos &amp;amp; transcoding on the VPS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Immich converts videos for playback in the browser (transcoding). On a normal VPS &lt;strong&gt;without a graphics card&lt;/strong&gt; this happens via the CPU – with many or long videos this is noticeably slower and maxes out cores. For a private phone backup this is usually no problem; whoever has a lot of video should choose the server correspondingly larger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡 More users&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the family, create further accounts under &lt;strong&gt;Administration → Users&lt;/strong&gt; – each gets its own separate library. Registration from outside is off by default; new users only arise via the admin account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Your photos are on the internet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As soon as Immich is reachable via &lt;code&gt;photos.YOUR_DOMAIN&lt;/code&gt;, the login page is open on the net. So set a &lt;strong&gt;long, unique password&lt;/strong&gt; for the admin and all user accounts and enable &lt;strong&gt;two-factor authentication&lt;/strong&gt; in the account settings. Whoever wants to be extra safe makes Immich reachable only via a VPN – but for automatic phone upload, direct HTTPS reachability is usually the more practical way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When things go wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Uploads of large videos abort after about a minute (error 502 or 499).&lt;/strong&gt; Traefik's &lt;code&gt;readTimeout&lt;/code&gt; (default 60 s) kicks in. Set it to &lt;code&gt;600s&lt;/code&gt; (or higher) as in step 4 and restart Traefik. This is by far the most common Immich-behind-proxy error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;immich-machine-learning&lt;/code&gt; container crashes or exits with "exit 137", search and face recognition don't work.&lt;/strong&gt; Too little RAM – the container was killed by the system (out of memory). Give the server more memory, or &lt;strong&gt;disable ML&lt;/strong&gt; by removing the service &lt;code&gt;immich-machine-learning&lt;/code&gt; from the &lt;code&gt;compose.yaml&lt;/code&gt;. Immich then runs without face recognition and smart search, but upload and timeline work normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After an update the containers no longer start or report migration errors.&lt;/strong&gt; Server, machine learning and database must run on the &lt;strong&gt;same&lt;/strong&gt; version. Set the new &lt;code&gt;IMMICH_VERSION&lt;/code&gt; in the &lt;code&gt;.env&lt;/code&gt; and update &lt;strong&gt;all&lt;/strong&gt; containers together (&lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;). A downgrade after a migration isn't possible – only a reset from the backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immich shows "maintenance mode" / "temporarily unavailable".&lt;/strong&gt; Immich v3 starts into a maintenance mode under certain database states. The log (&lt;code&gt;docker compose logs immich-server&lt;/code&gt;) then contains a URL with a &lt;strong&gt;one-time token&lt;/strong&gt; (&lt;code&gt;…/maintenance?token=…&lt;/code&gt;) – through it you log into the maintenance mode and choose "restart" or end it. After that the normal interface is back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container won't start, "permission denied" on the database or upload directory.&lt;/strong&gt; &lt;code&gt;UPLOAD_LOCATION&lt;/code&gt; or &lt;code&gt;DB_DATA_LOCATION&lt;/code&gt; have the wrong access rights or are on a network drive. Put both on a local disk and make sure Docker may write into them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance &amp;amp; backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two things belong in the backup – consistently together.&lt;/strong&gt; Your &lt;strong&gt;photos&lt;/strong&gt; (&lt;code&gt;UPLOAD_LOCATION&lt;/code&gt;, i.e. &lt;code&gt;~/immich/library&lt;/code&gt;) and the &lt;strong&gt;database&lt;/strong&gt;. The DB dump contains only the metadata; without the matching files it's worthless, and vice versa. Cleanest is to stop Immich briefly and back up both together:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker compose stop immich-server
  docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; database pg_dump &lt;span class="nt"&gt;--clean&lt;/span&gt; &lt;span class="nt"&gt;--if-exists&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;immich &lt;span class="nt"&gt;--username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; immich-db.sql.gz
  docker compose start immich-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back up the dump together with the &lt;code&gt;library&lt;/code&gt; folder &lt;strong&gt;encrypted and off-site&lt;/strong&gt; with &lt;a href="https://serverkueche.de/en/tutorials/restic-backups/" rel="noopener noreferrer"&gt;Restic&lt;/a&gt;. Alternatively, Immich can generate automatic DB dumps under &lt;strong&gt;Administration → Job queues&lt;/strong&gt; – you still have to back up the photos separately.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rehearse the worst case once.&lt;/strong&gt; A backup you've never restored is just a hope. To restore, you import the DB dump into a fresh Immich instance of the same version and place the &lt;code&gt;library&lt;/code&gt; folder in the same spot – Immich offers the option &lt;strong&gt;"Restore from database"&lt;/strong&gt; on the first start for this (the second button from step 5). What matters is the &lt;strong&gt;same version&lt;/strong&gt;: the database state and the program must match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup before every update.&lt;/strong&gt; Because migrations aren't reversible, the backup is your only way back. Back up first, then raise &lt;code&gt;IMMICH_VERSION&lt;/code&gt;, then &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest about the effort:&lt;/strong&gt; Immich often brings several releases per month. You don't have to follow every one – but before a jump across several versions, read the release notes, and stick to the pinned version until you deliberately update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep an eye on disk space.&lt;/strong&gt; Photos and videos grow steadily; Immich additionally creates thumbnails and converted videos. Monitor the disk usage (e.g. with &lt;a href="https://serverkueche.de/en/tutorials/uptime-kuma-monitoring/" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt;) so the server doesn't fill up.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://serverkueche.de/en/tutorials/self-host-immich-photos/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Installing Uptime Kuma: server monitoring behind Traefik</title>
      <dc:creator>serverkueche.de</dc:creator>
      <pubDate>Fri, 24 Jul 2026 21:44:39 +0000</pubDate>
      <link>https://dev.to/serverkueche/installing-uptime-kuma-server-monitoring-behind-traefik-15af</link>
      <guid>https://dev.to/serverkueche/installing-uptime-kuma-server-monitoring-behind-traefik-15af</guid>
      <description>&lt;p&gt;Traefik is up – now we hang the first real app behind it. &lt;strong&gt;Uptime Kuma&lt;/strong&gt; is ideal for that: tiny, immediately useful and without a database. It monitors your services and raises the alarm when one fails – from now on &lt;em&gt;you&lt;/em&gt; notice first that something is stuck, not your users. Along the way you learn the pattern every further app repeats.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we building?
&lt;/h2&gt;

&lt;p&gt;By the end, &lt;strong&gt;Uptime Kuma 2.4&lt;/strong&gt; runs at &lt;code&gt;https://status.YOUR_DOMAIN&lt;/code&gt;, secured via Traefik with automatic HTTPS. You've set up the first monitor (which checks every minute whether a service responds), connected a notification and optionally a public status page. For the first time we bind a service that &lt;strong&gt;doesn't listen on port 80&lt;/strong&gt; – an important detail for all the following apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A running &lt;a href="https://serverkueche.de/en/tutorials/traefik-reverse-proxy/" rel="noopener noreferrer"&gt;Traefik reverse proxy&lt;/a&gt; including the &lt;code&gt;proxy&lt;/code&gt; network and a working Let's Encrypt resolver &lt;code&gt;le&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A DNS record &lt;code&gt;status.YOUR_DOMAIN&lt;/code&gt; that &lt;a href="https://serverkueche.de/en/tutorials/connect-domain-to-server/" rel="noopener noreferrer"&gt;points to the server&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step by step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: The compose.yaml
&lt;/h3&gt;

&lt;p&gt;Own folder, own file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/uptime-kuma &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;uptime-kuma&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;louislam/uptime-kuma:2&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;kuma-data:/app/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.rule=Host(`status.YOUR_DOMAIN`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.tls.certresolver=le"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.kuma.loadbalancer.server.port=3001"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kuma-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You know this from the Traefik tutorial – except for &lt;strong&gt;one new, decisive line&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.kuma.loadbalancer.server.port=3001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uptime Kuma listens internally on &lt;strong&gt;port 3001&lt;/strong&gt;, not on 80. This line tells Traefik which port to forward the requests to. Without it, Traefik guesses wrong and you get a &lt;code&gt;Bad Gateway&lt;/code&gt;. Remember the label – every app that doesn't run on port 80 needs it.&lt;/p&gt;

&lt;p&gt;The rest is the familiar pattern: &lt;strong&gt;no &lt;code&gt;ports:&lt;/code&gt;&lt;/strong&gt; (only reachable via Traefik), &lt;code&gt;proxy&lt;/code&gt; network, named volume for the data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Start and first login
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Kuma is ready, the log ends with this line – from here the service accepts requests (&lt;code&gt;Ctrl+C&lt;/code&gt; only ends the following):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SERVER] INFO: Welcome to Uptime Kuma
[SERVER] INFO: Uptime Kuma Version: 2.4.0
[SETUP-DATABASE] INFO: Listening on:
[SETUP-DATABASE] INFO: -  http://localhost:3001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first start, Kuma creates its database in the volume – that takes a few seconds. Then open &lt;code&gt;https://status.YOUR_DOMAIN&lt;/code&gt; in the browser. Uptime Kuma 2.x first asks for the &lt;strong&gt;database&lt;/strong&gt; – for a setup like ours, &lt;strong&gt;SQLite&lt;/strong&gt; is the right, simplest choice (select it, click &lt;strong&gt;Next&lt;/strong&gt;). Right after that you create the &lt;strong&gt;admin account&lt;/strong&gt; (username + strong password).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopbkqvm3p8aongkaasw5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopbkqvm3p8aongkaasw5.png" alt="Uptime Kuma's initial setup: choose the language and create the admin account" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Create the admin account immediately&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As long as no admin account exists, &lt;strong&gt;anyone&lt;/strong&gt; who opens the page can create one. Do this right after the first start – not "later".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3: Create the first monitor
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;monitor&lt;/strong&gt; is a recurring check. Click &lt;strong&gt;Add New Monitor&lt;/strong&gt; and create one for Traefik itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monitor type:&lt;/strong&gt; &lt;code&gt;HTTP(s)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Friendly name:&lt;/strong&gt; &lt;code&gt;Traefik Dashboard&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;code&gt;https://traefik.YOUR_DOMAIN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat interval:&lt;/strong&gt; &lt;code&gt;60&lt;/code&gt; seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; the Traefik dashboard is protected by basic auth (step 7 of the &lt;a href="https://serverkueche.de/en/tutorials/traefik-reverse-proxy/" rel="noopener noreferrer"&gt;Traefik tutorial&lt;/a&gt;) – so choose &lt;strong&gt;HTTP Basic Auth&lt;/strong&gt; as the authentication method below and enter the user and password. Without credentials, Kuma only gets &lt;code&gt;401&lt;/code&gt; and reports the monitor as &lt;strong&gt;Down&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save – after a few seconds the monitor is &lt;strong&gt;Up&lt;/strong&gt; (green) and shows the response time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftsbjbr4fy5bz2hupc0nc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftsbjbr4fy5bz2hupc0nc.png" alt="The Uptime Kuma dashboard with two running monitors – both green with 100% availability and " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Besides the simple &lt;code&gt;HTTP(s)&lt;/code&gt; check, it pays to choose the matching &lt;strong&gt;monitor type&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP(s) – Keyword:&lt;/strong&gt; additionally checks whether a certain word appears in the response text. This detects "the server responds, but shows an error page".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP port:&lt;/strong&gt; for services without a web interface (e.g. a database, an SSH port). Only checks whether the port is open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ping:&lt;/strong&gt; the simplest reachability test via ICMP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker container:&lt;/strong&gt; checks the container status directly via the Docker socket – handy for internal services without their own domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push:&lt;/strong&gt; here &lt;em&gt;the monitored service&lt;/em&gt; calls Kuma regularly. Ideal for cron jobs and backups: if the script doesn't report in time, Kuma raises the alarm ("dead man's switch").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create the matching monitor for every important service.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check &lt;strong&gt;public services via their real domain&lt;/strong&gt; (&lt;code&gt;https://…&lt;/code&gt;), not via &lt;code&gt;localhost&lt;/code&gt;. This way you simultaneously test that Traefik and the certificate work from outside – not just that the container is running.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the &lt;code&gt;HTTP(s)&lt;/code&gt; monitor, the option &lt;strong&gt;"certificate expiry notification"&lt;/strong&gt; is also worth it: Kuma then warns in good time before a TLS certificate expires. For services behind Traefik, Let's Encrypt renews automatically – but this very automation occasionally fails silently (a DNS record is changed, port 80 accidentally closed). The monitor is your safety net and reports in while there are still days to fix it, instead of visitors suddenly facing a certificate warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Set up notifications (email and Telegram)
&lt;/h3&gt;

&lt;p&gt;A monitor without an alarm is just a pretty chart. Under &lt;strong&gt;Settings → Notifications → Setup Notification&lt;/strong&gt; you create a channel. Uptime Kuma supports over 90 – we set up the two most common completely: &lt;strong&gt;email&lt;/strong&gt; for the classic message and &lt;strong&gt;Telegram&lt;/strong&gt; for push straight to the phone.&lt;/p&gt;

&lt;h4&gt;
  
  
  Email (SMTP)
&lt;/h4&gt;

&lt;p&gt;Choose "Email (SMTP)" as the &lt;strong&gt;notification service&lt;/strong&gt; and enter your mail provider's data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hostname / port:&lt;/strong&gt; e.g. &lt;code&gt;smtp.YOUR_PROVIDER.com&lt;/code&gt; and &lt;code&gt;587&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; &lt;code&gt;STARTTLS&lt;/code&gt; (port 587) or &lt;code&gt;TLS/SSL&lt;/code&gt; (port 465)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username / password:&lt;/strong&gt; your SMTP credentials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From / to address:&lt;/strong&gt; which address the warning comes from and which it's sent to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnw5gm2sqd9sp7rzuijc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnw5gm2sqd9sp7rzuijc.png" alt="The email notification (SMTP) in Uptime Kuma: hostname, port, security and from address" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Test&lt;/strong&gt; – within a few seconds a test message lands in the mailbox. Only when it really arrives are the credentials and port correct. Don't forget to save.&lt;/p&gt;

&lt;h4&gt;
  
  
  Telegram
&lt;/h4&gt;

&lt;p&gt;Telegram is ideal for instant push alerts to the phone – without your own mail server. You need two pieces of information:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bot token:&lt;/strong&gt; message &lt;a href="https://t.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt; in Telegram, send &lt;code&gt;/newbot&lt;/code&gt;, assign a name – BotFather replies with the &lt;strong&gt;token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat ID:&lt;/strong&gt; send your new bot any message, then open &lt;code&gt;https://api.telegram.org/bot&amp;lt;YOUR_TOKEN&amp;gt;/getUpdates&lt;/code&gt; in the browser and read the &lt;code&gt;chat.id&lt;/code&gt; from the response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Enter the token and chat ID into the Telegram notification (Uptime Kuma links both helpers directly in the dialog) and click &lt;strong&gt;Test&lt;/strong&gt; – the message should appear in the chat immediately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnx5ppuzma38wtlqzhe5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnx5ppuzma38wtlqzhe5i.png" alt="The Telegram notification in Uptime Kuma: bot token and chat ID, with a direct link to the BotFather" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Otherwise the alarm stays silent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A configured notification does &lt;strong&gt;not&lt;/strong&gt; take effect automatically. Enable it in each monitor (checkbox in the monitor form) or, in the notification dialog, turn on "Enabled by default" and "Apply on all existing monitors".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want push messages entirely under your own control (without a Telegram server), &lt;strong&gt;ntfy&lt;/strong&gt; comes later – self-hosted, with its own recipe in the series.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: A public status page (optional)
&lt;/h3&gt;

&lt;p&gt;Kuma can publish a &lt;strong&gt;status page&lt;/strong&gt; where visitors see whether your services are running. Here's how:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Status Pages → New Status Page&lt;/strong&gt;, then assign a &lt;strong&gt;name&lt;/strong&gt; (e.g. "Serverküche Status") and a &lt;strong&gt;slug&lt;/strong&gt; (the URL, e.g. &lt;code&gt;serverkueche&lt;/code&gt;) and click &lt;strong&gt;Next&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the editor, &lt;strong&gt;Add Group&lt;/strong&gt; (e.g. "Services"), and below it, add the desired &lt;strong&gt;monitors&lt;/strong&gt; via the selection field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save&lt;/strong&gt; at the top right – done. The page is then publicly reachable at &lt;code&gt;https://status.YOUR_DOMAIN/status/serverkueche&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only include what really everyone may see – better leave internal services out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzkvfb0dfijtar8ifbzhi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzkvfb0dfijtar8ifbzhi.png" alt="The public status page " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Avoid false alarms – fine-tune the alarm behavior
&lt;/h3&gt;

&lt;p&gt;A monitor that raises the alarm at every brief network hiccup is quickly ignored – and then you miss the real outage. In the monitor form you set the behavior appropriately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retries:&lt;/strong&gt; the service only counts as "Down" after &lt;em&gt;n&lt;/em&gt; failed checks. &lt;code&gt;2&lt;/code&gt;–&lt;code&gt;3&lt;/code&gt; filters out individual dropouts without obscuring real outages for long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat interval on failure:&lt;/strong&gt; Kuma may check more often in the error case (e.g. every 20 seconds) to detect recovery quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend notification:&lt;/strong&gt; Kuma can remind you every &lt;em&gt;x&lt;/em&gt; minutes as long as a service is down – useful so an outage at night doesn't get lost in a single mail.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Plan maintenance windows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you plan an update with downtime, create a &lt;strong&gt;maintenance window&lt;/strong&gt; under &lt;strong&gt;Maintenance&lt;/strong&gt;. Kuma then pauses the alarms for the affected monitors – so you (and the status page) don't get false alarms while you're at work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 7: Monitor cron jobs with a push monitor
&lt;/h3&gt;

&lt;p&gt;Classic monitors check from outside whether a service &lt;em&gt;responds&lt;/em&gt;. For things that &lt;strong&gt;run silently in the background&lt;/strong&gt; – a nightly backup, a sync script, a cron job – the &lt;strong&gt;push monitor&lt;/strong&gt; flips the principle: not Kuma asks, but &lt;em&gt;your script reports in&lt;/em&gt;. If the report fails to come, Kuma raises the alarm – the classic "dead man's switch".&lt;/p&gt;

&lt;p&gt;Create a monitor of type &lt;strong&gt;Push&lt;/strong&gt;. Kuma then shows you a unique &lt;strong&gt;push URL&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://status.YOUR_DOMAIN/api/push/YOUR_TOKEN?status=up&amp;amp;msg=OK&amp;amp;ping=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You call this URL at the end of your script – for example after a successfully completed backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ... your backup command ...&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"https://status.YOUR_DOMAIN/api/push/YOUR_TOKEN?status=up&amp;amp;msg=Backup+OK"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the &lt;strong&gt;check interval&lt;/strong&gt; in Kuma a bit more generously than your cron cadence (if the backup runs hourly, give Kuma e.g. 90 minutes of tolerance). If no &lt;code&gt;curl&lt;/code&gt; comes in that time, the monitor goes to &lt;strong&gt;Down&lt;/strong&gt; and you're notified – so you learn about a backup that did &lt;em&gt;not&lt;/em&gt; run, not only when you urgently need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Secure the admin login with 2FA
&lt;/h3&gt;

&lt;p&gt;Your Kuma login protects access to all monitors, the stored notification credentials and the status-page configuration – and it's publicly on the net. So enable &lt;strong&gt;two-factor authentication&lt;/strong&gt;: under &lt;strong&gt;Settings → Security → Two-Factor Authentication&lt;/strong&gt;. Kuma shows a QR code you scan with an authenticator app (e.g. Aegis or 2FAS); to activate, you enter the generated code once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Secure the recovery&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keep the TOTP secret or a second authenticator in a safe place (password manager). If you lose your phone &lt;strong&gt;and&lt;/strong&gt; have no copy, you can otherwise only get back in via the database in the &lt;code&gt;kuma-data&lt;/code&gt; volume.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When things go wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Bad Gateway&lt;/code&gt; (502) when opening &lt;code&gt;status.YOUR_DOMAIN&lt;/code&gt;.&lt;/strong&gt; Almost always the port label &lt;code&gt;traefik.http.services.kuma.loadbalancer.server.port=3001&lt;/code&gt; is missing or has a wrong port. Traefik then reaches the container but knocks on the wrong port. Check the label and run &lt;code&gt;docker compose up -d&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;404 page not found&lt;/code&gt; instead of Kuma.&lt;/strong&gt; As with every app behind Traefik: &lt;code&gt;traefik.enable=true&lt;/code&gt; set? Container on the &lt;code&gt;proxy&lt;/code&gt; network? Is the domain in the &lt;code&gt;Host(...)&lt;/code&gt; rule correct and does the DNS record &lt;code&gt;status.YOUR_DOMAIN&lt;/code&gt; point to the server? The Traefik dashboard shows under "HTTP Routers" whether &lt;code&gt;kuma&lt;/code&gt; is registered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The interface loads, but the live update stutters / breaks off.&lt;/strong&gt; Kuma uses WebSockets. Traefik forwards those correctly by default – if the problem still occurs, it's usually an upstream CDN/proxy (e.g. Cloudflare in "proxy" mode) that blocks WebSockets. For direct operation behind Traefik, no extra configuration is needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After a re-setup all monitors are gone.&lt;/strong&gt; The &lt;code&gt;kuma-data&lt;/code&gt; volume was deleted (e.g. by &lt;code&gt;docker compose down -v&lt;/code&gt;). All configuration and history lives solely in this volume – that's why it's at the top of the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance &amp;amp; backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Back up:&lt;/strong&gt; the complete heart of Kuma is the &lt;code&gt;kuma-data&lt;/code&gt; volume (a SQLite database). Back it up regularly – if it's gone, all monitors and the history are gone. We build the off-site backup for it in the &lt;a href="https://serverkueche.de/en/tutorials/restic-backups/" rel="noopener noreferrer"&gt;Restic tutorial&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates:&lt;/strong&gt; the tag &lt;code&gt;:2&lt;/code&gt; stays on the 2.x series and brings bug fixes with &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;. Before a jump to a new major version (e.g. later &lt;code&gt;:3&lt;/code&gt;), read the release notes and back up the volume first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coming from 1.x?&lt;/strong&gt; The switch to &lt;code&gt;:2&lt;/code&gt; &lt;strong&gt;migrates the SQLite database automatically on the first start&lt;/strong&gt; – that can take a moment, and going back to &lt;code&gt;:1&lt;/code&gt; is not intended afterwards. So back up the &lt;code&gt;kuma-data&lt;/code&gt; volume &lt;strong&gt;beforehand&lt;/strong&gt;, then you're on the safe side. New installations (like above) aren't affected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest limitation:&lt;/strong&gt; a monitor that runs &lt;strong&gt;on the same server&lt;/strong&gt; as the monitored services can't warn you when the whole server fails – then Kuma is offline too. For that case, add an &lt;strong&gt;external&lt;/strong&gt; watcher. Two cheap ways: a &lt;strong&gt;second Uptime Kuma&lt;/strong&gt; on a small server (or at home) that only monitors this instance via HTTP – or a &lt;strong&gt;free external ping service&lt;/strong&gt; that pings your public status page. This way you also get a notice when the whole host is gone – the only case a local monitor inherently can't cover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that you've internalized the app pattern and monitor everything you hang behind Traefik from now on. What every further app requires are &lt;a href="https://serverkueche.de/en/tutorials/restic-backups/" rel="noopener noreferrer"&gt;encrypted off-site backups with Restic&lt;/a&gt; – so your data survives a server crash.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://serverkueche.de/en/tutorials/uptime-kuma-monitoring/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>monitoring</category>
      <category>docker</category>
    </item>
    <item>
      <title>Vaultwarden: your own password manager behind Traefik</title>
      <dc:creator>serverkueche.de</dc:creator>
      <pubDate>Fri, 24 Jul 2026 21:36:20 +0000</pubDate>
      <link>https://dev.to/serverkueche/vaultwarden-your-own-password-manager-behind-traefik-b1c</link>
      <guid>https://dev.to/serverkueche/vaultwarden-your-own-password-manager-behind-traefik-b1c</guid>
      <description>&lt;p&gt;Passwords belong in a password manager – but do they have to live in the cloud of some third-party provider? With &lt;strong&gt;Vaultwarden&lt;/strong&gt; you host your own, encrypted and under your control. It speaks the Bitwarden language, so you use the familiar Bitwarden apps and browser extensions – they just point at &lt;strong&gt;your&lt;/strong&gt; server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we building?
&lt;/h2&gt;

&lt;p&gt;By the end, &lt;strong&gt;Vaultwarden 1.36&lt;/strong&gt; runs in a container behind your Traefik proxy, reachable at &lt;code&gt;https://vault.YOUR_DOMAIN&lt;/code&gt; with a valid HTTPS certificate. Vaultwarden is a lean reimplementation of the Bitwarden server written in Rust: API-compatible, but frugal enough to run on a small VPS. You connect the official &lt;strong&gt;Bitwarden clients&lt;/strong&gt; (browser extension, phone app, desktop) with your server, store your credentials – end-to-end encrypted – and manage the service via a secured &lt;strong&gt;admin panel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your passwords thus live on your server, in your country, under your backup – and you pay no subscription fee for features like 2FA or attachments.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Vaultwarden ≠ Bitwarden&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vaultwarden is an &lt;strong&gt;unofficial&lt;/strong&gt;, community-driven project that reimplements the Bitwarden server API. It's not affiliated with Bitwarden Inc. For private and small-team use it's excellent; the clients (apps, extensions) are the real, official ones from Bitwarden.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A running &lt;strong&gt;Traefik reverse proxy&lt;/strong&gt; with the shared &lt;code&gt;proxy&lt;/code&gt; network and the Let's Encrypt resolver &lt;code&gt;le&lt;/code&gt; – exactly the setup from the tutorial &lt;a href="https://serverkueche.de/en/tutorials/traefik-reverse-proxy/" rel="noopener noreferrer"&gt;reverse proxy with Traefik&lt;/a&gt;. Without Traefik this recipe doesn't work: Vaultwarden &lt;strong&gt;requires HTTPS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A subdomain, e.g. &lt;code&gt;vault.YOUR_DOMAIN&lt;/code&gt;, whose DNS record (A/AAAA) points to your server IP – see &lt;a href="https://serverkueche.de/en/tutorials/connect-domain-to-server/" rel="noopener noreferrer"&gt;connecting a domain to your server&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A working &lt;strong&gt;backup&lt;/strong&gt; of your server. A password manager is the place where data loss hurts most – if you haven't yet, first set up &lt;a href="https://serverkueche.de/en/tutorials/restic-backups/" rel="noopener noreferrer"&gt;backups with Restic&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to combine several services later, the &lt;a href="https://serverkueche.de/en/serverempfehlung/" rel="noopener noreferrer"&gt;server calculator&lt;/a&gt; helps with the right server size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Generate the ADMIN_TOKEN
&lt;/h3&gt;

&lt;p&gt;Vaultwarden has an admin panel at &lt;code&gt;/admin&lt;/code&gt;. Access to it is protected by an &lt;code&gt;ADMIN_TOKEN&lt;/code&gt;. You should &lt;strong&gt;not&lt;/strong&gt; store it in plaintext, but as a hash – then your panel password appears nowhere readable in the Compose file.&lt;/p&gt;

&lt;p&gt;Vaultwarden brings its own command for this. We run it briefly in a throwaway container:&lt;br&gt;
&lt;/p&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;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; vaultwarden/server:1.36.0 /vaultwarden &lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="nt"&gt;--preset&lt;/span&gt; owasp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command asks you twice for a password (input stays invisible) and then outputs an &lt;strong&gt;Argon2id hash&lt;/strong&gt; – a long string that starts like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate an Argon2id PHC string using the 'owasp' preset.

Password:
Confirm Password:

ADMIN_TOKEN='$argon2id$v=19$m=19456,t=2,p=1$FkxFEQ64Wy4zlQOWMI1fJ...$TxULe6MSND3By6GPVPKB1...'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy out the complete string between the quotes (including the &lt;code&gt;$argon2id$…&lt;/code&gt; parts) – you need it in a moment. The password you entered here is your &lt;strong&gt;admin panel password&lt;/strong&gt;; store it in your password manager (in the old one, for now).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Why the detour via the hash?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You could also set the &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; as a plaintext password. The hash is safer, though: even someone who gets hold of your &lt;code&gt;compose.yaml&lt;/code&gt; can't compute your panel password back from it. &lt;code&gt;--preset owasp&lt;/code&gt; chooses Argon2 parameters per the current OWASP recommendation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2: Create the compose.yaml
&lt;/h3&gt;

&lt;p&gt;Create a dedicated folder and change into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/vaultwarden &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/vaultwarden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the &lt;code&gt;compose.yaml&lt;/code&gt;. Replace &lt;code&gt;vault.YOUR_DOMAIN&lt;/code&gt; with your real subdomain and the &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; with the hash from step 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vaultwarden&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vaultwarden/server:1.36.0&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./vw-data:/data&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DOMAIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://vault.YOUR_DOMAIN"&lt;/span&gt;
      &lt;span class="na"&gt;SIGNUPS_ALLOWED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;ADMIN_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$$argon2id$$v=19$$m=19456,t=2,p=1$$FkxFE...$$TxULe..."&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.vaultwarden.rule=Host(`vault.YOUR_DOMAIN`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.vaultwarden.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.vaultwarden.tls.certresolver=le"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.vaultwarden.loadbalancer.server.port=80"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important lines in detail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;image: vaultwarden/server:1.36.0&lt;/code&gt;&lt;/strong&gt; – we deliberately pin the version instead of taking &lt;code&gt;latest&lt;/code&gt;. That way you update in a controlled manner (see "Maintenance"). For an even smaller image there's also &lt;code&gt;:1.36.0-alpine&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;volumes: ./vw-data:/data&lt;/code&gt;&lt;/strong&gt; – this is where your database (&lt;code&gt;db.sqlite3&lt;/code&gt;), the encryption keys and the attachments live. &lt;strong&gt;This volume is mandatory.&lt;/strong&gt; Without a volume Vaultwarden deliberately refuses to start, so you don't lose your data in an ephemeral container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DOMAIN&lt;/code&gt;&lt;/strong&gt; – the full HTTPS URL. Vaultwarden needs it for WebAuthn/2FA and invitation links, among other things. Must match your router rule exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SIGNUPS_ALLOWED: "true"&lt;/code&gt;&lt;/strong&gt; – allows registration for now so you can create your first account. We turn that off again shortly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ADMIN_TOKEN&lt;/code&gt;&lt;/strong&gt; – your hash from step 1. &lt;strong&gt;Important:&lt;/strong&gt; in a Compose file every dollar sign must be &lt;strong&gt;doubled&lt;/strong&gt; (&lt;code&gt;$&lt;/code&gt; → &lt;code&gt;$$&lt;/code&gt;), otherwise Compose tries to interpret it as a variable. So &lt;code&gt;$argon2id$…&lt;/code&gt; becomes &lt;code&gt;$$argon2id$$…&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;loadbalancer.server.port=80&lt;/code&gt;&lt;/strong&gt; – Vaultwarden listens on &lt;strong&gt;port 80&lt;/strong&gt; in the container. This line tells Traefik explicitly where to forward internally, instead of guessing the target port from the image – the same pattern as with every app behind Traefik.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;ports:&lt;/code&gt;&lt;/strong&gt; – as with every app behind Traefik, Vaultwarden is only reachable via the proxy, never directly from outside.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Start it and the first call
&lt;/h3&gt;

&lt;p&gt;Start the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first start, Vaultwarden creates the data directory and initializes the database. Take a look at the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; vaultwarden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end you should see this line – it confirms the service is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INFO] Rocket has launched from http://0.0.0.0:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;Ctrl+C&lt;/code&gt; you leave the log view again (the container keeps running). Now open &lt;code&gt;https://vault.YOUR_DOMAIN&lt;/code&gt; in the browser. On the first access Traefik fetches the Let's Encrypt certificate – that can take a few seconds. After that the Bitwarden web interface appears.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create account&lt;/strong&gt; and create your first account – with your email address and a name:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fve64eaqtw40zuqjmrvwo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fve64eaqtw40zuqjmrvwo.png" alt="The Vaultwarden web interface shows the form to create a new account with email address and name" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the next step you set your &lt;strong&gt;master password&lt;/strong&gt;. That's the one key that unlocks your entire vault:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0zfcpchrnx2lsllv9up.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0zfcpchrnx2lsllv9up.png" alt="The Vaultwarden page for setting a strong master password with a password field and strength indicator" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛑 The master password is not recoverable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your master password is &lt;strong&gt;never&lt;/strong&gt; transmitted to the server; it decrypts your vault locally. If you forget it, &lt;strong&gt;all&lt;/strong&gt; passwords stored in it are lost – no one, not even you, can recover them. Choose a long, unique passphrase and keep it in a safe place (e.g. printed out in a safe).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Close registration again
&lt;/h3&gt;

&lt;p&gt;Once your account is set up, you want to &lt;strong&gt;prevent strangers from registering too&lt;/strong&gt; – your Vaultwarden is, after all, open on the internet. Set &lt;code&gt;SIGNUPS_ALLOWED&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; in the &lt;code&gt;compose.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DOMAIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://vault.YOUR_DOMAIN"&lt;/span&gt;
      &lt;span class="na"&gt;SIGNUPS_ALLOWED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
      &lt;span class="na"&gt;ADMIN_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$$argon2id$$..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And apply the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose detects the changed environment variable and restarts the container. From now on the login page rejects new registrations. Further users (e.g. for the family) you invite specifically via the admin panel when needed – more on that shortly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Don't forget&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An open &lt;code&gt;SIGNUPS_ALLOWED: "true"&lt;/code&gt; is the most common misconfiguration with self-hosted Vaultwarden. Anyone who knows your domain could otherwise create an account. This step is mandatory, not an extra.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 5: The admin panel
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;https://vault.YOUR_DOMAIN/admin&lt;/code&gt; and log in with the &lt;strong&gt;password&lt;/strong&gt; you entered in step 1 when generating the hash (not with the hash itself). You land in the management interface:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmb70zhq32mgdoofqyi5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmb70zhq32mgdoofqyi5.png" alt="The Vaultwarden admin panel with the areas General, SMTP Email and Backup Database" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here you control the service centrally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users&lt;/strong&gt; – view existing users, &lt;strong&gt;invite&lt;/strong&gt; new ones (even with &lt;code&gt;SIGNUPS_ALLOWED=false&lt;/code&gt;), deactivate accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings → SMTP Email Settings&lt;/strong&gt; – enter your mail server's credentials here so Vaultwarden can send invitations and 2FA codes by email. Without SMTP, invitation by link works too, but email-based 2FA doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnostics&lt;/strong&gt; – shows you whether your &lt;code&gt;DOMAIN&lt;/code&gt; is set correctly and whether Vaultwarden is reachable from outside – handy for troubleshooting.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Settings in the panel vs. environment variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What you save in the admin panel is stored in &lt;code&gt;vw-data/config.json&lt;/code&gt; and &lt;strong&gt;overrides&lt;/strong&gt; the environment variables from the &lt;code&gt;compose.yaml&lt;/code&gt;. Decide per setting on one way to avoid confusion. For the basic configuration (domain, token, signups) we deliberately stick with the Compose file – it's versionable and reproducible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 6: Connect the clients
&lt;/h3&gt;

&lt;p&gt;Now comes the real benefit. Install the &lt;strong&gt;official Bitwarden app&lt;/strong&gt; or browser extension (from the respective app or add-on store). Before you log in, you switch the server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the app/extension, &lt;strong&gt;before logging in&lt;/strong&gt;, open the settings for the &lt;strong&gt;self-hosted environment&lt;/strong&gt; (gear icon or "Region: Self-hosted").&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;https://vault.YOUR_DOMAIN&lt;/code&gt; as the &lt;strong&gt;server URL&lt;/strong&gt; and save.&lt;/li&gt;
&lt;li&gt;Now log in with your email and your master password – the app talks to your server from now on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The nice part: it's the same mature clients as with commercial Bitwarden. Autofill, password generator, secure notes, attachments and 2FA storage work the same way – only the data lives on your server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Import existing passwords:&lt;/strong&gt; if you're switching from another manager (or the browser's password store), you don't have to retype everything. Export your entries there as CSV or JSON and import them via the &lt;strong&gt;web interface&lt;/strong&gt; under &lt;code&gt;Tools → Import data&lt;/code&gt;. Vaultwarden understands the export formats of the common managers (KeePass, LastPass, 1Password, Chrome/Firefox, etc.) directly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Securely delete export files afterwards&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An export CSV contains all passwords &lt;strong&gt;in plaintext&lt;/strong&gt;. Delete the file immediately after a successful import – and empty the trash. Never leave it lying in Downloads or a cloud folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡 Enable two-factor authentication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In your vault's account settings, enable &lt;strong&gt;2FA&lt;/strong&gt; (e.g. via an authenticator app). That way, even with a cracked master password, your vault isn't immediately open. For email-based 2FA, SMTP must be set up in the admin panel first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When things go wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The container won't start, the log says something like &lt;code&gt;Running without a persistent volume is not recommended&lt;/code&gt;.&lt;/strong&gt; The &lt;code&gt;volumes:&lt;/code&gt; mapping is missing. Vaultwarden deliberately refuses to start without a data directory so your passwords don't end up in an ephemeral container. Add &lt;code&gt;./vw-data:/data&lt;/code&gt; as in step 2 and restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The web interface shows "You need to enable HTTPS!" or the login fails with crypto errors.&lt;/strong&gt; Vaultwarden uses the browser's Web Crypto API, which is only available in a &lt;strong&gt;secure context&lt;/strong&gt; (real HTTPS). You opened the page over &lt;code&gt;http://&lt;/code&gt; or with an invalid certificate. Make sure Traefik fetched a valid Let's Encrypt certificate (check the Traefik log) and that you reach the page over &lt;code&gt;https://&lt;/code&gt;. The &lt;code&gt;DOMAIN&lt;/code&gt; variable must also start with &lt;code&gt;https://&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The admin panel rejects your password, even though it's correct.&lt;/strong&gt; Probably the dollar signs in the &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; aren't doubled. In the &lt;code&gt;compose.yaml&lt;/code&gt; every &lt;code&gt;$&lt;/code&gt; must become &lt;code&gt;$$&lt;/code&gt;. Check with &lt;code&gt;docker compose config&lt;/code&gt; how the token actually arrives (Compose shows the resolved value there). Also remember: at login you enter the &lt;strong&gt;password&lt;/strong&gt;, not the hash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The phone app can't find the server or reports "Server URL invalid".&lt;/strong&gt; The server URL must be the full &lt;code&gt;https://&lt;/code&gt; address without a trailing path (&lt;code&gt;https://vault.YOUR_DOMAIN&lt;/code&gt;). Also check whether the domain is reachable from outside via a browser and the certificate is valid – apps are stricter about certificate errors than browsers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Despite &lt;code&gt;SIGNUPS_ALLOWED=false&lt;/code&gt;, someone was able to register.&lt;/strong&gt; The setting was probably set in the admin panel and overrides the environment variable, or the container wasn't restarted after the change. Check the value under &lt;strong&gt;Settings → General settings&lt;/strong&gt; in the panel and restart with &lt;code&gt;docker compose up -d&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance &amp;amp; backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backups are non-negotiable for a password manager.&lt;/strong&gt; Your entire vault is in the &lt;code&gt;vw-data/&lt;/code&gt; directory (SQLite database, keys, attachments). Back it up &lt;strong&gt;encrypted and off-site&lt;/strong&gt; with &lt;a href="https://serverkueche.de/en/tutorials/restic-backups/" rel="noopener noreferrer"&gt;Restic&lt;/a&gt; – add the folder &lt;code&gt;~/vaultwarden/vw-data&lt;/code&gt; to your backup sources. For a consistent database state, briefly run &lt;code&gt;docker compose stop&lt;/code&gt; before the backup, or use the "Backup Database" function in the admin panel. A mere file copy while the container is &lt;strong&gt;running&lt;/strong&gt; can catch an inconsistent state (SQLite writes into WAL files) – with a password manager that's not a risk worth taking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply updates in a controlled way.&lt;/strong&gt; Because we pinned the version, you update deliberately: before the switch read the &lt;a href="https://github.com/dani-garcia/vaultwarden/releases" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;, then raise the tag in the &lt;code&gt;compose.yaml&lt;/code&gt; (e.g. to the next &lt;code&gt;1.x&lt;/code&gt;) and pull anew:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker compose pull &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Afterwards check the &lt;code&gt;Rocket has launched&lt;/code&gt; line in the log again and test a login.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep registration closed.&lt;/strong&gt; Occasionally check that &lt;code&gt;SIGNUPS_ALLOWED&lt;/code&gt; is still &lt;code&gt;false&lt;/code&gt; – always invite new users specifically via the admin panel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate the ADMIN_TOKEN&lt;/strong&gt; if it might be compromised: generate a new hash with the &lt;code&gt;hash&lt;/code&gt; command from step 1, replace it in the &lt;code&gt;compose.yaml&lt;/code&gt;, restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never lose the master password.&lt;/strong&gt; There is no recovery. In a team/family setup, an emergency access can make sense – you set that up per vault in the account settings.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://serverkueche.de/en/tutorials/vaultwarden-password-manager/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>security</category>
      <category>docker</category>
    </item>
    <item>
      <title>Paperless-ngx selbst hosten: papierloses Büro mit OCR</title>
      <dc:creator>serverkueche.de</dc:creator>
      <pubDate>Mon, 20 Jul 2026 21:15:45 +0000</pubDate>
      <link>https://dev.to/serverkueche/paperless-ngx-selbst-hosten-papierloses-buro-mit-ocr-2f8l</link>
      <guid>https://dev.to/serverkueche/paperless-ngx-selbst-hosten-papierloses-buro-mit-ocr-2f8l</guid>
      <description>&lt;p&gt;Rechnungen, Verträge, Behördenpost – der Papierstapel wächst und wächst, und finden tust du&lt;br&gt;
am Ende doch nie etwas. &lt;strong&gt;Paperless-ngx&lt;/strong&gt; macht daraus ein durchsuchbares digitales Archiv: Du wirfst ein&lt;br&gt;
Dokument hinein, es wird per &lt;strong&gt;OCR&lt;/strong&gt; erkannt, verschlagwortet und ist über die&lt;br&gt;
Volltextsuche in Sekunden wieder da. In diesem Rezept setzen wir es hinter Traefik auf.&lt;/p&gt;
&lt;h2&gt;
  
  
  Was bauen wir?
&lt;/h2&gt;

&lt;p&gt;Am Ende läuft &lt;strong&gt;Paperless-ngx 2.20&lt;/strong&gt; hinter deinem Traefik-Proxy, erreichbar unter&lt;br&gt;
&lt;code&gt;https://paperless.DEINE_DOMAIN&lt;/code&gt; mit HTTPS. Paperless ist ein Dokumenten-Management-System:&lt;br&gt;
Du fütterst es mit Scans oder PDFs, es liest den Text per &lt;strong&gt;OCR&lt;/strong&gt; aus (auch aus reinen&lt;br&gt;
Bild-Scans), erkennt Datum und Inhalt und legt alles durchsuchbar ab. Über Tags,&lt;br&gt;
Korrespondenten und Dokumenttypen bringst du Ordnung hinein; die Volltextsuche findet&lt;br&gt;
später jedes Dokument.&lt;/p&gt;

&lt;p&gt;Der Clou ist der &lt;strong&gt;Consume-Ordner&lt;/strong&gt;: Alles, was du dort ablegst (z. B. vom Netzwerk-Scanner),&lt;br&gt;
wird automatisch importiert und verarbeitet. Paperless besteht aus &lt;strong&gt;fünf Containern&lt;/strong&gt; –&lt;br&gt;
dem Webserver, einer PostgreSQL-Datenbank, einem Redis-Broker für die&lt;br&gt;
Hintergrundverarbeitung sowie &lt;strong&gt;Gotenberg&lt;/strong&gt; und &lt;strong&gt;Tika&lt;/strong&gt;, die Office-Dokumente in PDF&lt;br&gt;
umwandeln. Die offizielle Vorlage liefert das komplette Gespann.&lt;/p&gt;

&lt;p&gt;Der Gewinn gegenüber einem Ordner voller PDFs auf der Festplatte: Paperless macht jedes&lt;br&gt;
Dokument &lt;strong&gt;durchsuchbar&lt;/strong&gt; (auch eingescanntes Papier), hält Original und Archivversion&lt;br&gt;
sauber getrennt und lässt sich per Regeln automatisieren. Deine Unterlagen bleiben dabei&lt;br&gt;
auf deinem Server – kein Cloud-Dienst liest mit, und du bist nicht an ein proprietäres&lt;br&gt;
Format gebunden.&lt;/p&gt;
&lt;h2&gt;
  
  
  Voraussetzungen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ein laufender &lt;strong&gt;Traefik-Reverse-Proxy&lt;/strong&gt; mit dem &lt;code&gt;proxy&lt;/code&gt;-Netzwerk und dem Resolver &lt;code&gt;le&lt;/code&gt;
– siehe &lt;a href="https://serverkueche.de/tutorials/reverse-proxy-traefik/" rel="noopener noreferrer"&gt;Reverse Proxy mit Traefik&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Eine Subdomain &lt;code&gt;paperless.DEINE_DOMAIN&lt;/code&gt; mit DNS-Record auf deine Server-IP – siehe
&lt;a href="https://serverkueche.de/tutorials/domain-mit-server-verbinden/" rel="noopener noreferrer"&gt;Domain mit Server verbinden&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Ein &lt;strong&gt;Backup&lt;/strong&gt;. In Paperless landen deine wichtigsten Unterlagen – richte zuerst
&lt;a href="https://serverkueche.de/tutorials/backups-mit-restic/" rel="noopener noreferrer"&gt;Backups mit Restic&lt;/a&gt; ein.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Wie groß muss der Server sein?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Paperless selbst ist genügsam, aber es sind fünf Container, und die &lt;strong&gt;OCR-Verarbeitung ist&lt;br&gt;
CPU-lastig&lt;/strong&gt;: Beim Erkennen eines Scans läuft ein Kern für einige Sekunden bis Minuten auf&lt;br&gt;
Anschlag. Für den privaten Gebrauch reichen &lt;strong&gt;2 vCPU und 4 GB RAM&lt;/strong&gt; gut; der getestete VPS&lt;br&gt;
1000 (4 vCore / 8 GB) hat reichlich Luft. Beim Massenimport vieler Dokumente merkst du die&lt;br&gt;
CPU-Last – dann verarbeitet Paperless die Warteschlange eben nach und nach ab.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unsicher, welche Server-Größe reicht? Der &lt;a href="https://serverkueche.de/serverempfehlung/" rel="noopener noreferrer"&gt;Server-Rechner&lt;/a&gt; rechnet dir&lt;br&gt;
RAM- und CPU-Bedarf für deine Dienste aus.&lt;/p&gt;
&lt;h2&gt;
  
  
  Schritt für Schritt
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Schritt 1: DNS-Record anlegen
&lt;/h3&gt;

&lt;p&gt;Lege &lt;code&gt;paperless.DEINE_DOMAIN&lt;/code&gt; an (A/AAAA auf deine Server-IP) und prüfe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short paperless.DEINE_DOMAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Es muss deine Server-IP zurückkommen – sonst holt Traefik später kein Zertifikat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 2: Einen Secret Key erzeugen
&lt;/h3&gt;

&lt;p&gt;Paperless verschlüsselt Sitzungen mit einem geheimen Schlüssel. Der Default ist &lt;strong&gt;öffentlich&lt;br&gt;
bekannt&lt;/strong&gt; – bei einer Instanz im Internet ein echtes Risiko. Erzeuge einen eigenen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 50 /dev/urandom | &lt;span class="nb"&gt;base64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kopiere die Ausgabe – sie kommt gleich als &lt;code&gt;PAPERLESS_SECRET_KEY&lt;/code&gt; in die Konfiguration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 3: Die compose.yaml anlegen
&lt;/h3&gt;

&lt;p&gt;Leg das Projekt an – &lt;strong&gt;inklusive&lt;/strong&gt; der beiden Bind-Mount-Ordner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/paperless/&lt;span class="o"&gt;{&lt;/span&gt;consume,export&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/paperless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die Unterordner &lt;code&gt;consume&lt;/code&gt; und &lt;code&gt;export&lt;/code&gt; legst du bewusst &lt;strong&gt;jetzt, als normaler&lt;br&gt;
Benutzer&lt;/strong&gt; an: Würde erst der Docker-Daemon sie beim Start erzeugen, gehörten sie&lt;br&gt;
&lt;code&gt;root&lt;/code&gt; – dann könnten weder du (&lt;code&gt;cp&lt;/code&gt; in den Consume-Ordner) noch Paperless selbst&lt;br&gt;
(läuft via &lt;code&gt;USERMAP_UID&lt;/code&gt; als UID 1000) hineinschreiben.&lt;/p&gt;

&lt;p&gt;Erstelle &lt;code&gt;compose.yaml&lt;/code&gt;. Ersetze &lt;code&gt;paperless.DEINE_DOMAIN&lt;/code&gt;, die Passwörter und den&lt;br&gt;
&lt;code&gt;PAPERLESS_SECRET_KEY&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paperless&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;broker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/library/redis:8&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redisdata:/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/library/postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paperless&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paperless&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EIN_STARKES_DB_PASSWORT&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;gotenberg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/gotenberg/gotenberg:8.34&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gotenberg"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--chromium-disable-javascript=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--chromium-allow-list=file:///tmp/.*"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;tika&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/apache/tika:3.3.1.0&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;webserver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/paperless-ngx/paperless-ngx:2.20.15&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;broker&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;gotenberg&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tika&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_REDIS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://broker:6379&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_DBHOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_DBUSER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paperless&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_DBPASS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EIN_STARKES_DB_PASSWORT&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_TIKA_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_TIKA_GOTENBERG_ENDPOINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://gotenberg:3000&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_TIKA_ENDPOINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://tika:9998&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://paperless.DEINE_DOMAIN&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_SECRET_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEIN_LANGER_SECRET_KEY&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_OCR_LANGUAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deu&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_TIME_ZONE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Europe/Berlin&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_ADMIN_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
      &lt;span class="na"&gt;PAPERLESS_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EIN_STARKES_ADMIN_PASSWORT&lt;/span&gt;
      &lt;span class="na"&gt;USERMAP_UID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&lt;/span&gt;
      &lt;span class="na"&gt;USERMAP_GID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;data:/usr/src/paperless/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;media:/usr/src/paperless/media&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./export:/usr/src/paperless/export&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./consume:/usr/src/paperless/consume&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.ppl.rule=Host(`paperless.DEINE_DOMAIN`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.ppl.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.ppl.tls.certresolver=le"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.ppl.loadbalancer.server.port=8000"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redisdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die wichtigsten Punkte:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nur &lt;code&gt;webserver&lt;/code&gt; hängt im &lt;code&gt;proxy&lt;/code&gt;-Netz&lt;/strong&gt; und trägt Traefik-Labels (Port &lt;strong&gt;8000&lt;/strong&gt;). Die
vier Hilfsdienste (db, broker, gotenberg, tika) bleiben intern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PAPERLESS_URL&lt;/code&gt;&lt;/strong&gt; ist Pflicht hinter einem Proxy. Fehlt sie, weist Paperless den Login
mit einem CSRF-/„403 Forbidden"-Fehler ab. Sie setzt zugleich die erlaubten Hosts und die
vertrauenswürdigen Ursprünge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PAPERLESS_OCR_LANGUAGE: deu&lt;/code&gt;&lt;/strong&gt; stellt die Texterkennung auf Deutsch. Deutsch, Englisch
und ein paar weitere Sprachen sind im Image bereits enthalten – kein Zusatzpaket nötig.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PAPERLESS_ADMIN_USER&lt;/code&gt; / &lt;code&gt;_PASSWORD&lt;/code&gt;&lt;/strong&gt; legen beim ersten Start automatisch den
Superuser an, sodass du dich direkt anmelden kannst.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;USERMAP_UID&lt;/code&gt;/&lt;code&gt;GID&lt;/code&gt;&lt;/strong&gt; sollten zur Kennung deines Server-Benutzers passen (per &lt;code&gt;id -u&lt;/code&gt;
bzw. &lt;code&gt;id -g&lt;/code&gt; ermitteln, meist &lt;code&gt;1000&lt;/code&gt;). Sonst gibt es „Permission denied" im Consume-Ordner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;export&lt;/code&gt; und &lt;code&gt;consume&lt;/code&gt;&lt;/strong&gt; sind bewusst Ordner im Projektverzeichnis (Bind-Mounts):
&lt;code&gt;consume&lt;/code&gt; ist der Eingangskorb, &lt;code&gt;export&lt;/code&gt; das Ziel für Backups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paperless nutzt insgesamt &lt;strong&gt;vier Speicherbereiche&lt;/strong&gt;, die du auseinanderhalten solltest:&lt;br&gt;
&lt;code&gt;media&lt;/code&gt; enthält deine &lt;strong&gt;verarbeiteten Dokumente&lt;/strong&gt; (das Herzstück!), &lt;code&gt;data&lt;/code&gt; den Suchindex und&lt;br&gt;
Hilfsdaten, &lt;code&gt;consume&lt;/code&gt; ist der Eingangskorb und &lt;code&gt;export&lt;/code&gt; das Backup-Ziel. Fürs Backup zählen&lt;br&gt;
&lt;code&gt;media&lt;/code&gt; und &lt;code&gt;data&lt;/code&gt; sowie die Datenbank – genau das nimmt dir der &lt;code&gt;document_exporter&lt;/code&gt; weiter&lt;br&gt;
unten ab.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 4: Starten und anmelden
&lt;/h3&gt;

&lt;p&gt;Zieh die Images (mehrere GB) und starte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; webserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beim ersten Start richtet Paperless die Datenbank ein (Migrationen) – das dauert einen&lt;br&gt;
Moment. Prüfe, dass alle fünf Container laufen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Du solltest &lt;code&gt;webserver&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;, &lt;code&gt;broker&lt;/code&gt;, &lt;code&gt;gotenberg&lt;/code&gt; und &lt;code&gt;tika&lt;/code&gt; mit Status &lt;code&gt;running&lt;/code&gt; sehen&lt;br&gt;
(der &lt;code&gt;webserver&lt;/code&gt; wird nach kurzer Zeit &lt;code&gt;healthy&lt;/code&gt;). Ist der Worker bereit (&lt;code&gt;celery@… ready&lt;/code&gt;&lt;br&gt;
im Log), ruf &lt;code&gt;https://paperless.DEINE_DOMAIN&lt;/code&gt; auf. Es erscheint die Anmeldeseite:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff9vqwuttqrjr8mu88hxr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff9vqwuttqrjr8mu88hxr.png" alt="Die Anmeldeseite von Paperless-ngx unter der eigenen Domain" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Melde dich mit &lt;code&gt;admin&lt;/code&gt; und deinem Passwort an. Du landest auf der &lt;strong&gt;Startseite&lt;/strong&gt; mit einer&lt;br&gt;
kurzen Willkommensmeldung und ersten Statistiken. Ganz unten links siehst du die laufende&lt;br&gt;
&lt;strong&gt;Version&lt;/strong&gt; – praktisch, um vor einem Update den Ausgangsstand zu kennen. Die Sprache&lt;br&gt;
kannst du bei Bedarf unter &lt;strong&gt;Einstellungen&lt;/strong&gt; umstellen; standardmäßig folgt Paperless der&lt;br&gt;
Sprache deines Browsers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6e2ja4ovuxjco69rhmpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6e2ja4ovuxjco69rhmpj.png" alt="Die Startseite von Paperless-ngx mit Willkommensmeldung und Statistik-Widget" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 5: Das erste Dokument einlesen
&lt;/h3&gt;

&lt;p&gt;Jetzt der Kern. Es gibt drei Wege, ein Dokument hineinzubekommen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Über die Weboberfläche:&lt;/strong&gt; oben rechts auf &lt;strong&gt;Dokumente hochladen&lt;/strong&gt; und eine PDF- oder
Bilddatei auswählen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Über den Consume-Ordner:&lt;/strong&gt; Leg eine Datei in &lt;code&gt;~/paperless/consume&lt;/code&gt; – Paperless erkennt
sie automatisch, verarbeitet sie und löscht sie danach aus dem Ordner. Ideal für einen
Netzwerk-Scanner, der direkt dorthin scannt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aus einem Postfach:&lt;/strong&gt; Paperless kann unter &lt;strong&gt;E-Mail&lt;/strong&gt; ein IMAP-Konto abrufen und
Anhänge automatisch einlesen – praktisch für Rechnungen, die dir ohnehin per Mail kommen.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; ~/eine-rechnung.pdf ~/paperless/consume/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paperless nimmt PDFs, Bilder (JPG/PNG/TIFF) und – dank Gotenberg und Tika – auch&lt;br&gt;
Office-Dateien wie Word oder Excel an. Bei einem reinen Bild-Scan liest die &lt;strong&gt;OCR&lt;/strong&gt; den Text&lt;br&gt;
aus und legt ihn als durchsuchbare Ebene über das Dokument; bei einem PDF mit vorhandenem&lt;br&gt;
Textlayer überspringt Paperless die Erkennung und ist entsprechend schneller. Das Original&lt;br&gt;
bleibt dabei unangetastet erhalten – Paperless erzeugt zusätzlich eine durchsuchbare&lt;br&gt;
Archiv-Version.&lt;/p&gt;

&lt;p&gt;Nach ein paar Sekunden (OCR braucht etwas Zeit) taucht das Dokument unter &lt;strong&gt;Dokumente&lt;/strong&gt; auf –&lt;br&gt;
mit einer Vorschau und dem erkannten Titel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bziw41krn9huqxi2y7v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bziw41krn9huqxi2y7v.png" alt="Die Dokumentenliste von Paperless-ngx mit einem verarbeiteten Dokument als Kachel" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ein Klick öffnet die &lt;strong&gt;Detailansicht&lt;/strong&gt;: links die Metadaten (Titel, Datum, Korrespondent,&lt;br&gt;
Tags) und die Reiter für Inhalt, Metadaten und Verlauf, rechts das Dokument mit einer&lt;br&gt;
zoombaren Vorschau. Paperless hat aus dem Scan bereits das &lt;strong&gt;Datum erkannt&lt;/strong&gt; und&lt;br&gt;
schlägt es zur Bestätigung vor – genau das leistet die OCR im Hintergrund für dich:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1gjff1lghqxkhv7ht2a6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1gjff1lghqxkhv7ht2a6.png" alt="Die Detailansicht eines Dokuments in Paperless-ngx mit Metadaten links und dem erkannten Dokument rechts" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 6: Ordnung mit Tags, Korrespondenten &amp;amp; Dokumenttypen
&lt;/h3&gt;

&lt;p&gt;Damit die Suche später greift, vergibst du &lt;strong&gt;Tags&lt;/strong&gt; (z. B. &lt;code&gt;Steuer&lt;/code&gt;, &lt;code&gt;Versicherung&lt;/code&gt;),&lt;br&gt;
ordnest einen &lt;strong&gt;Korrespondenten&lt;/strong&gt; (den Absender) und einen &lt;strong&gt;Dokumenttyp&lt;/strong&gt; (Rechnung,&lt;br&gt;
Vertrag …) zu. Das kannst du von Hand machen – oder Paperless über &lt;strong&gt;Arbeitsabläufe&lt;/strong&gt;&lt;br&gt;
automatisieren: Regeln, die eingehende Dokumente anhand ihres Inhalts automatisch&lt;br&gt;
verschlagworten. So sortiert sich dein Archiv mit der Zeit von selbst.&lt;/p&gt;

&lt;p&gt;Die &lt;strong&gt;Volltextsuche&lt;/strong&gt; oben durchsucht danach nicht nur Titel und Tags, sondern den&lt;br&gt;
kompletten erkannten Text – eine Suche nach &lt;code&gt;Rechnungsbetrag&lt;/code&gt; oder einem Kundennamen findet&lt;br&gt;
das passende Dokument in Sekunden. Kombiniert mit den Filtern (Korrespondent, Zeitraum,&lt;br&gt;
Dokumenttyp) wird der Papierstapel endgültig zum durchsuchbaren Archiv.&lt;/p&gt;

&lt;p&gt;Ein einfaches Beispiel für einen &lt;strong&gt;Arbeitsablauf&lt;/strong&gt;: Enthält ein neues Dokument das Wort&lt;br&gt;
„Stromabrechnung", vergib automatisch den Tag &lt;code&gt;Energie&lt;/code&gt;, setz den Korrespondenten auf deinen&lt;br&gt;
Stromanbieter und den Dokumenttyp auf &lt;code&gt;Rechnung&lt;/code&gt;. Solche Regeln legst du unter&lt;br&gt;
&lt;strong&gt;Verwaltung → Arbeitsabläufe&lt;/strong&gt; an; sie greifen bei jedem eingehenden Dokument. Anfangs&lt;br&gt;
lohnt es sich, ein paar Dokumente von Hand zu sortieren – daraus siehst du schnell, welche&lt;br&gt;
Regeln sich wiederholen und automatisieren lassen. Nach ein paar Wochen landet der Großteil&lt;br&gt;
deiner Post ohne dein Zutun am richtigen Platz.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Scanner direkt in den Consume-Ordner&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Der volle Komfort entsteht mit einem Dokumentenscanner. Richte auf dem Server eine&lt;br&gt;
Netzwerkfreigabe (z. B. Samba) ein, die auf &lt;code&gt;~/paperless/consume&lt;/code&gt; zeigt, und stelle deinen&lt;br&gt;
Scanner so ein, dass er dorthin scannt. Ab dann gilt: Blatt einlegen, Knopf drücken – wenige&lt;br&gt;
Sekunden später ist das Dokument erkannt, verschlagwortet und durchsuchbar im Archiv. Für&lt;br&gt;
unterwegs gibt es zudem Community-Apps (z. B. „Paperless Mobile"), die sich mit deiner&lt;br&gt;
Instanz verbinden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Deine Unterlagen hängen im Internet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Paperless liegen sensible Dokumente – Rechnungen, Verträge, Behördenpost. Sobald die&lt;br&gt;
Instanz über &lt;code&gt;paperless.DEINE_DOMAIN&lt;/code&gt; erreichbar ist, steht die Anmeldeseite offen im Netz.&lt;br&gt;
Vergib deshalb ein &lt;strong&gt;langes, einmaliges Passwort&lt;/strong&gt;, und aktiviere unter &lt;strong&gt;Einstellungen&lt;/strong&gt; die&lt;br&gt;
&lt;strong&gt;Zwei-Faktor-Authentifizierung&lt;/strong&gt;. Wer maximale Sicherheit will, macht Paperless nur über ein&lt;br&gt;
VPN erreichbar – für ein reines Privatarchiv, das nur du nutzt, ist das eine Überlegung wert.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wenn es nicht funktioniert
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Beim Login erscheint „&lt;strong&gt;Forbidden (403)&lt;/strong&gt;" oder „CSRF verification failed".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; &lt;code&gt;PAPERLESS_URL&lt;/code&gt; ist nicht oder falsch gesetzt. Sie muss exakt deiner&lt;br&gt;
HTTPS-Adresse entsprechen (&lt;code&gt;https://paperless.DEINE_DOMAIN&lt;/code&gt;, ohne Schrägstrich am Ende).&lt;br&gt;
Nach der Korrektur &lt;code&gt;docker compose up -d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Dateien im Consume-Ordner werden nicht verarbeitet, „permission denied".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Häufigste Ursache: Die Ordner wurden nicht vorab angelegt&lt;br&gt;
(Schritt 3), sondern beim ersten Start vom Docker-Daemon erzeugt – dann gehören sie&lt;br&gt;
&lt;code&gt;root&lt;/code&gt;. Mit &lt;code&gt;sudo chown -R $(id -u):$(id -g) ~/paperless/consume ~/paperless/export&lt;/code&gt;&lt;br&gt;
gehören sie wieder dir. Ansonsten: &lt;code&gt;USERMAP_UID&lt;/code&gt;/&lt;code&gt;USERMAP_GID&lt;/code&gt; passen nicht zum&lt;br&gt;
Besitzer des Ordners – ermittle deine Kennung mit &lt;code&gt;id -u&lt;/code&gt; und &lt;code&gt;id -g&lt;/code&gt;, trag die Werte&lt;br&gt;
ein und starte neu.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Hochgeladene Dokumente bleiben „in Bearbeitung" hängen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Die Hintergrundverarbeitung läuft über Redis. Prüfe, dass der&lt;br&gt;
&lt;code&gt;broker&lt;/code&gt;-Container läuft, und sieh unter &lt;strong&gt;Dateiaufgaben&lt;/strong&gt; nach der Fehlermeldung der&lt;br&gt;
fehlgeschlagenen Aufgabe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Die Texterkennung liefert Unsinn oder erkennt nichts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Falsche OCR-Sprache. Setz &lt;code&gt;PAPERLESS_OCR_LANGUAGE=deu&lt;/code&gt; (oder&lt;br&gt;
&lt;code&gt;deu+eng&lt;/code&gt; für gemischte Dokumente). Nur installierte Sprachen funktionieren.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Office-Dokumente (Word, Excel) werden nicht angenommen oder enden im Timeout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Dafür sind Gotenberg und Tika zuständig. Prüfe, dass beide Container&lt;br&gt;
laufen und &lt;code&gt;PAPERLESS_TIKA_ENABLED=1&lt;/code&gt; samt der beiden Endpoint-Variablen gesetzt ist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Beim Massenimport wird der Server sehr langsam, die CPU ist dauerhaft am Anschlag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; OCR ist rechenintensiv, und Paperless nutzt standardmäßig alle Kerne.&lt;br&gt;
Auf kleinen Servern kannst du die Last drosseln, indem du die Zahl der Worker bzw. Threads&lt;br&gt;
begrenzt (&lt;code&gt;PAPERLESS_TASK_WORKERS&lt;/code&gt;, &lt;code&gt;PAPERLESS_THREADS_PER_WORKER&lt;/code&gt;). Dann dauert der Import&lt;br&gt;
länger, aber die Oberfläche bleibt bedienbar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wartung &amp;amp; Backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Das saubere Backup macht der &lt;code&gt;document_exporter&lt;/code&gt;.&lt;/strong&gt; Er schreibt alle Dokumente,
Vorschaubilder, Metadaten und den Datenbankinhalt in den &lt;code&gt;export&lt;/code&gt;-Ordner – portabel und
wieder importierbar:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; webserver document_exporter ../export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sichere den &lt;code&gt;export&lt;/code&gt;-Ordner anschließend &lt;strong&gt;verschlüsselt und off-site&lt;/strong&gt; mit&lt;br&gt;
  &lt;a href="https://serverkueche.de/tutorials/backups-mit-restic/" rel="noopener noreferrer"&gt;Restic&lt;/a&gt;. Alternativ sicherst du die Volumes &lt;code&gt;media&lt;/code&gt;,&lt;br&gt;
  &lt;code&gt;data&lt;/code&gt; und die Datenbank direkt – der Exporter ist aber der empfohlene, umzugssichere Weg.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beim Wiederherstellen die gleiche Version verwenden.&lt;/strong&gt; Ein Export enthält ein Abbild
passend zum Datenbank-Schema; spiel ihn nur in eine Paperless-Instanz &lt;strong&gt;derselben Version&lt;/strong&gt;
ein (&lt;code&gt;document_importer ../export&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates.&lt;/strong&gt; Vor dem Update ein Backup ziehen, dann den Image-Tag erhöhen (z. B.
&lt;code&gt;2.20.15&lt;/code&gt; → nächste Version), &lt;code&gt;docker compose pull&lt;/code&gt; und &lt;code&gt;docker compose up -d&lt;/code&gt;. Die
Datenbank-Migrationen laufen beim Start automatisch. Bleib bei der stabilen &lt;code&gt;2.x&lt;/code&gt;-Reihe –
die &lt;code&gt;3.0&lt;/code&gt;-Beta ist noch nicht für den Produktivbetrieb gedacht.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup automatisieren.&lt;/strong&gt; Den &lt;code&gt;document_exporter&lt;/code&gt; legst du am besten in einen täglichen
Cron-Job (z. B. nachts), der anschließend den &lt;code&gt;export&lt;/code&gt;-Ordner per Restic sichert. So hast
du jeden Morgen einen frischen, wiederherstellbaren Stand – ohne daran denken zu müssen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ehrlich zum Aufwand:&lt;/strong&gt; Paperless läuft danach sehr wartungsarm. Der eigentliche Aufwand
ist das Einsortieren neuer Dokumente – das nimmt dir mit etwas eingerichteter Automatik
aber zunehmend die Software ab. Plane einmal die grobe Tag-Struktur, dann trägt sich das
Archiv weitgehend selbst.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Dieser Beitrag erschien zuerst auf &lt;a href="https://serverkueche.de/tutorials/paperless-dokumentenverwaltung/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>german</category>
      <category>paperless</category>
      <category>selfhosting</category>
      <category>docker</category>
    </item>
    <item>
      <title>Uptime Kuma installieren: Server-Monitoring hinter Traefik</title>
      <dc:creator>serverkueche.de</dc:creator>
      <pubDate>Sun, 19 Jul 2026 20:37:39 +0000</pubDate>
      <link>https://dev.to/serverkueche/uptime-kuma-installieren-server-monitoring-hinter-traefik-2inl</link>
      <guid>https://dev.to/serverkueche/uptime-kuma-installieren-server-monitoring-hinter-traefik-2inl</guid>
      <description>&lt;p&gt;Traefik steht – jetzt hängen wir die erste echte App dahinter. &lt;strong&gt;Uptime Kuma&lt;/strong&gt; ist&lt;br&gt;
dafür ideal: winzig, sofort nützlich und ohne Datenbank. Es überwacht deine Dienste&lt;br&gt;
und schlägt Alarm, wenn einer ausfällt – ab jetzt merkst &lt;em&gt;du&lt;/em&gt; zuerst, dass etwas&lt;br&gt;
klemmt, nicht deine Nutzer. Nebenbei lernst du das Muster, das jede weitere App&lt;br&gt;
wiederholt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Was bauen wir?
&lt;/h2&gt;

&lt;p&gt;Am Ende läuft &lt;strong&gt;Uptime Kuma 2.4&lt;/strong&gt; unter &lt;code&gt;https://status.DEINE_DOMAIN&lt;/code&gt;, abgesichert&lt;br&gt;
über Traefik mit automatischem HTTPS. Du hast den ersten Monitor eingerichtet (der&lt;br&gt;
prüft im Minutentakt, ob ein Dienst antwortet), eine Benachrichtigung verbunden und&lt;br&gt;
optional eine öffentliche Status-Seite. Zum ersten Mal binden wir dabei einen Dienst&lt;br&gt;
an, der &lt;strong&gt;nicht auf Port 80&lt;/strong&gt; lauscht – ein wichtiges Detail für alle folgenden Apps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Voraussetzungen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ein laufender &lt;a href="https://serverkueche.de/tutorials/reverse-proxy-traefik/" rel="noopener noreferrer"&gt;Traefik-Reverse-Proxy&lt;/a&gt; samt
&lt;code&gt;proxy&lt;/code&gt;-Netzwerk und funktionierendem Let's-Encrypt-Resolver &lt;code&gt;le&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ein DNS-Record &lt;code&gt;status.DEINE_DOMAIN&lt;/code&gt;, der
&lt;a href="https://serverkueche.de/tutorials/domain-mit-server-verbinden/" rel="noopener noreferrer"&gt;auf den Server zeigt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Schritt für Schritt
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Schritt 1: Die compose.yaml
&lt;/h3&gt;

&lt;p&gt;Eigener Ordner, eigene Datei:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/uptime-kuma &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;uptime-kuma&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;louislam/uptime-kuma:2&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;kuma-data:/app/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.rule=Host(`status.DEINE_DOMAIN`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.tls.certresolver=le"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.kuma.loadbalancer.server.port=3001"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kuma-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Das kennst du aus dem Traefik-Tutorial – bis auf &lt;strong&gt;eine neue, entscheidende Zeile&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.kuma.loadbalancer.server.port=3001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uptime Kuma lauscht intern auf &lt;strong&gt;Port 3001&lt;/strong&gt;, nicht auf 80. Diese Zeile sagt Traefik,&lt;br&gt;
an welchen Port es die Anfragen weiterreichen soll. Ohne sie rät Traefik falsch und&lt;br&gt;
du bekommst einen &lt;code&gt;Bad Gateway&lt;/code&gt;. Merke dir das Label – jede App, die nicht auf Port&lt;br&gt;
80 läuft, braucht es.&lt;/p&gt;

&lt;p&gt;Der Rest ist das bekannte Muster: &lt;strong&gt;kein &lt;code&gt;ports:&lt;/code&gt;&lt;/strong&gt; (nur über Traefik erreichbar),&lt;br&gt;
&lt;code&gt;proxy&lt;/code&gt;-Netzwerk, Named Volume für die Daten.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 2: Starten und ersten Login
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Wenn Kuma bereit ist, endet das Log mit dieser Zeile – ab hier nimmt der Dienst&lt;br&gt;
Anfragen an (&lt;code&gt;Strg+C&lt;/code&gt; beendet nur das Mitlesen):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SERVER] INFO: Welcome to Uptime Kuma
[SERVER] INFO: Uptime Kuma Version: 2.4.0
[SETUP-DATABASE] INFO: Listening on:
[SETUP-DATABASE] INFO: -  http://localhost:3001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beim ersten Start legt Kuma seine Datenbank im Volume an – das dauert einige&lt;br&gt;
Sekunden. Ruf dann &lt;code&gt;https://status.DEINE_DOMAIN&lt;/code&gt; im Browser auf. Uptime Kuma 2.x&lt;br&gt;
fragt zuerst die &lt;strong&gt;Datenbank&lt;/strong&gt; ab – für ein Setup wie unseres ist &lt;strong&gt;SQLite&lt;/strong&gt; die&lt;br&gt;
richtige, einfachste Wahl (auswählen, auf &lt;strong&gt;Weiter&lt;/strong&gt; klicken). Direkt danach legst du&lt;br&gt;
das &lt;strong&gt;Admin-Konto&lt;/strong&gt; an (Benutzername + starkes Passwort).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx24yjwhdr4s96uv6lg4h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx24yjwhdr4s96uv6lg4h.png" alt="Uptime Kumas Ersteinrichtung: Sprache wählen und das Admin-Konto anlegen" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Sofort das Admin-Konto anlegen&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Solange kein Admin-Konto existiert, kann &lt;strong&gt;jeder&lt;/strong&gt;, der die Seite aufruft, eines&lt;br&gt;
anlegen. Erledige das direkt nach dem ersten Start – nicht „später".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Schritt 3: Den ersten Monitor anlegen
&lt;/h3&gt;

&lt;p&gt;Ein &lt;strong&gt;Monitor&lt;/strong&gt; ist eine wiederkehrende Prüfung. Klicke auf &lt;strong&gt;Neuen Monitor&lt;br&gt;
hinzufügen&lt;/strong&gt; und lege einen für Traefik selbst an:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monitortyp:&lt;/strong&gt; &lt;code&gt;HTTP(s)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freundlicher Name:&lt;/strong&gt; &lt;code&gt;Traefik Dashboard&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;code&gt;https://traefik.DEINE_DOMAIN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prüfintervall:&lt;/strong&gt; &lt;code&gt;60&lt;/code&gt; Sekunden&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentifizierung:&lt;/strong&gt; Das Traefik-Dashboard ist per Basic-Auth geschützt
(Schritt 7 des &lt;a href="https://serverkueche.de/tutorials/reverse-proxy-traefik/" rel="noopener noreferrer"&gt;Traefik-Tutorials&lt;/a&gt;) – wähle
deshalb unten &lt;strong&gt;HTTP Basic Auth&lt;/strong&gt; als Authentifizierungsmethode und trage
Benutzer und Passwort ein. Ohne Zugangsdaten bekommt Kuma nur &lt;code&gt;401&lt;/code&gt; und meldet
den Monitor als &lt;strong&gt;Down&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Speichern – nach wenigen Sekunden steht der Monitor auf &lt;strong&gt;Online&lt;/strong&gt; (grün) und zeigt&lt;br&gt;
die Antwortzeit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6l4p68upk3ym2afn5ky0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6l4p68upk3ym2afn5ky0.png" alt="Das Uptime-Kuma-Dashboard mit zwei laufenden Monitoren – beide grün mit 100 % Verfügbarkeit und „200 – OK" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Neben dem einfachen &lt;code&gt;HTTP(s)&lt;/code&gt;-Check lohnt es sich, den passenden &lt;strong&gt;Monitortyp&lt;/strong&gt; zu&lt;br&gt;
wählen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP(s) – Keyword:&lt;/strong&gt; prüft zusätzlich, ob ein bestimmtes Wort im Antworttext
steht. So erkennst du „Server antwortet zwar, zeigt aber eine Fehlerseite".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP-Port:&lt;/strong&gt; für Dienste ohne Web-Oberfläche (z. B. eine Datenbank, ein
SSH-Port). Prüft nur, ob der Port offen ist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ping:&lt;/strong&gt; einfachster Erreichbarkeitstest per ICMP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker-Container:&lt;/strong&gt; prüft direkt den Container-Status über den Docker-Socket –
praktisch für interne Dienste ohne eigene Domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push:&lt;/strong&gt; hier ruft &lt;em&gt;der überwachte Dienst&lt;/em&gt; Kuma regelmäßig an. Ideal für Cronjobs
und Backups: meldet sich das Skript nicht rechtzeitig, schlägt Kuma Alarm
(„Dead man's switch").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lege für jeden wichtigen Dienst den passenden Monitor an.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tipp&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prüfe &lt;strong&gt;öffentliche Dienste über ihre echte Domain&lt;/strong&gt; (&lt;code&gt;https://…&lt;/code&gt;), nicht über&lt;br&gt;
&lt;code&gt;localhost&lt;/code&gt;. So testest du zugleich, dass Traefik und das Zertifikat von außen&lt;br&gt;
funktionieren – nicht nur, dass der Container läuft.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Beim &lt;code&gt;HTTP(s)&lt;/code&gt;-Monitor lohnt sich zusätzlich die Option&lt;br&gt;
&lt;strong&gt;„Zertifikatsablauf-Benachrichtigung"&lt;/strong&gt;: Kuma warnt dann rechtzeitig, bevor ein&lt;br&gt;
TLS-Zertifikat ausläuft. Bei Diensten hinter Traefik erneuert Let's Encrypt zwar&lt;br&gt;
automatisch – aber genau dieser Automatismus fällt gelegentlich leise aus (ein&lt;br&gt;
DNS-Record wird geändert, Port 80 versehentlich zugemacht). Der Monitor ist dein&lt;br&gt;
Sicherheitsnetz und meldet sich, solange noch Tage zum Reparieren bleiben, statt dass&lt;br&gt;
Besucher plötzlich vor einer Zertifikatswarnung stehen.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 4: Benachrichtigungen einrichten (E-Mail und Telegram)
&lt;/h3&gt;

&lt;p&gt;Ein Monitor ohne Alarm ist nur ein hübsches Diagramm. Unter &lt;strong&gt;Einstellungen →&lt;br&gt;
Benachrichtigungen → Benachrichtigung einrichten&lt;/strong&gt; legst du einen Kanal an. Uptime&lt;br&gt;
Kuma unterstützt über 90 – wir richten die zwei häufigsten komplett ein: &lt;strong&gt;E-Mail&lt;/strong&gt;&lt;br&gt;
für die klassische Nachricht und &lt;strong&gt;Telegram&lt;/strong&gt; für Push direkt aufs Handy.&lt;/p&gt;
&lt;h4&gt;
  
  
  E-Mail (SMTP)
&lt;/h4&gt;

&lt;p&gt;Wähle als &lt;strong&gt;Benachrichtigungsdienst&lt;/strong&gt; „E-Mail (SMTP)" und trage die Daten deines&lt;br&gt;
Mailanbieters ein:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hostname / Port:&lt;/strong&gt; z. B. &lt;code&gt;smtp.DEIN_ANBIETER.de&lt;/code&gt; und &lt;code&gt;587&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sicherheit:&lt;/strong&gt; &lt;code&gt;STARTTLS&lt;/code&gt; (Port 587) oder &lt;code&gt;TLS/SSL&lt;/code&gt; (Port 465)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benutzername / Passwort:&lt;/strong&gt; deine SMTP-Zugangsdaten&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absender- / Empfänger-Adresse:&lt;/strong&gt; von welcher Adresse die Warnung kommt und an
welche sie geschickt wird&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flqeddg6t2n6z4fjkhjxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flqeddg6t2n6z4fjkhjxy.png" alt="Die E-Mail-Benachrichtigung (SMTP) in Uptime Kuma: Hostname, Port, Sicherheit und Absenderadresse" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Klick auf &lt;strong&gt;Test&lt;/strong&gt; – innerhalb weniger Sekunden landet eine Testnachricht im&lt;br&gt;
Postfach. Erst wenn die wirklich ankommt, stimmen Zugangsdaten und Port. Speichern&lt;br&gt;
nicht vergessen.&lt;/p&gt;
&lt;h4&gt;
  
  
  Telegram
&lt;/h4&gt;

&lt;p&gt;Telegram ist ideal für sofortige Push-Alarme aufs Handy – ohne eigenen Mailserver.&lt;br&gt;
Du brauchst zwei Angaben:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bot-Token:&lt;/strong&gt; Schreib in Telegram &lt;a href="https://t.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt; an, sende
&lt;code&gt;/newbot&lt;/code&gt;, vergib einen Namen – BotFather antwortet mit dem &lt;strong&gt;Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat-ID:&lt;/strong&gt; Schreib deinem neuen Bot eine beliebige Nachricht, ruf dann
&lt;code&gt;https://api.telegram.org/bot&amp;lt;DEIN_TOKEN&amp;gt;/getUpdates&lt;/code&gt; im Browser auf und lies die
&lt;code&gt;chat.id&lt;/code&gt; aus der Antwort.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Trage Token und Chat-ID in die Telegram-Benachrichtigung ein (Uptime Kuma verlinkt&lt;br&gt;
beide Hilfen direkt im Dialog) und klick &lt;strong&gt;Test&lt;/strong&gt; – die Nachricht sollte sofort im&lt;br&gt;
Chat erscheinen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy366egb2w88ax7nvfid2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy366egb2w88ax7nvfid2.png" alt="Die Telegram-Benachrichtigung in Uptime Kuma: Bot-Token und Chat-ID, mit Direktlink zum BotFather" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Sonst bleibt der Alarm stumm&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eine eingerichtete Benachrichtigung greift &lt;strong&gt;nicht automatisch&lt;/strong&gt;. Aktiviere sie in&lt;br&gt;
jedem Monitor (Häkchen im Monitor-Formular) oder schalte im Benachrichtigungs-Dialog&lt;br&gt;
„Standardmäßig aktiviert" ein und „Auf alle existierenden Monitore anwenden".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Willst du Push-Nachrichten komplett in Eigenregie (ohne Telegram-Server), kommt&lt;br&gt;
später &lt;strong&gt;ntfy&lt;/strong&gt; dazu – selbst gehostet, mit eigenem Rezept in der Serie.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 5: Eine öffentliche Status-Seite (optional)
&lt;/h3&gt;

&lt;p&gt;Kuma kann eine &lt;strong&gt;Status-Seite&lt;/strong&gt; veröffentlichen, auf der Besucher sehen, ob deine&lt;br&gt;
Dienste laufen. So gehst du vor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Statusseiten → Neue Status-Seite&lt;/strong&gt;, dann &lt;strong&gt;Name&lt;/strong&gt; (z. B. „Serverküche Status")
und &lt;strong&gt;Slug&lt;/strong&gt; (die URL, z. B. &lt;code&gt;serverkueche&lt;/code&gt;) vergeben und auf &lt;strong&gt;Weiter&lt;/strong&gt; klicken.&lt;/li&gt;
&lt;li&gt;Im Editor &lt;strong&gt;Gruppe hinzufügen&lt;/strong&gt; (z. B. „Dienste"), darunter über das Auswahlfeld
die gewünschten &lt;strong&gt;Monitore&lt;/strong&gt; hinzufügen.&lt;/li&gt;
&lt;li&gt;Oben rechts &lt;strong&gt;Speichern&lt;/strong&gt; – fertig. Öffentlich erreichbar ist die Seite dann unter
&lt;code&gt;https://status.DEINE_DOMAIN/status/serverkueche&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nimm nur auf, was wirklich jeder sehen darf – interne Dienste besser weglassen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4a51eyoxpmgqm4rkr7jj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4a51eyoxpmgqm4rkr7jj.png" alt="Die öffentliche Status-Seite „Serverküche Status" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 6: Fehlalarme vermeiden – das Alarm-Verhalten feinjustieren
&lt;/h3&gt;

&lt;p&gt;Ein Monitor, der bei jedem kurzen Netzwerk-Schluckauf Alarm schlägt, wird schnell&lt;br&gt;
ignoriert – und dann verpasst du den echten Ausfall. Im Monitor-Formular stellst du&lt;br&gt;
das Verhalten passend ein:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wiederholungen (Retries):&lt;/strong&gt; Erst nach &lt;em&gt;n&lt;/em&gt; fehlgeschlagenen Prüfungen gilt der
Dienst als „Down". &lt;code&gt;2&lt;/code&gt;–&lt;code&gt;3&lt;/code&gt; filtert einzelne Aussetzer heraus, ohne echte Ausfälle
lange zu verschleiern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat-Intervall bei Ausfall:&lt;/strong&gt; Kuma darf im Fehlerfall häufiger prüfen
(z. B. alle 20 Sekunden), um die Erholung schnell zu erkennen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erneut benachrichtigen:&lt;/strong&gt; Kuma kann dich alle &lt;em&gt;x&lt;/em&gt; Minuten erinnern, solange ein
Dienst down ist – nützlich, damit ein Ausfall nachts nicht in einer einzigen
Mail untergeht.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Wartungsfenster einplanen&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Planst du ein Update mit Downtime, lege unter &lt;strong&gt;Wartung&lt;/strong&gt; ein &lt;strong&gt;Wartungsfenster&lt;/strong&gt; an.&lt;br&gt;
Kuma pausiert dann die Alarme für die betroffenen Monitore – so bekommst du (und die&lt;br&gt;
Status-Seite) keine Fehlalarme, während du selbst am Werk bist.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Schritt 7: Cronjobs überwachen mit einem Push-Monitor
&lt;/h3&gt;

&lt;p&gt;Klassische Monitore prüfen von außen, ob ein Dienst &lt;em&gt;antwortet&lt;/em&gt;. Für Dinge, die&lt;br&gt;
&lt;strong&gt;still im Hintergrund laufen&lt;/strong&gt; – ein nächtliches Backup, ein Sync-Skript, ein&lt;br&gt;
Cronjob – dreht der &lt;strong&gt;Push-Monitor&lt;/strong&gt; das Prinzip um: Nicht Kuma fragt an, sondern&lt;br&gt;
&lt;em&gt;dein Skript meldet sich&lt;/em&gt;. Bleibt die Meldung aus, schlägt Kuma Alarm – der klassische&lt;br&gt;
„Dead man's switch".&lt;/p&gt;

&lt;p&gt;Lege einen Monitor vom Typ &lt;strong&gt;Push&lt;/strong&gt; an. Kuma zeigt dir dann eine eindeutige&lt;br&gt;
&lt;strong&gt;Push-URL&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://status.DEINE_DOMAIN/api/push/DEIN_TOKEN?status=up&amp;amp;msg=OK&amp;amp;ping=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diese URL rufst du am Ende deines Skripts auf – zum Beispiel nach einem erfolgreich&lt;br&gt;
durchgelaufenen Backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ... dein Backup-Befehl ...&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"https://status.DEINE_DOMAIN/api/push/DEIN_TOKEN?status=up&amp;amp;msg=Backup+OK"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stell das &lt;strong&gt;Prüfintervall&lt;/strong&gt; in Kuma etwas großzügiger ein als deinen Cron-Takt (läuft&lt;br&gt;
das Backup stündlich, gib Kuma z. B. 90 Minuten Toleranz). Kommt in dieser Zeit kein&lt;br&gt;
&lt;code&gt;curl&lt;/code&gt;, geht der Monitor auf &lt;strong&gt;Down&lt;/strong&gt; und du wirst benachrichtigt – du erfährst also&lt;br&gt;
von einem &lt;em&gt;nicht&lt;/em&gt; gelaufenen Backup, nicht erst, wenn du es dringend brauchst.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 8: Den Admin-Login mit 2FA absichern
&lt;/h3&gt;

&lt;p&gt;Dein Kuma-Login schützt den Zugang zu allen Monitoren, den hinterlegten&lt;br&gt;
Benachrichtigungs-Zugangsdaten und der Status-Seiten-Konfiguration – und er hängt&lt;br&gt;
öffentlich im Netz. Schalte deshalb &lt;strong&gt;Zwei-Faktor-Authentifizierung&lt;/strong&gt; ein: unter&lt;br&gt;
&lt;strong&gt;Einstellungen → Sicherheit → Zwei-Faktor-Authentifizierung&lt;/strong&gt;. Kuma zeigt einen&lt;br&gt;
QR-Code, den du mit einer Authenticator-App (z. B. Aegis oder 2FAS) scannst; zum&lt;br&gt;
Aktivieren gibst du einmal den erzeugten Code ein.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Wiederherstellung absichern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bewahre das TOTP-Secret bzw. einen zweiten Authenticator an einem sicheren Ort auf&lt;br&gt;
(Passwortmanager). Verlierst du dein Telefon &lt;strong&gt;und&lt;/strong&gt; hast keine Kopie, kommst du sonst&lt;br&gt;
nur noch über die Datenbank im &lt;code&gt;kuma-data&lt;/code&gt;-Volume wieder hinein.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wenn es nicht funktioniert
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;Bad Gateway&lt;/code&gt; (502) beim Aufruf von &lt;code&gt;status.DEINE_DOMAIN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Fast immer fehlt das Port-Label&lt;br&gt;
&lt;code&gt;traefik.http.services.kuma.loadbalancer.server.port=3001&lt;/code&gt; oder es steht ein&lt;br&gt;
falscher Port drin. Traefik erreicht den Container dann zwar, klopft aber am&lt;br&gt;
falschen Port an. Label prüfen und &lt;code&gt;docker compose up -d&lt;/code&gt; erneut ausführen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;404 page not found&lt;/code&gt; statt Kuma.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Wie bei jeder App hinter Traefik: &lt;code&gt;traefik.enable=true&lt;/code&gt;&lt;br&gt;
gesetzt? Container im &lt;code&gt;proxy&lt;/code&gt;-Netzwerk? Stimmt die Domain in der &lt;code&gt;Host(...)&lt;/code&gt;-Regel&lt;br&gt;
und zeigt der DNS-Record &lt;code&gt;status.DEINE_DOMAIN&lt;/code&gt; auf den Server? Das Traefik-Dashboard&lt;br&gt;
zeigt unter „HTTP Routers", ob &lt;code&gt;kuma&lt;/code&gt; registriert ist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Die Oberfläche lädt, aber die Live-Aktualisierung ruckelt / bricht ab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Kuma nutzt WebSockets. Traefik leitet die standardmäßig korrekt&lt;br&gt;
weiter – tritt das Problem trotzdem auf, liegt es meist an einem davorgeschalteten&lt;br&gt;
CDN/Proxy (z. B. Cloudflare im „Proxy"-Modus), der WebSockets blockt. Für den&lt;br&gt;
Direktbetrieb hinter Traefik ist keine Zusatzkonfiguration nötig.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Nach einem Neuaufsetzen sind alle Monitore weg.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Das &lt;code&gt;kuma-data&lt;/code&gt;-Volume wurde gelöscht (z. B. durch&lt;br&gt;
&lt;code&gt;docker compose down -v&lt;/code&gt;). Alle Konfiguration und Historie liegt allein in diesem&lt;br&gt;
Volume – deshalb steht es im nächsten Abschnitt ganz oben.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wartung &amp;amp; Backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sichern:&lt;/strong&gt; Das komplette Herz von Kuma ist das Volume &lt;code&gt;kuma-data&lt;/code&gt; (eine
SQLite-Datenbank). Sichere es regelmäßig – ist es weg, sind alle Monitore und die
Historie weg. Das Off-Site-Backup dafür bauen wir im
&lt;a href="https://serverkueche.de/tutorials/backups-mit-restic/" rel="noopener noreferrer"&gt;Restic-Tutorial&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates:&lt;/strong&gt; Tag &lt;code&gt;:2&lt;/code&gt; bleibt bei der 2.x-Reihe und bringt Fehlerbehebungen mit
&lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;. Vor einem Sprung auf eine neue
Hauptversion (z. B. später &lt;code&gt;:3&lt;/code&gt;) die Release-Notes lesen und vorher das Volume
sichern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Von 1.x kommend?&lt;/strong&gt; Der Wechsel auf &lt;code&gt;:2&lt;/code&gt; &lt;strong&gt;migriert die SQLite-Datenbank beim
ersten Start automatisch&lt;/strong&gt; – das kann einen Moment dauern, und ein Zurück auf &lt;code&gt;:1&lt;/code&gt;
ist danach nicht vorgesehen. Sichere deshalb &lt;strong&gt;vorher&lt;/strong&gt; das &lt;code&gt;kuma-data&lt;/code&gt;-Volume, dann
bist du auf der sicheren Seite. Neu-Installationen (wie oben) betrifft das nicht.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ehrliche Einschränkung:&lt;/strong&gt; Ein Monitor, der &lt;strong&gt;auf demselben Server&lt;/strong&gt; läuft wie die
überwachten Dienste, kann dich nicht warnen, wenn der ganze Server ausfällt – dann
ist auch Kuma offline. Ergänze für den Ernstfall einen &lt;strong&gt;externen&lt;/strong&gt; Wächter. Zwei
günstige Wege: ein &lt;strong&gt;zweites Uptime Kuma&lt;/strong&gt; auf einem kleinen Server (oder zu Hause),
das nur diese Instanz per HTTP überwacht – oder ein &lt;strong&gt;kostenloser externer
Ping-Dienst&lt;/strong&gt;, der deine öffentliche Status-Seite anpingt. So bekommst du auch dann
eine Meldung, wenn der ganze Host weg ist – der einzige Fall, den ein lokaler
Monitor prinzipbedingt nicht abdecken kann.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Damit hast du das App-Muster verinnerlicht und überwachst ab sofort alles, was du&lt;br&gt;
hinter Traefik hängst. Was jede weitere App voraussetzt, sind&lt;br&gt;
&lt;a href="https://serverkueche.de/tutorials/backups-mit-restic/" rel="noopener noreferrer"&gt;verschlüsselte Off-Site-Backups mit Restic&lt;/a&gt; –&lt;br&gt;
damit deine Daten einen Servercrash überleben.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Dieser Beitrag erschien zuerst auf &lt;a href="https://serverkueche.de/tutorials/uptime-kuma-monitoring/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>german</category>
      <category>monitoring</category>
      <category>uptime</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>netcup-Firewall im SCP einrichten (mit stateless-UDP-Kniff)</title>
      <dc:creator>serverkueche.de</dc:creator>
      <pubDate>Sun, 19 Jul 2026 09:00:46 +0000</pubDate>
      <link>https://dev.to/serverkueche/netcup-firewall-im-scp-einrichten-mit-stateless-udp-kniff-2hl</link>
      <guid>https://dev.to/serverkueche/netcup-firewall-im-scp-einrichten-mit-stateless-udp-kniff-2hl</guid>
      <description>&lt;p&gt;netcup bringt eine &lt;strong&gt;Firewall schon vor deinem Server&lt;/strong&gt; mit – im Server Control Panel,&lt;br&gt;
noch bevor ein Paket das Betriebssystem erreicht. Richtig gebaut ist sie ein starker&lt;br&gt;
Schutzschild. Es gibt dabei aber einen Kniff, an dem viele scheitern: Die netcup-Firewall&lt;br&gt;
ist bei &lt;strong&gt;UDP zustandslos&lt;/strong&gt;. Dieses Tutorial zeigt dir eine saubere, wiederverwendbare&lt;br&gt;
Firewall – und warum DNS und NTP sonst plötzlich klemmen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was bauen wir?
&lt;/h2&gt;

&lt;p&gt;Wir bauen im &lt;strong&gt;netcup SCP&lt;/strong&gt; eine Netzwerk-Firewall aus &lt;strong&gt;komponierbaren Policy-Templates&lt;/strong&gt;:&lt;br&gt;
ein &lt;strong&gt;Basis&lt;/strong&gt;-Template (das jeder Server braucht), eins für &lt;strong&gt;SSH&lt;/strong&gt; und eins für&lt;br&gt;
&lt;strong&gt;Web (HTTP/HTTPS)&lt;/strong&gt;. Diese Bausteine weist du deinem Server nach Bedarf zu – ein&lt;br&gt;
Webserver bekommt Basis + SSH + Web, ein Server ohne Website nur Basis + SSH. Am Ende ist&lt;br&gt;
eingehend alles gesperrt außer dem, was du bewusst erlaubst.&lt;/p&gt;

&lt;p&gt;Ein netcup-spezifisches Detail macht dabei den Unterschied: Bei &lt;strong&gt;UDP arbeitet die Firewall&lt;br&gt;
zustandslos&lt;/strong&gt; – weshalb Dienste wie DNS und NTP eine eigene Eingangs-Regel brauchen, sonst&lt;br&gt;
klemmen sie plötzlich. Warum das so ist und wie das Basis-Template es löst, zeigt Schritt 2.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Netzwerk-Firewall ≠ Host-Firewall&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Die netcup-Firewall arbeitet im &lt;strong&gt;Netzwerk vor dem Server&lt;/strong&gt; und ersetzt &lt;strong&gt;nicht&lt;/strong&gt; die&lt;br&gt;
Firewall auf dem Server selbst (&lt;a href="https://serverkueche.de/tutorials/firewall-ufw-einrichten/" rel="noopener noreferrer"&gt;UFW&lt;/a&gt;). Beide&lt;br&gt;
zusammen sind „Defense in Depth": Fällt eine Schicht aus oder ist falsch konfiguriert,&lt;br&gt;
greift die andere. Richte ruhig beide ein.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Voraussetzungen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ein netcup-Server, z. B. dein &lt;a href="https://serverkueche.de/tutorials/erste-schritte-netcup-vps/" rel="noopener noreferrer"&gt;erster VPS&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Deine &lt;strong&gt;SCP-Zugangsdaten&lt;/strong&gt; (Kundennummer + SCP-Passwort aus der netcup-Willkommensmail –
andere als dein SSH-Login).&lt;/li&gt;
&lt;li&gt;Klarheit über deinen &lt;strong&gt;SSH-Port&lt;/strong&gt;: Standard ist &lt;code&gt;22&lt;/code&gt;, und beim
&lt;a href="https://serverkueche.de/tutorials/ssh-absichern/" rel="noopener noreferrer"&gt;SSH-Absichern&lt;/a&gt; bleibt er auch dabei. Hast du ihn
anderweitig geändert, nimm deinen echten Port – sonst sperrst du dich beim
Aktivieren aus.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Schritt für Schritt
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Schritt 1: Firewall Policys im SCP öffnen
&lt;/h3&gt;

&lt;p&gt;Melde dich im &lt;a href="https://www.servercontrolpanel.de/" rel="noopener noreferrer"&gt;Server Control Panel&lt;/a&gt; an und öffne oben&lt;br&gt;
den Menüpunkt &lt;strong&gt;Firewall Policys&lt;/strong&gt;. Hier legst du wiederverwendbare Regelsätze („Policys")&lt;br&gt;
an – unabhängig vom einzelnen Server. Erst später weist du sie zu.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 2: Das Basis-Template bauen
&lt;/h3&gt;

&lt;p&gt;Klick auf &lt;strong&gt;Firewall Policy erstellen&lt;/strong&gt;, vergib den Namen &lt;code&gt;Serverküche Basis&lt;/code&gt; und lege über&lt;br&gt;
&lt;strong&gt;Regel hinzufügen&lt;/strong&gt; diese vier Regeln an – alle &lt;strong&gt;EINGEHEND&lt;/strong&gt; und &lt;strong&gt;ACCEPT&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Quell-Port (Src)&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DNS-Antworten (stateless)&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;53&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;beliebig&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NTP-Antworten (stateless)&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;123&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;beliebig&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ICMP (Ping/PMTU)&lt;/td&gt;
&lt;td&gt;ICMP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ICMPv6 (Neighbor Discovery)&lt;/td&gt;
&lt;td&gt;ICMPv6&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8cpz0sntlb2b3frlwopc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8cpz0sntlb2b3frlwopc.png" alt="Das Basis-Template im netcup SCP mit vier eingehenden ACCEPT-Regeln: DNS und NTP über den Quell-Port, dazu ICMP und ICMPv6" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Zwei Dinge, die hier den Unterschied machen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS/NTP über den &lt;code&gt;Src Port&lt;/code&gt;, nicht den Ziel-Port.&lt;/strong&gt; Dein Server &lt;em&gt;fragt&lt;/em&gt; DNS/NTP an
(ausgehend); die &lt;strong&gt;Antwort&lt;/strong&gt; kommt von Port 53 bzw. 123 zurück. Weil UDP zustandslos ist,
brauchst du diese Eingangs-Regel – sonst gibt es keine Namensauflösung und keine
Zeitsynchronisation, obwohl „alles andere" zu funktionieren scheint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ICMPv6 nie vergessen.&lt;/strong&gt; Ohne Neighbor Discovery bricht IPv6 komplett. ICMP (v4) dazu
ist für Ping und die Pfad-MTU-Erkennung sinnvoll. netcups Default-Policy „Ping allow"
deckt ICMP zwar schon ab – im Basis-Template steht es bewusst nochmal drin, damit dein
Fundament auch dann vollständig bleibt, wenn du die netcup-Defaults einmal entfernst.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Sobald eine Regel da ist, sperrt netcup den Rest automatisch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ein abschließendes „alles sperren" musst du &lt;strong&gt;nicht&lt;/strong&gt; anlegen. &lt;strong&gt;Ohne&lt;/strong&gt; zugewiesene Policy&lt;br&gt;
ist eingehend alles erlaubt. &lt;strong&gt;Sobald du dem Server aber mindestens eine eigene Policy&lt;br&gt;
zuweist, schaltet netcup den Default auf „block all"&lt;/strong&gt; – ab dann ist eingehend alles&lt;br&gt;
gesperrt, was keine deiner Regeln ausdrücklich erlaubt. Die Whitelist entsteht also von&lt;br&gt;
selbst; ausgehend bleibt offen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡 Nutzt dein Server DHCP?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;netcup-VPS sind in der Regel &lt;strong&gt;statisch&lt;/strong&gt; konfiguriert (&lt;code&gt;iface … inet static&lt;/code&gt;) – dann ist&lt;br&gt;
hier nichts zu tun. netcups Netz betreibt zwar einen DHCP-Server, aber weil UDP zustandslos&lt;br&gt;
ist, wird die DHCP-&lt;strong&gt;Antwort&lt;/strong&gt; (Quell-Port &lt;strong&gt;67&lt;/strong&gt;) von der Whitelist verworfen. Ist dein&lt;br&gt;
Server ausnahmsweise per DHCP konfiguriert, ergänze im Basis-Template&lt;br&gt;
&lt;code&gt;EINGEHEND · UDP · ACCEPT · Src 67&lt;/code&gt; (für DHCPv6 zusätzlich &lt;code&gt;Src 547&lt;/code&gt;) – sonst verliert er&lt;br&gt;
bei der nächsten Lease-Erneuerung seine IP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Schritt 3: Kleine Bausteine für SSH und Web
&lt;/h3&gt;

&lt;p&gt;Statt eines großen Regel-Monolithen legst du &lt;strong&gt;atomare Templates&lt;/strong&gt; an, die du frei&lt;br&gt;
kombinierst. Erstelle nach demselben Muster zwei weitere Policys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Serverküche SSH&lt;/code&gt;&lt;/strong&gt; – eine Regel: EINGEHEND, TCP, ACCEPT, Ziel-Port &lt;strong&gt;22&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Serverküche Web&lt;/code&gt;&lt;/strong&gt; – zwei Regeln: EINGEHEND, TCP, ACCEPT, Ziel-Port &lt;strong&gt;80&lt;/strong&gt; und &lt;strong&gt;443&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk0bf0hoxg505otf54545.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk0bf0hoxg505otf54545.png" alt="Die drei komponierbaren Firewall-Templates in der netcup-SCP-Übersicht: Basis, SSH und Web" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Der Gewinn: Die Basis-Regeln stehen nur &lt;strong&gt;einmal&lt;/strong&gt; da, nicht in jeder Policy dupliziert.&lt;br&gt;
Kommt später ein Dienst dazu (z. B. ein Mailserver oder WireGuard), baust du dafür ein&lt;br&gt;
weiteres kleines Template und steckst es dazu – ohne die bestehenden anzufassen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 4: Templates dem Server zuweisen
&lt;/h3&gt;

&lt;p&gt;Wechsle zu &lt;strong&gt;Server → deinen Server → Reiter „Firewall"&lt;/strong&gt; und klick auf &lt;strong&gt;Firewall Policys&lt;br&gt;
editieren&lt;/strong&gt;. Schieb aus &lt;strong&gt;Verfügbare Firewall Policys&lt;/strong&gt; die passenden nach &lt;strong&gt;Ausgewählte&lt;/strong&gt;:&lt;br&gt;
für einen Webserver &lt;code&gt;Serverküche Basis&lt;/code&gt; + &lt;code&gt;Serverküche SSH&lt;/code&gt; + &lt;code&gt;Serverküche Web&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet4fl8duyeh5nu8f9pmf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet4fl8duyeh5nu8f9pmf.png" alt="Der Zuweisungs-Dialog im netcup SCP: links die verfügbaren Templates, rechts die für diesen Server ausgewählten" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bestätige mit &lt;strong&gt;Übernehmen&lt;/strong&gt; und dann &lt;strong&gt;Speichern&lt;/strong&gt;. Prüfe, dass der Schalter &lt;strong&gt;„Firewall&lt;br&gt;
aktiv"&lt;/strong&gt; eingeschaltet ist. Die zugewiesenen Regeln erscheinen jetzt in der Liste –&lt;br&gt;
zusammen mit netcups &lt;strong&gt;Default-Policys&lt;/strong&gt; (dazu unten mehr).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuhlgd12r1iyj26r6of7a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuhlgd12r1iyj26r6of7a.png" alt="Der Firewall-Reiter des Servers mit aktiver Firewall und den zugewiesenen Regeln" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Nicht aussperren&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Aktiviere die Firewall nur, wenn die &lt;strong&gt;SSH-Regel&lt;/strong&gt; (Port 22 bzw. dein echter Port) drin&lt;br&gt;
ist. Lass während der Umstellung eine &lt;strong&gt;zweite SSH-Sitzung offen&lt;/strong&gt; und öffne parallel eine&lt;br&gt;
neue Verbindung, um den Zugang zu testen – erst wenn die klappt, die alte Sitzung&lt;br&gt;
schließen. Kommst du gar nicht mehr rein, hilft die VNC-Konsole unter &lt;strong&gt;Bildschirm&lt;/strong&gt; im SCP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Schritt 5: Fertige Templates für gängige Dienste
&lt;/h3&gt;

&lt;p&gt;Genau nach dem Muster aus Schritt 3 folgen hier fertige Vorlagen für die häufigsten&lt;br&gt;
Selfhosting-Dienste. Jede ist ein &lt;strong&gt;eigenes kleines Template&lt;/strong&gt;, alle Regeln sind&lt;br&gt;
&lt;strong&gt;EINGEHEND&lt;/strong&gt; und &lt;strong&gt;ACCEPT&lt;/strong&gt;. Bau nur die, die du wirklich brauchst, und steck sie wie in&lt;br&gt;
Schritt 4 zu &lt;code&gt;Serverküche Basis&lt;/code&gt; + &lt;code&gt;Serverküche SSH&lt;/code&gt; dazu.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Bei UDP-Diensten: Ziel-Port, nicht Quell-Port&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Für UDP-Dienste, die du &lt;strong&gt;selbst anbietest&lt;/strong&gt; (VPN, TURN, eigener DNS-Server), steht der&lt;br&gt;
Port im &lt;strong&gt;Ziel-Port (Dst)&lt;/strong&gt; – denn hier verbinden sich Clients &lt;em&gt;zu deinem Server&lt;/em&gt;. Das ist&lt;br&gt;
das &lt;strong&gt;Gegenteil&lt;/strong&gt; der DNS/NTP-Regeln aus dem Basis-Template: Dort steht der Port im&lt;br&gt;
&lt;strong&gt;Quell-Port (Src)&lt;/strong&gt;, weil dein Server dort der &lt;em&gt;fragende Client&lt;/em&gt; ist, dessen Antwort&lt;br&gt;
zurückkommt. TCP-Antworten sind davon nicht betroffen (der Firewall-Zustand spielt nur bei&lt;br&gt;
UDP keine Rolle); ausgehend ist ohnehin alles erlaubt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Mailserver (mailcow &amp;amp; Co.)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Quell-Port (Src)&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SMTP (Mailannahme anderer Server)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Submission (STARTTLS)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;587&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Submission (implizites TLS)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;465&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IMAP (STARTTLS)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;143&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IMAPS&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;993&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POP3 (STARTTLS)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;110&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POP3S&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;995&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ManageSieve (Filterregeln)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4190&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zwei Dinge, die beim Mailserver &lt;strong&gt;zusätzlich&lt;/strong&gt; dazugehören:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web-UI und Zertifikate:&lt;/strong&gt; mailcow braucht &lt;code&gt;80&lt;/code&gt;/&lt;code&gt;443&lt;/code&gt; für das Webinterface und den
Let's-Encrypt-Abruf – dafür einfach das &lt;code&gt;Serverküche Web&lt;/code&gt;-Template mit zuweisen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ausgehendes SMTP freischalten:&lt;/strong&gt; Ein Mailserver muss auf &lt;strong&gt;Port 25 ausgehend&lt;/strong&gt;
zustellen können. netcup sperrt das per Default-Policy „netcup Mail block" – die musst du
am Server &lt;strong&gt;entfernen&lt;/strong&gt; (siehe „Wenn es nicht funktioniert"). Und ohne korrekten
&lt;a href="https://serverkueche.de/tutorials/domain-mit-server-verbinden/" rel="noopener noreferrer"&gt;PTR-/Reverse-DNS-Eintrag&lt;/a&gt; landen deine Mails
im Spam.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  WireGuard-VPN
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Quell-Port (Src)&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WireGuard&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;51820&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;51820&lt;/code&gt; ist der übliche Standard – nimm den Wert aus deinem &lt;code&gt;ListenPort&lt;/code&gt;. Nur diese eine&lt;br&gt;
eingehende Regel ist nötig; die Antworten an die Clients gehen ausgehend raus.&lt;/p&gt;

&lt;h4&gt;
  
  
  OpenVPN
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Quell-Port (Src)&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenVPN (UDP, Standard)&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1194&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenVPN (TCP-Fallback)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1194&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Standard ist &lt;strong&gt;UDP 1194&lt;/strong&gt;. Die TCP-Regel nur, wenn du OpenVPN bewusst über TCP betreibst&lt;br&gt;
(manche legen es zusätzlich auf &lt;code&gt;TCP 443&lt;/code&gt;, um durch restriktive fremde Netze zu kommen) –&lt;br&gt;
sonst weglassen.&lt;/p&gt;

&lt;h4&gt;
  
  
  Zabbix-Monitoring
&lt;/h4&gt;

&lt;p&gt;Hier kommt es auf die Rolle des Servers an:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rolle&lt;/th&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Überwachter Host&lt;/strong&gt; (Zabbix-Agent)&lt;/td&gt;
&lt;td&gt;passive Checks vom Server&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10050&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zabbix-Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trapper (aktive Agenten/Proxies)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10051&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Der Zabbix-Server braucht zusätzlich &lt;code&gt;80&lt;/code&gt;/&lt;code&gt;443&lt;/code&gt; fürs Web-Frontend (&lt;code&gt;Serverküche Web&lt;/code&gt;).&lt;br&gt;
Betreibst du &lt;strong&gt;aktive&lt;/strong&gt; Checks, verbindet sich der Agent ausgehend zum Server auf &lt;code&gt;10051&lt;/code&gt; –&lt;br&gt;
dafür ist am Agenten-Host &lt;strong&gt;keine&lt;/strong&gt; eingehende Regel nötig.&lt;/p&gt;

&lt;h4&gt;
  
  
  Checkmk
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rolle&lt;/th&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Überwachter Host&lt;/strong&gt; (Agent)&lt;/td&gt;
&lt;td&gt;Agent-Controller, Pull-Modus&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6556&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Checkmk-Server&lt;/strong&gt; (optional)&lt;/td&gt;
&lt;td&gt;Agent-Receiver (Push/Registrierung)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Checkmk-Server&lt;/strong&gt; (optional)&lt;/td&gt;
&lt;td&gt;Livestatus (verteiltes Monitoring)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6557&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Im &lt;strong&gt;Standard-Pull-Modus&lt;/strong&gt; holt der Checkmk-Server die Daten aktiv ab – er verbindet sich&lt;br&gt;
also ausgehend zu &lt;code&gt;6556&lt;/code&gt; der Agenten. Eingehend braucht deshalb nur der &lt;strong&gt;überwachte Host&lt;/strong&gt;&lt;br&gt;
den Port &lt;code&gt;6556&lt;/code&gt;. Der Checkmk-Server selbst kommt mit &lt;code&gt;80&lt;/code&gt;/&lt;code&gt;443&lt;/code&gt; (&lt;code&gt;Serverküche Web&lt;/code&gt;) aus;&lt;br&gt;
&lt;code&gt;8000&lt;/code&gt; und &lt;code&gt;6557&lt;/code&gt; nur, wenn du Push-Modus bzw. verteiltes Monitoring nutzt.&lt;/p&gt;

&lt;h4&gt;
  
  
  Coturn / TURN-Server (Nextcloud Talk, Matrix)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Quell-Port (Src)&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;STUN/TURN&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3478&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STUN/TURN&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3478&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STUN/TURN über TLS&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5349&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STUN/TURN über TLS&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5349&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medien-Relay (Audio/Video)&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49152–65535&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Die große UDP-Relay-Range ist der Standard – über sie laufen die eigentlichen&lt;br&gt;
Audio-/Video-Streams. Du kannst sie in der coturn-Konfiguration (&lt;code&gt;min-port&lt;/code&gt;/&lt;code&gt;max-port&lt;/code&gt;)&lt;br&gt;
enger fassen und die Firewall-Regel dann auf denselben, kleineren Bereich setzen.&lt;/p&gt;

&lt;h4&gt;
  
  
  Eigener DNS-Server (AdGuard Home, Pi-hole, Unbound)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Quell-Port (Src)&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DNS-Anfragen&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;53&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS-Anfragen (große Antworten/TCP)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;53&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS-over-TLS (DoT)&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;853&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Achtung, das ist genau die &lt;strong&gt;Gegenrichtung&lt;/strong&gt; zum Basis-Template: Dort erlaubt &lt;code&gt;Src 53&lt;/code&gt; die&lt;br&gt;
&lt;em&gt;Antworten&lt;/em&gt; auf deine eigenen DNS-Anfragen; hier erlaubt &lt;code&gt;Dst 53&lt;/code&gt; die &lt;em&gt;Anfragen fremder&lt;br&gt;
Clients&lt;/em&gt; an deinen DNS-Server. Beides existiert problemlos nebeneinander.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Kein offener Resolver&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ein weltweit offener rekursiver DNS-Server wird für &lt;strong&gt;DNS-Amplification-Angriffe&lt;/strong&gt;&lt;br&gt;
missbraucht. Beschränke den Zugriff auf deine eigenen Netze (Quell-Adresse in der Regel&lt;br&gt;
setzen) oder biete DNS ausschließlich verschlüsselt (DoT/DoH) für angemeldete Clients an –&lt;br&gt;
öffne &lt;code&gt;53&lt;/code&gt; nie „mal eben" für die ganze Welt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Datenbank-Fernzugriff (nur im Notfall)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Beschreibung&lt;/th&gt;
&lt;th&gt;Protokoll&lt;/th&gt;
&lt;th&gt;Quell-Port (Src)&lt;/th&gt;
&lt;th&gt;Ziel-Port (Dst)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5432&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL / MariaDB&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3306&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛑 Datenbanken gehören nicht ins offene Internet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eine direkt erreichbare Datenbank ist ein bevorzugtes Angriffsziel. Der &lt;strong&gt;richtige&lt;/strong&gt; Weg&lt;br&gt;
ist, den Port &lt;strong&gt;gar nicht&lt;/strong&gt; zu öffnen und stattdessen über einen&lt;br&gt;
WireGuard-Tunnel oder einen SSH-Tunnel&lt;br&gt;
(&lt;code&gt;ssh -L 5432:localhost:5432 …&lt;/code&gt;) zuzugreifen. Musst du den Port trotzdem freigeben, dann&lt;br&gt;
&lt;strong&gt;nur&lt;/strong&gt; mit einer auf deine feste Admin-IP eingeschränkten Quell-Adresse in der&lt;br&gt;
netcup-Regel – niemals für alle.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wenn es nicht funktioniert
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Nach dem Aktivieren geht keine Namensauflösung mehr (&lt;code&gt;apt update&lt;/code&gt; hängt,&lt;br&gt;
&lt;code&gt;ping domain.de&lt;/code&gt; scheitert, aber &lt;code&gt;ping 1.1.1.1&lt;/code&gt; geht).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Die DNS-&lt;strong&gt;Antworten&lt;/strong&gt; werden geblockt. Prüfe im Basis-Template die&lt;br&gt;
Regel &lt;em&gt;EINGEHEND UDP ACCEPT, Src-Port 53&lt;/em&gt;. Wichtig: &lt;strong&gt;Quell&lt;/strong&gt;-Port, nicht Ziel-Port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Die Uhr driftet, oder TLS-Zertifikate werden wegen falscher Zeit abgelehnt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Die NTP-Antworten fehlen. Ergänze &lt;em&gt;EINGEHEND UDP ACCEPT, Src-Port 123&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; IPv6 funktioniert nicht mehr (v4 schon).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Es fehlt die &lt;strong&gt;ICMPv6&lt;/strong&gt;-Regel. Ohne Neighbor Discovery kann der&lt;br&gt;
Server über IPv6 nicht einmal seinen Nachbarn (Router) finden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Du kommst nach dem Aktivieren nicht mehr per SSH rein.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Die SSH-Regel fehlt oder nennt den falschen Port. Über die&lt;br&gt;
&lt;strong&gt;VNC-Konsole&lt;/strong&gt; (SCP → Bildschirm) kommst du trotzdem auf den Server; korrigiere die Policy&lt;br&gt;
und speichere neu. Genau dagegen hilft die „zweite Sitzung offen lassen"-Regel oben.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Der Server kann keine E-Mails versenden (Port 25 raus geht nicht).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Das ist &lt;strong&gt;kein&lt;/strong&gt; Fehler deiner Policy, sondern netcups&lt;br&gt;
&lt;strong&gt;Default-Policy „netcup Mail block"&lt;/strong&gt; – sie sperrt ausgehendes SMTP (Port 25/465/587) als&lt;br&gt;
Spam-Schutz. Für einen echten Mailversand musst du dieses netcup-Template am Server deaktivieren.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Der Server läuft zunächst normal und ist nach ein bis zwei Wochen plötzlich&lt;br&gt;
offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Nutzt der Server DHCP, wird die &lt;strong&gt;Lease-Erneuerung&lt;/strong&gt; von der Whitelist&lt;br&gt;
geblockt – die DHCP-Antwort kommt von UDP-Quell-Port &lt;strong&gt;67&lt;/strong&gt;, den keine Regel erlaubt, und&lt;br&gt;
UDP ist zustandslos. Ergänze &lt;code&gt;EINGEHEND · UDP · ACCEPT · Src 67&lt;/code&gt; im Basis-Template.&lt;br&gt;
Statisch konfigurierte netcup-VPS (der Standard) sind davon nicht betroffen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wartung &amp;amp; Backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nach jeder Änderung den Zugang testen.&lt;/strong&gt; Firewall-Regeln sind der klassische Weg, sich
selbst auszusperren. Zweite Sitzung offen halten, neue Verbindung prüfen, erst dann fertig.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neue Dienste = neues Template.&lt;/strong&gt; Brauchst du später einen weiteren Port (Game-Server,
WireGuard-VPN über UDP, Datenbank), leg ein kleines eigenes Template an und weise es
zusätzlich zu. Die bestehenden Bausteine bleiben unberührt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reihenfolge beachten.&lt;/strong&gt; netcup wertet die Regeln &lt;strong&gt;von oben nach unten&lt;/strong&gt; aus; die
&lt;strong&gt;erste&lt;/strong&gt; zutreffende Regel greift. Bei reinen ACCEPT-Regeln ist das egal – sobald du aber
eigene DROP-Regeln einsetzt, achte auf die Reihenfolge relativ zu den ACCEPTs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;netcups Default-Policys kennen.&lt;/strong&gt; „netcup Mail block" (ausgehendes SMTP auf Port
25/465/587 gesperrt) und „netcup Ping allow" sind vorgegeben. Über &lt;strong&gt;Default Policys
wiederherstellen&lt;/strong&gt; kommst du im Notfall auf einen bekannten, funktionierenden
Grundzustand zurück, falls du dich mit eigenen Regeln ausgesperrt hast – zusammen mit
der VNC-Konsole dein zweites Sicherheitsnetz.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Dieser Beitrag erschien zuerst auf &lt;a href="https://serverkueche.de/tutorials/netcup-firewall-einrichten/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Docker Compose verstehen: Services, Volumes, Netzwerke</title>
      <dc:creator>serverkueche.de</dc:creator>
      <pubDate>Fri, 17 Jul 2026 22:10:57 +0000</pubDate>
      <link>https://dev.to/serverkueche/docker-compose-verstehen-services-volumes-netzwerke-11je</link>
      <guid>https://dev.to/serverkueche/docker-compose-verstehen-services-volumes-netzwerke-11je</guid>
      <description>&lt;p&gt;Im &lt;a href="https://serverkueche.de/tutorials/docker-installieren/" rel="noopener noreferrer"&gt;Docker-Tutorial&lt;/a&gt; hast du das Compose-Plugin&lt;br&gt;
installiert – erklärt haben wir es noch nicht. Das holen wir jetzt nach, denn fast&lt;br&gt;
jedes App-Rezept hier beschreibt eine Anwendung als &lt;strong&gt;&lt;code&gt;compose.yaml&lt;/code&gt;&lt;/strong&gt;. Wer diese&lt;br&gt;
Datei liest wie einen Einkaufszettel, kann jedes folgende Tutorial anpassen, statt&lt;br&gt;
nur zu kopieren.&lt;/p&gt;
&lt;h2&gt;
  
  
  Was bauen wir?
&lt;/h2&gt;

&lt;p&gt;Wir bauen Schritt für Schritt einen kleinen Stack auf und lernen dabei die fünf&lt;br&gt;
Bausteine kennen, aus denen praktisch jede &lt;code&gt;compose.yaml&lt;/code&gt; besteht: &lt;strong&gt;Services&lt;/strong&gt;&lt;br&gt;
(die Container), &lt;strong&gt;Ports&lt;/strong&gt; (Erreichbarkeit von außen), &lt;strong&gt;Volumes&lt;/strong&gt; (persistente&lt;br&gt;
Daten), &lt;strong&gt;Netzwerke&lt;/strong&gt; (Container reden miteinander) und &lt;strong&gt;Umgebungsvariablen&lt;/strong&gt;&lt;br&gt;
(Konfiguration). Am Ende verstehst du, warum deine Daten einen &lt;code&gt;down&lt;/code&gt;-Befehl&lt;br&gt;
überleben – und wann nicht.&lt;/p&gt;

&lt;p&gt;Getestet mit &lt;strong&gt;Docker Compose v5.3&lt;/strong&gt; (das &lt;code&gt;docker compose&lt;/code&gt;-Plugin ohne Bindestrich –&lt;br&gt;
nicht das alte &lt;code&gt;docker-compose&lt;/code&gt; v1 mit Bindestrich).&lt;/p&gt;
&lt;h2&gt;
  
  
  Voraussetzungen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ein &lt;a href="https://serverkueche.de/tutorials/ssh-absichern/" rel="noopener noreferrer"&gt;abgesicherter Server&lt;/a&gt; mit &lt;a href="https://serverkueche.de/tutorials/docker-installieren/" rel="noopener noreferrer"&gt;installiertem
Docker&lt;/a&gt; und Compose-Plugin&lt;/li&gt;
&lt;li&gt;Der Benutzer ist in der &lt;code&gt;docker&lt;/code&gt;-Gruppe (dann brauchst du kein &lt;code&gt;sudo&lt;/code&gt; vor
&lt;code&gt;docker&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Schritt für Schritt
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Schritt 1: Die erste compose.yaml
&lt;/h3&gt;

&lt;p&gt;Eine &lt;code&gt;compose.yaml&lt;/code&gt; beschreibt &lt;strong&gt;deklarativ&lt;/strong&gt;, welche Container laufen sollen – du&lt;br&gt;
sagst &lt;em&gt;was&lt;/em&gt; du willst, nicht &lt;em&gt;wie&lt;/em&gt;. Lege einen Projektordner an; der Ordnername&lt;br&gt;
wird später zum Präfix aller Container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/compose-demo/site &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/compose-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lege eine kleine HTML-Seite an, die wir gleich ausliefern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h1&amp;gt;Hallo aus der Serverküche&amp;lt;/h1&amp;gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; site/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Und jetzt die zentrale Datei &lt;code&gt;compose.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.31&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./site:/usr/share/nginx/html:ro&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zeile für Zeile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;services:&lt;/code&gt;&lt;/strong&gt; – die oberste Ebene. Jeder Eintrag darunter ist ein Container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;web:&lt;/code&gt;&lt;/strong&gt; – ein frei wählbarer &lt;strong&gt;Service-Name&lt;/strong&gt;. Merke ihn dir, er wird später
auch zum Hostnamen im internen Netzwerk (Schritt 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;image: nginx:1.31&lt;/code&gt;&lt;/strong&gt; – das Container-Image mit &lt;strong&gt;festem Tag&lt;/strong&gt;. Nie &lt;code&gt;latest&lt;/code&gt;
verwenden: &lt;code&gt;latest&lt;/code&gt; ändert sich unter dir und macht Fehler unreproduzierbar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ports: - "8080:80"&lt;/code&gt;&lt;/strong&gt; – Format &lt;code&gt;HOST:CONTAINER&lt;/code&gt;. Port &lt;strong&gt;80 im Container&lt;/strong&gt; wird
auf &lt;strong&gt;8080 des Servers&lt;/strong&gt; gelegt. Links steht immer der Server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;volumes: - ./site:...:ro&lt;/code&gt;&lt;/strong&gt; – der lokale Ordner &lt;code&gt;site&lt;/code&gt; wird ins Web-Root
gehängt, &lt;code&gt;:ro&lt;/code&gt; = read-only. Mehr dazu in Schritt 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt;&lt;/strong&gt; – der Container startet nach einem Reboot oder
Absturz automatisch neu, außer du hast ihn selbst gestoppt. Für Server-Dienste
der sinnvolle Standard. Die Alternativen: &lt;code&gt;no&lt;/code&gt; (nie automatisch – der Default),
&lt;code&gt;always&lt;/code&gt; (startet selbst nach einem manuellen Stopp wieder, selten gewollt) und
&lt;code&gt;on-failure&lt;/code&gt; (nur nach einem Absturz mit Fehlercode). Für die allermeisten Dienste
ist &lt;code&gt;unless-stopped&lt;/code&gt; genau richtig.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starte den Stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-d&lt;/code&gt; bedeutet &lt;strong&gt;detached&lt;/strong&gt; (im Hintergrund). Beim ersten Mal lädt Docker das Image;&lt;br&gt;
danach siehst du am Ende:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Network compose-demo_default  Created
 Container compose-demo-web-1  Started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prüfe den Status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                 IMAGE        COMMAND                  SERVICE   CREATED          STATUS          PORTS
compose-demo-web-1   nginx:1.31   "/docker-entrypoint.…"   web       10 seconds ago   Up 9 seconds    0.0.0.0:8080-&amp;gt;80/tcp, [::]:8080-&amp;gt;80/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Der Container heißt &lt;code&gt;compose-demo-web-1&lt;/code&gt; – &lt;strong&gt;Projektordner + Service + Nummer&lt;/strong&gt;. Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Hallo aus der Serverküche&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Läuft. Logs eines Dienstes siehst du mit &lt;code&gt;docker compose logs web&lt;/code&gt; (oder &lt;code&gt;-f&lt;/code&gt; zum&lt;br&gt;
Mitlaufen).&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 2: Volumes – wo deine Daten wirklich liegen
&lt;/h3&gt;

&lt;p&gt;Container sind &lt;strong&gt;vergänglich&lt;/strong&gt;: Löschst du einen Container, ist alles weg, was&lt;br&gt;
&lt;em&gt;im&lt;/em&gt; Container geschrieben wurde. Damit Daten das überleben, gibt es zwei Arten von&lt;br&gt;
Volumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bind-Mount&lt;/strong&gt; (&lt;code&gt;./site:/usr/share/nginx/html&lt;/code&gt;): ein &lt;strong&gt;Ordner von deinem Server&lt;/strong&gt;
wird in den Container gehängt. Ideal für Config-Dateien, die du selbst bearbeitest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named Volume&lt;/strong&gt; (&lt;code&gt;webdata:/var/lib/...&lt;/code&gt;): ein von &lt;strong&gt;Docker verwalteter&lt;/strong&gt;
Speicher. Ideal für Datenbank-Daten – performant und sauber getrennt vom Host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Schritt 1 haben wir ein Bind-Mount genutzt. Für datenbankartige Dienste sieht&lt;br&gt;
es so aus – ändere &lt;code&gt;compose.yaml&lt;/code&gt; testweise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.31&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;webdata:/usr/share/nginx/html&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;webdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Named Volumes müssen &lt;strong&gt;zusätzlich&lt;/strong&gt; auf oberster Ebene unter &lt;code&gt;volumes:&lt;/code&gt; deklariert&lt;br&gt;
werden. Nach &lt;code&gt;docker compose up -d&lt;/code&gt; taucht das Volume auf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DRIVER    VOLUME NAME
local     compose-demo_webdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Der entscheidende Punkt kommt jetzt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Container compose-demo-web-1  Removed
 Network compose-demo_default  Removed
DRIVER    VOLUME NAME
local     compose-demo_webdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker compose down&lt;/code&gt; entfernt Container und Netzwerk – &lt;strong&gt;das Volume bleibt&lt;/strong&gt;. Genau&lt;br&gt;
deshalb überleben deine Datenbank-Inhalte ein Update. Merke dir das Gegenstück:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛑 down -v löscht Daten&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker compose down -v&lt;/code&gt; löscht &lt;strong&gt;auch die Named Volumes&lt;/strong&gt; – also alle persistenten&lt;br&gt;
Daten des Stacks. Tippe das &lt;code&gt;-v&lt;/code&gt; nie aus Reflex mit. Für ein reines Neustarten&lt;br&gt;
reicht &lt;code&gt;docker compose down&lt;/code&gt; (ohne &lt;code&gt;-v&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Schritt 3: Netzwerke – Container reden miteinander
&lt;/h3&gt;

&lt;p&gt;Compose legt automatisch &lt;strong&gt;ein Netzwerk pro Projekt&lt;/strong&gt; an (oben gesehen:&lt;br&gt;
&lt;code&gt;compose-demo_default&lt;/code&gt;). Alle Services darin erreichen sich &lt;strong&gt;über ihren&lt;br&gt;
Service-Namen&lt;/strong&gt; als Hostname – kein IP-Gefummel nötig. Das ist der Grund, warum in&lt;br&gt;
App-Tutorials die Anwendung ihre Datenbank einfach unter &lt;code&gt;db&lt;/code&gt; erreicht.&lt;/p&gt;

&lt;p&gt;Zum Beweis ein zweiter Service, der den ersten anspricht:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.31&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./site:/usr/share/nginx/html:ro&lt;/span&gt;

  &lt;span class="na"&gt;ping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curlimages/curl:8.21.0&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://web"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;depends_on: - web&lt;/code&gt;&lt;/strong&gt; – Compose startet &lt;code&gt;web&lt;/code&gt; &lt;strong&gt;vor&lt;/strong&gt; &lt;code&gt;ping&lt;/code&gt;. (Achtung: das
wartet nur auf den &lt;em&gt;Start&lt;/em&gt;, nicht auf „fertig hochgefahren" – dafür gibt es
Healthchecks, Schritt 4.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;command:&lt;/code&gt;&lt;/strong&gt; – überschreibt den Standardbefehl des Images. &lt;code&gt;ping&lt;/code&gt; ruft
&lt;code&gt;http://web&lt;/code&gt; auf – &lt;strong&gt;&lt;code&gt;web&lt;/code&gt; ist der Service-Name aus derselben Datei&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Führe nur den &lt;code&gt;ping&lt;/code&gt;-Service einmalig aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Hallo aus der Serverküche&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ping&lt;/code&gt; hat &lt;code&gt;web&lt;/code&gt; allein über den Namen erreicht – ganz ohne Port-Mapping. &lt;strong&gt;Merke:&lt;br&gt;
&lt;code&gt;ports:&lt;/code&gt; brauchst du nur, um einen Dienst von &lt;em&gt;außen&lt;/em&gt; (dem Internet) erreichbar zu&lt;br&gt;
machen.&lt;/strong&gt; Container untereinander reden über das interne Netzwerk – später lassen&lt;br&gt;
wir deshalb Datenbanken bewusst &lt;em&gt;ohne&lt;/em&gt; &lt;code&gt;ports:&lt;/code&gt; laufen.&lt;/p&gt;

&lt;p&gt;Bisher lebt jedes Netzwerk &lt;strong&gt;innerhalb&lt;/strong&gt; eines Compose-Projekts. Manchmal sollen aber&lt;br&gt;
Container aus &lt;strong&gt;verschiedenen&lt;/strong&gt; Projekten miteinander reden – das klassische Beispiel ist&lt;br&gt;
ein &lt;strong&gt;Reverse Proxy&lt;/strong&gt;, der vor vielen unabhängigen App-Stacks sitzt. Dafür gibt es das&lt;br&gt;
&lt;strong&gt;externe Netzwerk&lt;/strong&gt;: eines, das du &lt;strong&gt;einmalig von Hand&lt;/strong&gt; anlegst und das danach mehrere&lt;br&gt;
Compose-Projekte gemeinsam nutzen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In der &lt;code&gt;compose.yaml&lt;/code&gt; legst du es dann nicht neu an, sondern verweist mit &lt;code&gt;external: true&lt;/code&gt;&lt;br&gt;
auf das bereits bestehende Netzwerk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.31&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;external: true&lt;/code&gt; sagt Compose: „Dieses Netzwerk existiert schon – leg es &lt;strong&gt;nicht&lt;/strong&gt; an und&lt;br&gt;
lösch es beim &lt;code&gt;down&lt;/code&gt; &lt;strong&gt;nicht&lt;/strong&gt;." Fehlt das Netzwerk, bricht &lt;code&gt;up&lt;/code&gt; mit &lt;code&gt;network proxy&lt;br&gt;
declared as external, but could not be found&lt;/code&gt; ab – dann hast du das &lt;code&gt;docker network create&lt;/code&gt;&lt;br&gt;
vergessen. Genau dieses Muster – ein gemeinsames &lt;code&gt;proxy&lt;/code&gt;-Netz plus &lt;code&gt;external: true&lt;/code&gt; – ist&lt;br&gt;
die Grundlage des &lt;a href="https://serverkueche.de/tutorials/reverse-proxy-traefik/" rel="noopener noreferrer"&gt;Traefik-Tutorials&lt;/a&gt;, mit dem jede App&lt;br&gt;
später ihre Domain und ihr HTTPS bekommt.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 4: Konfiguration – Umgebungsvariablen, .env und Healthchecks
&lt;/h3&gt;

&lt;p&gt;Fast jede Anwendung wird über &lt;strong&gt;Umgebungsvariablen&lt;/strong&gt; konfiguriert. Zwei Wege:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beispiel/app:1.0&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=Europe/Berlin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APP_PORT=3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Geheimnisse (Passwörter, Tokens) gehören &lt;strong&gt;nicht&lt;/strong&gt; in die &lt;code&gt;compose.yaml&lt;/code&gt;, sondern in&lt;br&gt;
eine &lt;code&gt;.env&lt;/code&gt;-Datei im selben Ordner. Compose liest sie automatisch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"DB_PASSWORD=EIN_LANGES_ZUFALLSPASSWORT"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${DB_PASSWORD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ &lt;code&gt;.env&lt;/code&gt; nie ins Backup-Repo pushen&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Die &lt;code&gt;.env&lt;/code&gt; enthält Klartext-Geheimnisse. Nimm sie in eine &lt;code&gt;.gitignore&lt;/code&gt; auf, falls du&lt;br&gt;
deine Compose-Dateien versionierst, und sichere sie getrennt (verschlüsselt).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So prüfst du, ob Compose deine Datei versteht &lt;strong&gt;und&lt;/strong&gt; die &lt;code&gt;.env&lt;/code&gt;-Werte richtig&lt;br&gt;
einsetzt – ohne etwas zu starten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;config&lt;/code&gt; löst alle Variablen auf und gibt die fertige, normalisierte Konfiguration&lt;br&gt;
aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose-demo&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EIN_LANGES_ZUFALLSPASSWORT&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Steht dort dein &lt;strong&gt;echter&lt;/strong&gt; Wert statt &lt;code&gt;${DB_PASSWORD}&lt;/code&gt;, greift die &lt;code&gt;.env&lt;/code&gt;. Brauchst&lt;br&gt;
du nur einen schnellen Syntax-Check ohne die ganze Ausgabe, nimm&lt;br&gt;
&lt;code&gt;docker compose config --quiet&lt;/code&gt; – kommt nichts zurück (Exit-Code &lt;code&gt;0&lt;/code&gt;), ist die Datei&lt;br&gt;
gültig. Genau das ist auch dein erster Griff bei YAML-Fehlern (siehe unten).&lt;/p&gt;

&lt;p&gt;Ein &lt;strong&gt;Healthcheck&lt;/strong&gt; sagt Docker, wann ein Dienst wirklich bereit ist – die Basis&lt;br&gt;
dafür, dass abhängige Dienste erst dann starten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beispiel/app:1.0&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mit &lt;code&gt;condition: service_healthy&lt;/code&gt; startet &lt;code&gt;app&lt;/code&gt; erst, wenn der Healthcheck von &lt;code&gt;db&lt;/code&gt;&lt;br&gt;
grün ist – das häufigste „warum verbindet sich meine App nicht zur Datenbank?"&lt;br&gt;
verschwindet damit. Die vier Healthcheck-Felder bedeuten: &lt;strong&gt;&lt;code&gt;test&lt;/code&gt;&lt;/strong&gt; ist der Befehl,&lt;br&gt;
der im Container läuft (Exit-Code &lt;code&gt;0&lt;/code&gt; = gesund), &lt;strong&gt;&lt;code&gt;interval&lt;/code&gt;&lt;/strong&gt; der Abstand zwischen&lt;br&gt;
den Prüfungen, &lt;strong&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/strong&gt; wie lange eine Prüfung dauern darf, und &lt;strong&gt;&lt;code&gt;retries&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
wie viele Fehlversuche in Folge nötig sind, bevor der Container als &lt;code&gt;unhealthy&lt;/code&gt; gilt.&lt;br&gt;
Den aktuellen Zustand zeigt die &lt;code&gt;STATUS&lt;/code&gt;-Spalte von &lt;code&gt;docker compose ps&lt;/code&gt; als&lt;br&gt;
&lt;code&gt;(healthy)&lt;/code&gt; bzw. &lt;code&gt;(unhealthy)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 5: Der Betriebs-Werkzeugkasten
&lt;/h3&gt;

&lt;p&gt;Diese Befehle brauchst du täglich – immer &lt;strong&gt;im Projektordner&lt;/strong&gt; ausführen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;        &lt;span class="c"&gt;# starten / Änderungen anwenden&lt;/span&gt;
docker compose ps           &lt;span class="c"&gt;# Status der Services&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; web  &lt;span class="c"&gt;# Logs live mitlesen (Strg+C beendet nur das Ansehen)&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;web sh  &lt;span class="c"&gt;# Shell im laufenden Container&lt;/span&gt;
docker compose restart web  &lt;span class="c"&gt;# einen einzelnen Dienst neu starten&lt;/span&gt;
docker compose stop         &lt;span class="c"&gt;# anhalten, ohne Container/Netzwerk zu entfernen&lt;/span&gt;
docker compose pull         &lt;span class="c"&gt;# neue Image-Versionen holen&lt;/span&gt;
docker compose down         &lt;span class="c"&gt;# Stack stoppen und entfernen (Volumes bleiben)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fast alle Befehle lassen sich auf &lt;strong&gt;einen&lt;/strong&gt; Service einschränken, indem du seinen&lt;br&gt;
Namen anhängst (&lt;code&gt;docker compose logs -f web&lt;/code&gt;, &lt;code&gt;docker compose restart web&lt;/code&gt;) – ohne&lt;br&gt;
Namen gelten sie für den ganzen Stack. Der Unterschied zwischen &lt;code&gt;stop&lt;/code&gt; und &lt;code&gt;down&lt;/code&gt;:&lt;br&gt;
&lt;code&gt;stop&lt;/code&gt; hält die Container nur an (mit &lt;code&gt;start&lt;/code&gt; geht's weiter), &lt;code&gt;down&lt;/code&gt; entfernt sie&lt;br&gt;
samt Netzwerk (die Named Volumes bleiben in beiden Fällen).&lt;/p&gt;

&lt;p&gt;Ein Update läuft fast immer nach demselben Muster: Tag in der &lt;code&gt;compose.yaml&lt;/code&gt;&lt;br&gt;
hochsetzen → &lt;code&gt;docker compose pull&lt;/code&gt; → &lt;code&gt;docker compose up -d&lt;/code&gt;. Compose ersetzt nur die&lt;br&gt;
Container, deren Image sich geändert hat.&lt;/p&gt;
&lt;h3&gt;
  
  
  Schritt 6: Alles zusammen – ein realistischer App-Stack
&lt;/h3&gt;

&lt;p&gt;So sieht das Muster aus, das dir in den App-Tutorials immer wieder begegnet: eine&lt;br&gt;
Anwendung plus ihre Datenbank. Diese Datei bündelt alles aus den Schritten 1–4 –&lt;br&gt;
lies sie einmal komplett, dann hast du 90 % jeder späteren &lt;code&gt;compose.yaml&lt;/code&gt;&lt;br&gt;
verstanden:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beispiel/app:1.4&lt;/span&gt;          &lt;span class="c1"&gt;# feste Version, kein latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:3000"&lt;/span&gt;                  &lt;span class="c1"&gt;# nur die App ist von außen erreichbar&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=Europe/Berlin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://app:${DB_PASSWORD}@db:5432/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;appdata:/data&lt;/span&gt;                &lt;span class="c1"&gt;# persistente App-Daten (Named Volume)&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;   &lt;span class="c1"&gt;# startet erst, wenn db bereit ist&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=app&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${DB_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dbdata:/var/lib/postgresql/data&lt;/span&gt;   &lt;span class="c1"&gt;# die eigentlichen Datenbank-Dateien&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="c1"&gt;# kein ports: – die Datenbank ist NUR intern über den Namen "db" erreichbar&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;appdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dbdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drei Design-Entscheidungen, die du dir merken solltest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nur &lt;code&gt;app&lt;/code&gt; hat &lt;code&gt;ports:&lt;/code&gt;.&lt;/strong&gt; Die Datenbank braucht keinen offenen Host-Port – die
App erreicht sie intern über den Hostnamen &lt;code&gt;db&lt;/code&gt; (die &lt;code&gt;DATABASE_URL&lt;/code&gt; zeigt genau
dorthin). Ein nicht veröffentlichter Port ist ein Port, den niemand aus dem
Internet angreifen kann.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zwei getrennte Named Volumes.&lt;/strong&gt; App-Daten und Datenbank-Dateien liegen sauber
getrennt – das macht spätere Backups und Restores nachvollziehbar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passwort nur als &lt;code&gt;${DB_PASSWORD}&lt;/code&gt;.&lt;/strong&gt; Der echte Wert steht in der &lt;code&gt;.env&lt;/code&gt;, nicht
in dieser Datei. Dieselbe &lt;code&gt;compose.yaml&lt;/code&gt; kann so gefahrlos geteilt werden.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Genau dieses Grundgerüst – App nach außen, Datenbank nur intern, Daten in Named&lt;br&gt;
Volumes, Secrets in der &lt;code&gt;.env&lt;/code&gt; – wiederholt sich in Nextcloud, Vaultwarden,&lt;br&gt;
Paperless und den meisten anderen Rezepten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wenn es nicht funktioniert
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;yaml: line 7: did not find expected key&lt;/code&gt; (oder ähnliche YAML-Fehler)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; YAML ist &lt;strong&gt;einrückungssensibel&lt;/strong&gt; – ausschließlich Leerzeichen,&lt;br&gt;
&lt;strong&gt;niemals Tabs&lt;/strong&gt;, und pro Ebene konsistent (üblich: 2 Leerzeichen). Prüfe die Datei&lt;br&gt;
ohne sie zu starten: &lt;code&gt;docker compose config&lt;/code&gt; löst alles auf und meckert genau die&lt;br&gt;
falsche Zeile an.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;Error ... address already in use&lt;/code&gt; beim &lt;code&gt;up&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Der Host-Port (links in &lt;code&gt;8080:80&lt;/code&gt;) ist schon belegt. Finde den&lt;br&gt;
Beleger mit &lt;code&gt;sudo ss -tlnp | grep 8080&lt;/code&gt; oder wähle einen anderen Host-Port. Zwei&lt;br&gt;
Container dürfen sich denselben Host-Port nicht teilen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Eine App findet ihre Datenbank nicht (&lt;code&gt;could not translate host name&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Als Hostname muss der &lt;strong&gt;Service-Name&lt;/strong&gt; stehen (z. B. &lt;code&gt;db&lt;/code&gt;),&lt;br&gt;
nicht &lt;code&gt;localhost&lt;/code&gt;. Innerhalb eines Containers ist &lt;code&gt;localhost&lt;/code&gt; der Container selbst,&lt;br&gt;
nicht der Nachbar-Service. Und: Beide Services müssen im selben Compose-Projekt&lt;br&gt;
(derselben Datei) liegen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Nach &lt;code&gt;docker compose down&lt;/code&gt; sind alle Daten weg.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Entweder lag das Volume nicht als &lt;strong&gt;Named Volume&lt;/strong&gt; unter&lt;br&gt;
&lt;code&gt;volumes:&lt;/code&gt; vor (dann war es nur der vergängliche Container-Speicher), oder es wurde&lt;br&gt;
&lt;code&gt;down -v&lt;/code&gt; verwendet. Persistente Dienste immer mit deklariertem Named Volume fahren.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;docker-compose: command not found&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ursache &amp;amp; Lösung:&lt;/strong&gt; Das ist das alte Compose v1 (mit Bindestrich). Aktuell ist&lt;br&gt;
&lt;code&gt;docker compose&lt;/code&gt; (mit Leerzeichen, Plugin). Falls es fehlt:&lt;br&gt;
&lt;code&gt;sudo apt install docker-compose-plugin&lt;/code&gt; (siehe &lt;a href="https://serverkueche.de/tutorials/docker-installieren/" rel="noopener noreferrer"&gt;Docker-Tutorial&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wartung &amp;amp; Backups
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image-Tags pflegen:&lt;/strong&gt; Feste Tags (&lt;code&gt;nginx:1.31&lt;/code&gt;) bedeuten, dass du Updates
&lt;strong&gt;bewusst&lt;/strong&gt; durch Hochsetzen des Tags einspielst. Das ist gewollt – so entscheidest
du, wann ein Update kommt, statt überrascht zu werden. Rechne mit &lt;strong&gt;monatlichem&lt;/strong&gt;
Draufschauen auf die Release-Notes deiner Dienste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was gehört ins Backup?&lt;/strong&gt; Nicht die Container – die sind aus der &lt;code&gt;compose.yaml&lt;/code&gt;
jederzeit neu baubar. Sichern musst du &lt;strong&gt;die Named Volumes&lt;/strong&gt; (bzw. Bind-Mount-
Ordner), die &lt;code&gt;compose.yaml&lt;/code&gt; und die &lt;code&gt;.env&lt;/code&gt;. Ein durchdachtes Off-Site-Backup dieser
Daten bauen wir mit &lt;a href="https://serverkueche.de/tutorials/backups-mit-restic/" rel="noopener noreferrer"&gt;verschlüsselten Restic-Backups&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aufräumen:&lt;/strong&gt; &lt;code&gt;docker compose down&lt;/code&gt; beim Abbau eines Stacks; ungenutzte Images
räumst du mit &lt;code&gt;docker image prune&lt;/code&gt; weg. Named Volumes werden &lt;strong&gt;nie&lt;/strong&gt; automatisch
gelöscht – das ist Absicht.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Dieser Beitrag erschien zuerst auf &lt;a href="https://serverkueche.de/tutorials/docker-compose-grundlagen/" rel="noopener noreferrer"&gt;serverkueche.de&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>tutorial</category>
      <category>german</category>
    </item>
  </channel>
</rss>
