<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Backup – Serverküche</title><link>https://serverkueche.de/en/tags/backup/</link><description>Backup – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Mon, 20 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/backup/index.xml" rel="self" type="application/rss+xml"/><item><title>Self-hosting Immich: your private photo backup behind Traefik</title><link>https://serverkueche.de/en/tutorials/self-host-immich-photos/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/self-host-immich-photos/</guid><description>Set up Immich with Docker behind Traefik: the self-hosted alternative to Google Photos – with automatic phone backup, face recognition and HTTPS.</description><content:encoded><![CDATA[<p>Thousands of photos and videos from your phone – and all of them at Google or Apple.
<strong>Immich</strong> 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.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, <strong>Immich v3</strong> runs behind your Traefik proxy, reachable at
<code>https://photos.YOUR_DOMAIN</code> with valid HTTPS. The official <strong>Immich phone app</strong> 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 (&ldquo;beach&rdquo;, &ldquo;dog&rdquo;) and let Immich group faces.</p>
<p>Immich consists of <strong>four containers</strong>: 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.</p>
<p>The appeal over Google Photos: the images don&rsquo;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 <strong>backups</strong>: if the server dies and
you have no backup, the photos are gone. That&rsquo;s exactly why the backup below is not optional
but mandatory.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Immich moves fast – pin the version
  </p>
  <div class="prose-kitchen text-sm">Immich develops rapidly. Database migrations run automatically on update and are <strong>not
backward-compatible</strong> – a downgrade is no longer cleanly possible afterwards. So <strong>always pin
a fixed version</strong> (here <code>v3.0.3</code>) instead of <code>release</code> or <code>latest</code>, and make a <strong>backup
before every update</strong>. That way you decide when to update.</div>
</div>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A running <strong>Traefik reverse proxy</strong> with the <code>proxy</code> network and the Let&rsquo;s Encrypt resolver
<code>le</code> – see <a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy with Traefik</a>.</li>
<li>A subdomain <code>photos.YOUR_DOMAIN</code> with a DNS record to your server IP – see
<a href="/en/tutorials/connect-domain-to-server/">connecting a domain to your server</a>.</li>
<li>A <strong>backup</strong>. Your photo collection is irreplaceable – first set up
<a href="/en/tutorials/restic-backups/">backups with Restic</a>.</li>
</ul>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>How big does the server need to be?
  </p>
  <div class="prose-kitchen text-sm">Immich is the most resource-hungry service in the Serverküche. Officially, <strong>at least 6 GB
RAM (8 recommended)</strong> are required – the machine-learning service for face recognition and
search in particular needs memory. The tested <strong>VPS 1000 with 8 GB RAM</strong> just meets that but
has little reserve. For a large library or several users, a server with <strong>16 GB RAM</strong> (e.g.
VPS 2000) is more relaxed. On a 4 GB server, Immich only runs with <strong>ML turned off</strong> (see
&ldquo;When things go wrong&rdquo;). And: photos need space – plan for enough disk.</div>
</div>
<p>How much RAM your photo library including AI search really needs is calculated by the
<a href="/en/server-calculator/">server calculator</a>.</p>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 2000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCores · 16 GB RAM · 512 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €19.24/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">For a large photo library with face recognition, 16 GB RAM is comfortable.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-2000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € voucher</strong> for new netcup customers:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(new customers only, no domains)</span>
</p></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-create-the-dns-record">Step 1: Create the DNS record</h3>
<p>Create an entry <code>photos.YOUR_DOMAIN</code> that points to your server IP, and check it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig +short photos.YOUR_DOMAIN</span></span></code></pre></div>
</div>
<p>Your server IP must come back – otherwise Traefik won&rsquo;t fetch a certificate later.</p>
<h3 id="step-2-prepare-the-folder-and-env">Step 2: Prepare the folder and .env</h3>
<p>Immich is configured via a <code>.env</code> file. Create the project:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/immich <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/immich</span></span></code></pre></div>
</div>
<p>Create a <code>.env</code> with the core values (set a strong DB password – <strong>letters and digits
only</strong>, no special characters, Immich&rsquo;s DB init doesn&rsquo;t like those):</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">UPLOAD_LOCATION</span><span class="o">=</span><span class="s">./library</span>
</span></span><span class="line"><span class="cl"><span class="na">DB_DATA_LOCATION</span><span class="o">=</span><span class="s">./postgres</span>
</span></span><span class="line"><span class="cl"><span class="na">DB_PASSWORD</span><span class="o">=</span><span class="s">YOUR_DB_PASSWORD</span>
</span></span><span class="line"><span class="cl"><span class="na">DB_USERNAME</span><span class="o">=</span><span class="s">postgres</span>
</span></span><span class="line"><span class="cl"><span class="na">DB_DATABASE_NAME</span><span class="o">=</span><span class="s">immich</span>
</span></span><span class="line"><span class="cl"><span class="na">IMMICH_VERSION</span><span class="o">=</span><span class="s">v3.0.3</span></span></span></code></pre></div>
</div>
<ul>
<li><strong><code>UPLOAD_LOCATION</code></strong> – this is where your <strong>photos and videos</strong> land. It&rsquo;s the most
important directory for the backup.</li>
<li><strong><code>DB_DATA_LOCATION</code></strong> – the PostgreSQL data. Must be on a <strong>local disk</strong> (no
network drive/NFS – the DB resents that).</li>
<li><strong><code>IMMICH_VERSION</code></strong> – the pinned version.</li>
</ul>
<h3 id="step-3-create-the-composeyaml">Step 3: Create the compose.yaml</h3>
<p>We adopt the official Immich template and only add the Traefik labels on the <code>immich-server</code>.
Create <code>compose.yaml</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">immich</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">immich-server</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/immich-app/immich-server:${IMMICH_VERSION}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">${UPLOAD_LOCATION}:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/localtime:/etc/localtime:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DB_HOSTNAME</span><span class="p">:</span><span class="w"> </span><span class="l">database</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">REDIS_HOSTNAME</span><span class="p">:</span><span class="w"> </span><span class="l">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_started</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">database</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.immich.rule=Host(`photos.YOUR_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.immich.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.immich.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.immich.loadbalancer.server.port=2283&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default, proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">immich-machine-learning</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">model-cache:/cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/valkey/valkey:9</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">database</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${DB_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">${DB_USERNAME}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">${DB_DATABASE_NAME}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">${DB_DATA_LOCATION}:/var/lib/postgresql/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD-SHELL&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE_NAME}&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">model-cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>Important to understand:</p>
<ul>
<li><strong>Only <code>immich-server</code> is on the <code>proxy</code> network</strong> and carries Traefik labels. The ML
service, Valkey and the database stay exclusively on the internal <code>default</code> network – not
reachable from outside.</li>
<li><strong><code>loadbalancer.server.port=2283</code></strong> – Immich listens on port 2283 in the container.</li>
<li>The <strong>database</strong> is deliberately the prebuilt Immich Postgres image with the vector
extension <em>VectorChord</em> (for image search). Don&rsquo;t just use a standard <code>postgres</code> – the
extension is then missing.</li>
<li><strong>Valkey</strong> is the Redis successor; the service is still called <code>redis</code> in the Immich
template for compatibility reasons.</li>
<li>The <strong>healthcheck</strong> on the database reports when Postgres is really ready. Via
<code>depends_on: … condition: service_healthy</code>, <code>immich-server</code> only starts then – this
prevents migration errors and the &ldquo;database not reachable&rdquo; start on the first boot.</li>
</ul>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Applying changes to the .env
  </p>
  <div class="prose-kitchen text-sm">If you later change something in the <code>.env</code>, a <code>docker compose restart</code> is <strong>not</strong> enough –
Docker only reads the environment variables when <strong>recreating</strong> the containers. Use
<code>docker compose up -d</code> then; Compose detects the change and rebuilds the affected containers.</div>
</div>
<h3 id="step-4-allow-large-uploads-traefik-timeout">Step 4: Allow large uploads (Traefik timeout)</h3>
<p>This is <strong>the</strong> stumbling block with Immich behind Traefik: by default Traefik aborts
connections after <strong>60 seconds</strong> (<code>readTimeout</code>). When uploading large videos from the phone
this leads to aborted uploads (error 502/499). Increase the timeout on the <code>websecure</code>
entrypoint in your <strong>Traefik configuration</strong> (the <code>traefik</code> service from the
<a href="/en/tutorials/traefik-reverse-proxy/">Traefik tutorial</a>):</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># ... your existing lines ...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--entrypoints.websecure.transport.respondingTimeouts.readTimeout=600s&#34;</span></span></span></code></pre></div>
</div>
<p>Restart Traefik afterwards (<code>docker compose up -d</code> in the Traefik folder). Unlike nginx,
Traefik has no fixed size limit for uploads – only this timeout has to be high.</p>
<h3 id="step-5-start-and-run-through-the-initial-setup">Step 5: Start and run through the initial setup</h3>
<p>Pull the images (several GB – this takes a while) and start:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose logs -f immich-server</span></span></code></pre></div>
</div>
<p>The images are several gigabytes in size – the first <code>pull</code> takes a few minutes depending on
your connection. Then check that all four containers are running:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<p>You should see <code>immich-server</code>, <code>immich-machine-learning</code>, <code>redis</code> and <code>database</code> with status
<code>running</code> (the database <code>healthy</code>). Wait until
<code>Immich Server is listening on http://[::1]:2283 [v3.0.3]</code> appears in the log. Then open
<code>https://photos.YOUR_DOMAIN</code>. Traefik fetches the Let&rsquo;s Encrypt certificate on the first
access (be patient for a moment). On the very first start, Immich greets you with <strong>&ldquo;Welcome
to Immich&rdquo;</strong> and the choice of whether you <strong>start fresh</strong> or restore a backup:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-immich-photos/immich-willkommen_hu_df42e8458020275e.webp" srcset="/en/tutorials/self-host-immich-photos/immich-willkommen_hu_a2524608a6612199.webp 480w, /en/tutorials/self-host-immich-photos/immich-willkommen_hu_df42e8458020275e.webp 768w, /en/tutorials/self-host-immich-photos/immich-willkommen_hu_22b944244b61376c.webp 1200w, /en/tutorials/self-host-immich-photos/immich-willkommen_hu_6355c1dbc067f168.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-immich-photos/immich-willkommen_hu_157a6edfcb1fc735.webp"
    alt="The welcome screen of Immich with the options &ldquo;Getting Started&rdquo; and &ldquo;Restore from database&rdquo;" title="First launch: choose \&#34;Getting Started\&#34; for a fresh installation"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">First launch: choose \&#34;Getting Started\&#34; for a fresh installation</figcaption></figure></p>
<p>Click <strong>Getting Started</strong> (you only need Restore if you&rsquo;re importing a backup). Then you
create the <strong>administrator account</strong> – as the first user you automatically become admin:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-immich-photos/immich-registrierung_hu_5e17e8d959b4be91.webp" srcset="/en/tutorials/self-host-immich-photos/immich-registrierung_hu_5da2b7656e747eea.webp 480w, /en/tutorials/self-host-immich-photos/immich-registrierung_hu_5e17e8d959b4be91.webp 768w, /en/tutorials/self-host-immich-photos/immich-registrierung_hu_dc0630b55968f943.webp 1200w, /en/tutorials/self-host-immich-photos/immich-registrierung_hu_94e19f1b46ee602b.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-immich-photos/immich-registrierung_hu_7d1f7c90347fc007.webp"
    alt="The Immich form &ldquo;Admin Registration&rdquo; with fields for email, password and name" title="Admin registration – the first user automatically becomes administrator"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Admin registration – the first user automatically becomes administrator</figcaption></figure></p>
<p>After the first login, a short <strong>setup wizard</strong> guides you through theme, language and basic
privacy settings:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-immich-photos/immich-onboarding_hu_605f1bfc221f0d38.webp" srcset="/en/tutorials/self-host-immich-photos/immich-onboarding_hu_3985d8f71d5bbe6b.webp 480w, /en/tutorials/self-host-immich-photos/immich-onboarding_hu_605f1bfc221f0d38.webp 768w, /en/tutorials/self-host-immich-photos/immich-onboarding_hu_7a4acec9c89174a5.webp 1200w, /en/tutorials/self-host-immich-photos/immich-onboarding_hu_afbeaf6cfb205ba2.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-immich-photos/immich-onboarding_hu_8ded2f93b7bae082.webp"
    alt="The Immich setup wizard greets the new administrator" title="The onboarding wizard after the first login"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The onboarding wizard after the first login</figcaption></figure></p>
<p>After that you land on your (still empty) <strong>timeline</strong> – the heart of Immich:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-immich-photos/immich-timeline_hu_7736fed89cfe0c0f.webp" srcset="/en/tutorials/self-host-immich-photos/immich-timeline_hu_29dd4a91051bc5d2.webp 480w, /en/tutorials/self-host-immich-photos/immich-timeline_hu_7736fed89cfe0c0f.webp 768w, /en/tutorials/self-host-immich-photos/immich-timeline_hu_5f77e6240ffda01c.webp 1200w, /en/tutorials/self-host-immich-photos/immich-timeline_hu_3e1ec3671920a427.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-immich-photos/immich-timeline_hu_d8deafe9761a334f.webp"
    alt="The empty photo timeline of Immich with the prompt to upload the first photo" title="The photo timeline – all images later flow in here (server online, v3.0.3)"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The photo timeline – all images later flow in here (server online, v3.0.3)</figcaption></figure></p>
<h3 id="step-6-connect-the-phone-app">Step 6: Connect the phone app</h3>
<p>The real benefit comes from the app. Install <strong>Immich</strong> from the App Store or Play Store (or
F-Droid). At launch it asks for the <strong>server address</strong> – enter <code>https://photos.YOUR_DOMAIN</code>
and log in with your account. Then, in the app settings, enable the <strong>backup</strong> and choose the
albums to be uploaded.</p>
<p>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.</p>
<p>After the upload, Immich keeps working in the background: it generates thumbnails, reads the
capture metadata (date, location) and lets the ML service <strong>recognize faces</strong> and index the
images for <strong>smart search</strong>. These jobs run for some time after the first big import – so
faces and search hits only appear gradually. You see the progress under <strong>Administration →
Job queues</strong>. In the sidebar you then find the typical photo features: <strong>Explore</strong> (by people
and places), the <strong>map</strong> with geolocation, <strong>albums</strong> for sharing and the <strong>memories</strong> (&ldquo;a
year ago&rdquo;).</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Videos &amp; transcoding on the VPS
  </p>
  <div class="prose-kitchen text-sm">Immich converts videos for playback in the browser (transcoding). On a normal VPS <strong>without a
graphics card</strong> 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.</div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>More users
  </p>
  <div class="prose-kitchen text-sm">For the family, create further accounts under <strong>Administration → Users</strong> – each gets its own
separate library. Registration from outside is off by default; new users only arise via the
admin account.</div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Your photos are on the internet
  </p>
  <div class="prose-kitchen text-sm">As soon as Immich is reachable via <code>photos.YOUR_DOMAIN</code>, the login page is open on the net. So
set a <strong>long, unique password</strong> for the admin and all user accounts and enable <strong>two-factor
authentication</strong> 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.</div>
</div>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>Uploads of large videos abort after about a minute (error 502 or 499).</strong> Traefik&rsquo;s
<code>readTimeout</code> (default 60 s) kicks in. Set it to <code>600s</code> (or higher) as in step 4 and restart
Traefik. This is by far the most common Immich-behind-proxy error.</p>
<p><strong>The <code>immich-machine-learning</code> container crashes or exits with &ldquo;exit 137&rdquo;, search and face
recognition don&rsquo;t work.</strong> Too little RAM – the container was killed by the system (out of
memory). Give the server more memory, or <strong>disable ML</strong> by removing the service
<code>immich-machine-learning</code> from the <code>compose.yaml</code>. Immich then runs without face recognition
and smart search, but upload and timeline work normally.</p>
<p><strong>After an update the containers no longer start or report migration errors.</strong> Server, machine
learning and database must run on the <strong>same</strong> version. Set the new <code>IMMICH_VERSION</code> in the
<code>.env</code> and update <strong>all</strong> containers together (<code>docker compose pull &amp;&amp; docker compose up -d</code>).
A downgrade after a migration isn&rsquo;t possible – only a reset from the backup.</p>
<p><strong>Immich shows &ldquo;maintenance mode&rdquo; / &ldquo;temporarily unavailable&rdquo;.</strong> Immich v3 starts into a
maintenance mode under certain database states. The log (<code>docker compose logs immich-server</code>)
then contains a URL with a <strong>one-time token</strong> (<code>…/maintenance?token=…</code>) – through it you log
into the maintenance mode and choose &ldquo;restart&rdquo; or end it. After that the normal interface is
back.</p>
<p><strong>Container won&rsquo;t start, &ldquo;permission denied&rdquo; on the database or upload directory.</strong>
<code>UPLOAD_LOCATION</code> or <code>DB_DATA_LOCATION</code> 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.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li>
<p><strong>Two things belong in the backup – consistently together.</strong> Your <strong>photos</strong>
(<code>UPLOAD_LOCATION</code>, i.e. <code>~/immich/library</code>) and the <strong>database</strong>. The DB dump contains only
the metadata; without the matching files it&rsquo;s worthless, and vice versa. Cleanest is to stop
Immich briefly and back up both together:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose stop immich-server
</span></span><span class="line"><span class="cl">docker compose <span class="nb">exec</span> -T database pg_dump --clean --if-exists <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dbname<span class="o">=</span>immich --username<span class="o">=</span>postgres <span class="p">|</span> gzip &gt; immich-db.sql.gz
</span></span><span class="line"><span class="cl">docker compose start immich-server</span></span></code></pre></div>
</div>
<p>Back up the dump together with the <code>library</code> folder <strong>encrypted and off-site</strong> with
<a href="/en/tutorials/restic-backups/">Restic</a>. Alternatively, Immich can generate automatic DB
dumps under <strong>Administration → Job queues</strong> – you still have to back up the photos
separately.</p>
</li>
<li>
<p><strong>Rehearse the worst case once.</strong> A backup you&rsquo;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
<code>library</code> folder in the same spot – Immich offers the option <strong>&ldquo;Restore from database&rdquo;</strong> on
the first start for this (the second button from step 5). What matters is the <strong>same
version</strong>: the database state and the program must match.</p>
</li>
<li>
<p><strong>Backup before every update.</strong> Because migrations aren&rsquo;t reversible, the backup is your only
way back. Back up first, then raise <code>IMMICH_VERSION</code>, then
<code>docker compose pull &amp;&amp; docker compose up -d</code>.</p>
</li>
<li>
<p><strong>Honest about the effort:</strong> Immich often brings several releases per month. You don&rsquo;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.</p>
</li>
<li>
<p><strong>Keep an eye on disk space.</strong> Photos and videos grow steadily; Immich additionally creates
thumbnails and converted videos. Monitor the disk usage (e.g. with
<a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a>) so the server doesn&rsquo;t fill up.</p>
</li>
</ul>
]]></content:encoded></item><item><title>Restic backups: encrypted and off-site</title><link>https://serverkueche.de/en/tutorials/restic-backups/</link><pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/restic-backups/</guid><description>Off-site backups with Restic: set up encrypted, restore snapshots, prune with retention and automate via a systemd timer.</description><content:encoded><![CDATA[<p>A snapshot at the provider is <strong>not a backup</strong>: it sits on the same infrastructure and
is gone too if your account is suspended, the data center burns down, or you
accidentally delete the wrong project. <strong>Restic</strong> turns it into a real backup –
encrypted, deduplicated and in a place <strong>outside</strong> your server.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, <strong>Restic</strong> backs up your important data – app volumes, configurations,
database dumps – <strong>end-to-end encrypted</strong> and <strong>deduplicated</strong> into a repository,
ideally <strong>off-site</strong> (a second server or storage). You can restore individual files or
whole snapshots at any time, automatically thin out old backups by a retention policy,
and check integrity. Finally, everything runs automatically via a systemd timer and
alerts you if a run fails to happen.</p>
<p>Tested with <strong>Restic 0.18</strong> (the version from the Debian 13 package sources); upstream
already has 0.19.x – how to update is in step 1. Restic <strong>always</strong> encrypts – there is
no unencrypted repository.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Snapshot ≠ backup
  </p>
  <div class="prose-kitchen text-sm">The <a href="/en/tutorials/netcup-snapshots-scp/">snapshot in the provider panel</a> is a
convenient &ldquo;rewind&rdquo; – but it sits on the <strong>same</strong> infrastructure and under the <strong>same</strong>
account. If the provider, the data center or your access fails, the snapshot is gone
too. A real backup lives <strong>outside</strong> (another provider/location), is <strong>encrypted</strong> and
you can restore it <strong>independently</strong>. That&rsquo;s exactly Restic&rsquo;s job – the snapshot stays
the quick safety net for small mishaps.</div>
</div>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A server (Debian 13) with the data you want to back up – e.g. the volumes and
Compose files of your <a href="/en/tutorials/install-docker/">Docker apps</a>.</li>
<li>An <strong>off-site target</strong> for the backups. Restic speaks, among others:
<ul>
<li><strong>SFTP</strong> – a second server or a storage box (easiest to start with),</li>
<li><strong>S3-compatible</strong> – e.g. an object-storage bucket,</li>
<li><strong>rclone</strong> – as a bridge to Backblaze B2, Google Drive and dozens more.</li>
</ul>
</li>
<li>Basic familiarity with the shell.</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €10.36/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">A small second VPS at a different location is a cheap, dedicated backup target via SFTP.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € voucher</strong> for new netcup customers:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(new customers only, no domains)</span>
</p></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-install-restic">Step 1: Install Restic</h3>
<p>Restic is in the package sources and is updated with the system:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install -y restic</span></span></code></pre></div>
</div>
<p>Check the version:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic version</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">restic 0.18.0 compiled with go1.24.4 on linux/amd64</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>A newer version if needed
  </p>
  <div class="prose-kitchen text-sm">Upstream, Restic is sometimes ahead (currently 0.19.x). If you need a newer feature,
download the official binary from the
<a href="https://github.com/restic/restic/releases">Restic release page</a> and place it in
<code>/usr/local/bin</code> (that takes precedence over <code>/usr/bin</code>). The built-in command
<code>restic self-update</code> does <strong>not</strong> work with the Debian package version – the package is
built without that feature (<code>unknown command &quot;self-update&quot;</code>). For most people the
package version is entirely enough.</div>
</div>
<h3 id="step-2-create-the-encrypted-repository">Step 2: Create the encrypted repository</h3>
<p>A Restic <strong>repository</strong> is the encrypted storage location of your backups. It&rsquo;s
protected with a password – and this password does <strong>not</strong> belong in a script and
<strong>not</strong> next to the repository. Put it in a file only root may read:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo install -d -m <span class="m">700</span> /etc/restic
</span></span><span class="line"><span class="cl">openssl rand -base64 <span class="m">32</span> <span class="p">|</span> sudo tee /etc/restic/password &gt; /dev/null
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">600</span> /etc/restic/password</span></span></code></pre></div>
</div>
<p>The <code>openssl</code> command generates a long random password and stores it under
<code>/etc/restic/password</code> (the <code>install</code> command before it creates the directory, the
<code>chmod</code> after it protects the file). Tell Restic via environment variables where the
repository is and where the password is – here first an off-site target via SFTP:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">RESTIC_REPOSITORY</span><span class="o">=</span><span class="s2">&#34;sftp:BACKUP_USER@YOUR_BACKUP_HOST:/backups/YOUR_SERVER&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">RESTIC_PASSWORD_FILE</span><span class="o">=</span><span class="s2">&#34;/etc/restic/password&#34;</span></span></span></code></pre></div>
</div>
<p>Replace <code>BACKUP_USER</code>, <code>YOUR_BACKUP_HOST</code> and the path with your real target. If you
want to practice locally first, use a folder instead:
<code>export RESTIC_REPOSITORY=&quot;/srv/restic-repo&quot;</code>. The commands afterwards are
<strong>identical</strong> – only the repository address differs.</p>
<p>You use other off-site targets via the same variable, only with a different prefix:</p>
<ul>
<li><strong>S3-compatible:</strong> <code>s3:https://ENDPOINT/BUCKET</code> – plus the credentials as
<code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code> in the environment (or in the
environment file from step 7).</li>
<li><strong>rclone:</strong> <code>rclone:REMOTE:path</code> – the bridge to Backblaze B2, Google Drive and many
more, once you&rsquo;ve set up the rclone remote.</li>
</ul>
<p>No matter which backend: encryption, deduplication and all the following commands stay
the same.</p>
<p>Now initialize the repository (the example output shows the local practice repository –
with the SFTP target, your <code>sftp:</code> address appears there instead):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic init</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">created restic repository d29613209a at /srv/restic-repo
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Please note that knowledge of your password is required to access
</span></span><span class="line"><span class="cl">the repository. Losing your password means that your data is
</span></span><span class="line"><span class="cl">irrecoverably lost.</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Password lost = data lost
  </p>
  <div class="prose-kitchen text-sm">There is <strong>no</strong> back door. Without the repository password your backups are
irrecoverably encrypted. Back up the contents of <code>/etc/restic/password</code> additionally in
a <strong>password manager</strong> – separate from the server, otherwise it&rsquo;s no use to you if the
server is lost.</div>
</div>
<h3 id="step-3-the-first-backup">Step 3: The first backup</h3>
<p>Back up the directory with your data. The <code>--tag</code> helps with filtering later:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic backup /root/backup-demo --tag demo</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">no parent snapshot found, will read all files
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Files:           3 new,     0 changed,     0 unmodified
</span></span><span class="line"><span class="cl">Dirs:            2 new,     0 changed,     0 unmodified
</span></span><span class="line"><span class="cl">Added to the repository: 2.002 MiB (2.001 MiB stored)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">processed 3 files, 2.000 MiB in 0:00
</span></span><span class="line"><span class="cl">snapshot f16c6ffd saved</span></span></code></pre></div>
</div>
<p>On the <strong>second</strong> run, Restic&rsquo;s strength shows: it only backs up the changes
(deduplication). If a file changes and you back up again:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">using parent snapshot f16c6ffd
</span></span><span class="line"><span class="cl">Files:           0 new,     1 changed,     2 unmodified
</span></span><span class="line"><span class="cl">Added to the repository: 1.820 KiB (1.121 KiB stored)
</span></span><span class="line"><span class="cl">processed 3 files, 2.000 MiB in 0:00
</span></span><span class="line"><span class="cl">snapshot 0bb5e679 saved</span></span></code></pre></div>
</div>
<p>Instead of 2 MiB again, only <strong>1.1 KiB</strong> land in the repository – just the changed
part.</p>
<p>What belongs in the backup? For Docker apps that&rsquo;s three things – recipe, payload, DB:</p>
<ul>
<li><strong>Compose files</strong> (your &ldquo;recipe&rdquo;) and <code>.env</code>: small but indispensable for rebuilding
the stack.</li>
<li><strong>Bind mounts</strong> (<code>./data:/…</code>) live directly in the project folder – you back those up
right along. <strong>Named volumes</strong> you&rsquo;ll find under
<code>/var/lib/docker/volumes/&lt;name&gt;/_data</code>; include the path.</li>
<li><strong>Databases</strong> never as a live file – a backup mid-write is potentially unusable.
Instead create a <strong>consistent dump</strong> (<code>pg_dump</code>/<code>mariadb-dump</code>) and back that up (see
step 7).</li>
</ul>
<p>You exclude the unnecessary:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic backup /opt --tag apps --exclude<span class="o">=</span><span class="s1">&#39;*.log&#39;</span> --exclude<span class="o">=</span><span class="s1">&#39;**/cache/**&#39;</span></span></span></code></pre></div>
</div>
<h3 id="step-4-restore">Step 4: Restore</h3>
<p>A backup is only as good as its restore. First list the snapshots:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic snapshots</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ID        Time                 Host                  Tags        Paths              Size
</span></span><span class="line"><span class="cl">---------------------------------------------------------------------------------------------
</span></span><span class="line"><span class="cl">f16c6ffd  2026-07-16 20:52:35  v2200000000000000000  demo        /root/backup-demo  2.000 MiB
</span></span><span class="line"><span class="cl">0bb5e679  2026-07-16 20:52:36  v2200000000000000000  demo        /root/backup-demo  2.000 MiB
</span></span><span class="line"><span class="cl">---------------------------------------------------------------------------------------------
</span></span><span class="line"><span class="cl">2 snapshots</span></span></code></pre></div>
</div>
<p>Restore a complete snapshot to a target directory (use <code>latest</code> for the newest or a
specific ID):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic restore latest --target /root/restore-test</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">restoring snapshot 0bb5e679 of [/root/backup-demo] ... to /root/restore-test
</span></span><span class="line"><span class="cl">Summary: Restored 5 files/dirs (2.000 MiB) in 0:00</span></span></code></pre></div>
</div>
<p>If you only need <strong>one</strong> file, there are two ways – restore that specific path:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic restore latest --target /tmp/wiederher --include /root/backup-demo/config.yaml</span></span></code></pre></div>
</div>
<p>… or stream the file directly to standard output, without any detour:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic dump latest /root/backup-demo/config.yaml &gt; /tmp/config.yaml</span></span></code></pre></div>
</div>
<p>To <strong>browse</strong>, you can even mount the repository as a filesystem (<code>restic mount /mnt/backup</code>, requires FUSE) and copy out what you need in the file manager – handy when
you no longer know which snapshot the version you&rsquo;re looking for is in.</p>
<h3 id="step-5-check-integrity">Step 5: Check integrity</h3>
<p><code>check</code> verifies that the repository&rsquo;s structure is intact:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic check</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">load indexes
</span></span><span class="line"><span class="cl">check all packs
</span></span><span class="line"><span class="cl">check snapshots, trees and blobs
</span></span><span class="line"><span class="cl">[0:00] 100.00%  2 / 2 snapshots
</span></span><span class="line"><span class="cl">no errors were found</span></span></code></pre></div>
</div>
<p>That checks the metadata, but not the actual data. So regularly read part of the real
data along with it – e.g. 10% per run, i.e. everything over the course of weeks:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic check --read-data-subset<span class="o">=</span>10%</span></span></code></pre></div>
</div>
<h3 id="step-6-prune-with-a-retention-policy">Step 6: Prune with a retention policy</h3>
<p>Without pruning, the repository grows forever. <code>forget</code> applies a <strong>retention policy</strong>,
<code>--prune</code> frees the storage of the removed snapshots:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">restic forget --keep-daily <span class="m">7</span> --keep-weekly <span class="m">4</span> --keep-monthly <span class="m">6</span> --prune</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Applying Policy: keep 7 daily, 4 weekly, 6 monthly snapshots
</span></span><span class="line"><span class="cl">keep 2 snapshots:
</span></span><span class="line"><span class="cl">2 snapshots</span></span></code></pre></div>
</div>
<p>This keeps the last 7 daily, 4 weekly and 6 monthly snapshots – a good default. The
tiers interlock: fine resolution for the recent past (one per day), coarser the further
back (one per week, then one per month). So, if in doubt, you can get close to the
desired state without the repository growing unbounded. Older snapshots drop off
automatically. <strong><code>--prune</code> is the expensive part</strong> (it repacks data); on large
repositories you often run it only once or twice a week, not on every backup.</p>
<h3 id="step-7-automate-everything-systemd-timer">Step 7: Automate everything (systemd timer)</h3>
<p>A backup you have to start by hand is one you forget. Put the credentials in an
environment file (so the service knows them):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># /etc/restic/restic.env</span>
</span></span><span class="line"><span class="cl"><span class="nv">RESTIC_REPOSITORY</span><span class="o">=</span>sftp:BACKUP_USER@YOUR_BACKUP_HOST:/backups/YOUR_SERVER
</span></span><span class="line"><span class="cl"><span class="nv">RESTIC_PASSWORD_FILE</span><span class="o">=</span>/etc/restic/password</span></span></code></pre></div>
</div>
<p>Then a wrapper script <code>/usr/local/bin/restic-backup.sh</code> – it first dumps the database
(PostgreSQL example), backs up, prunes and finally reports success to
<a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a> (the push monitor as a &ldquo;dead man&rsquo;s
switch&rdquo;):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="c1"># 0) Ensure the target directory for the dump (otherwise the first run aborts)</span>
</span></span><span class="line"><span class="cl">mkdir -p /opt/dumps
</span></span><span class="line"><span class="cl"><span class="c1"># 1) Dump the database consistently (never back up the live DB file)</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> YOUR_DB_CONTAINER pg_dumpall -U postgres &gt; /opt/dumps/db.sql
</span></span><span class="line"><span class="cl"><span class="c1"># 2) Backup</span>
</span></span><span class="line"><span class="cl">restic backup /opt/apps /opt/dumps --tag auto
</span></span><span class="line"><span class="cl"><span class="c1"># 3) Prune (retention)</span>
</span></span><span class="line"><span class="cl">restic forget --keep-daily <span class="m">7</span> --keep-weekly <span class="m">4</span> --keep-monthly <span class="m">6</span> --prune
</span></span><span class="line"><span class="cl"><span class="c1"># 4) Report success to Uptime Kuma</span>
</span></span><span class="line"><span class="cl">curl -fsS <span class="s2">&#34;https://status.YOUR_DOMAIN/api/push/YOUR_TOKEN?status=up&amp;msg=restic+OK&#34;</span> &gt; /dev/null</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chmod <span class="m">700</span> /usr/local/bin/restic-backup.sh</span></span></code></pre></div>
</div>
<p>The service loads the environment file and runs the script –
<code>/etc/systemd/system/restic-backup.service</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">EnvironmentFile</span><span class="o">=</span><span class="s">/etc/restic/restic.env</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/bin/restic-backup.sh</span></span></span></code></pre></div>
</div>
<p>And the timer <code>/etc/systemd/system/restic-backup.timer</code> starts it nightly:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">*-*-* 02:30:00</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span></span></span></code></pre></div>
</div>
<p><code>Persistent=true</code> catches up a missed run (e.g. because the server was off at night) on
the next start. Enable it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl daemon-reload
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now restic-backup.timer</span></span></code></pre></div>
</div>
<p>Check with <code>systemctl list-timers restic-backup.timer</code> when the next run is due.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Don&#39;t let backup time collide with the automatic reboot
  </p>
  <div class="prose-kitchen text-sm">If the server runs an <a href="/en/tutorials/unattended-upgrades-automatic-updates/">automatic reboot via
unattended-upgrades</a> (in that
example <code>04:00</code>), the backup must be done <strong>before</strong> then. If the running backup run
(<code>restic backup</code> or the subsequent <code>restic forget --prune</code>) is cut off mid-operation by
the reboot, a <strong>stale lock</strong> remains in the repository (<code>repository is already locked</code>,
see below). That&rsquo;s why the timer here starts at <code>02:30</code> – enough headroom for a larger
backup plus pruning. If you adjust either time, deliberately keep the gap large.</div>
</div>
<h3 id="step-8-rehearse-the-worst-case-once">Step 8: Rehearse the worst case once</h3>
<p>A backup you&rsquo;ve never restored is just a hope. So deliberately run through a complete
restore once – ideally on a <strong>different</strong> or freshly set-up server, so you also need the
credentials (repository address <strong>and</strong> password) under real conditions:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">RESTIC_REPOSITORY</span><span class="o">=</span><span class="s2">&#34;sftp:BACKUP_USER@YOUR_BACKUP_HOST:/backups/YOUR_SERVER&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">RESTIC_PASSWORD_FILE</span><span class="o">=</span><span class="s2">&#34;/etc/restic/password&#34;</span>
</span></span><span class="line"><span class="cl">restic snapshots
</span></span><span class="line"><span class="cl">restic restore latest --target /tmp/dr-test</span></span></code></pre></div>
</div>
<p>Then compare samples with the original (<code>diff</code>, file hashes) and boot the app from the
restored data as a test. Only once that works do you know your backup holds up in an
emergency – and that you&rsquo;ll remember the procedure when it really counts.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong><code>subprocess ssh: Host key verification failed</code> with the SFTP target.</strong> Restic can&rsquo;t
confirm the SSH host key of the backup server because it isn&rsquo;t known yet. Connect
<strong>once manually</strong> (<code>ssh BACKUP_USER@YOUR_BACKUP_HOST</code>) and confirm the fingerprint – or
store it with <code>ssh-keyscan YOUR_BACKUP_HOST &gt;&gt; ~/.ssh/known_hosts</code>. After that Restic
runs through.</p>
<p><strong><code>wrong password</code> or <code>repository does not exist</code>.</strong> <code>RESTIC_REPOSITORY</code> or
<code>RESTIC_PASSWORD_FILE</code> isn&rsquo;t set or points nowhere – so check the <code>EnvironmentFile</code> in
the systemd service. A manually set <code>export</code> only applies in the current shell.</p>
<p><strong><code>repository is already locked</code>.</strong> An aborted run left a lock. Check that <strong>really
no</strong> backup is running anymore, then <code>restic unlock</code>. Never unlock blindly while a run
is active in parallel.</p>
<p><strong>The backup gets huge / backs up nonsense.</strong> Restrict it with
<code>--exclude</code>/<code>--exclude-file</code> (caches, logs, temporary files) and use
<code>--one-file-system</code> so Restic doesn&rsquo;t dive into mounted foreign filesystems.</p>
<p><strong><code>prune</code> takes forever or was aborted.</strong> No reason to panic – <code>prune</code> is resumable and
the repository stays valid. Start it again and afterwards run <code>restic check</code> once.
Separate <code>--prune</code> from the daily backup on large repos (see step 6).</p>
<p><strong>The timer runs, but a success message never arrives in Uptime Kuma.</strong> Look at the
last run with <code>journalctl -u restic-backup.service</code>. Usually the script aborts
<strong>before</strong> the <code>curl</code> call (e.g. the DB dump failed) – thanks to <code>set -euo pipefail</code> it
then stops cleanly instead of reporting a broken backup as success. So the missing push
isn&rsquo;t a bug, but exactly the alarm signal you want.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Test restores regularly</strong> – that&rsquo;s the single most important point. A backup you&rsquo;ve
never restored is not a backup but a hope. Schedule a test restore into an empty
directory <strong>monthly</strong>.</li>
<li><strong>3-2-1 rule:</strong> at least 3 copies, on 2 different media, of which 1 is <strong>off-site</strong>.
Restic covers the off-site piece – but &ldquo;off-site&rdquo; really means a <strong>different
provider/location</strong>, not the same data center.</li>
<li><strong>Monitor backups:</strong> without monitoring, you only notice a dead backup in an
emergency. The push call from step 7 to
<a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a> raises the alarm when a run fails
to happen.</li>
<li><strong>Check integrity:</strong> run <code>restic check --read-data-subset</code> regularly so silent bit rot
is caught before you need the data.</li>
<li><strong>Password/key management:</strong> keep the repository password separate from the server.
For individual clients you can assign additional passwords with <code>restic key add</code>
without sharing the main password.</li>
</ul>
]]></content:encoded></item><item><title>netcup snapshots &amp; the Server Control Panel (SCP)</title><link>https://serverkueche.de/en/tutorials/netcup-snapshots-scp/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/netcup-snapshots-scp/</guid><description>Use the netcup Server Control Panel &amp; snapshots properly: a safety net before risky changes – and why it's no substitute for a real backup.</description><content:encoded><![CDATA[<p>Before you do something risky on the server – a big update, a delicate config change –
you want a point you can return to in minutes. That&rsquo;s exactly what <strong>netcup snapshots</strong>
are for. This tutorial shows you the <strong>Server Control Panel (SCP)</strong> and how to use
snapshots properly – and where their limit lies.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>You get to know the <strong>netcup Server Control Panel (SCP)</strong> – the web interface to your
server – and create a <strong>snapshot</strong> there: a complete image of your server disk, to which
you return in a few minutes if a mishap happens. By the end you know when a snapshot is
your rescue – and why it&rsquo;s still <strong>no substitute for a real backup</strong>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A netcup server, e.g. your <a href="/en/tutorials/first-steps-netcup-vps/">first VPS</a>.</li>
<li>Your <strong>SCP credentials</strong> – they are in the netcup welcome email and are <strong>different</strong>
from your SSH login.</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €10.36/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">All netcup servers include the SCP with its snapshot function.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € voucher</strong> for new netcup customers:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(new customers only, no domains)</span>
</p></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-log-into-the-scp-and-find-the-server">Step 1: Log into the SCP and find the server</h3>
<p>Open <a href="https://www.servercontrolpanel.de/">servercontrolpanel.de</a> and log in with your
SCP credentials. You land in the server list; a click on your server opens the
<strong>overview</strong> with product, status and hardware. At the top you see the tabs through which
you control everything – <strong>snapshots are under &ldquo;Media&rdquo;</strong>.</p>
<p><figure class="my-6"><img src="/en/tutorials/netcup-snapshots-scp/netcup-scp-uebersicht_hu_f14d52af6d3ebf4e.webp" srcset="/en/tutorials/netcup-snapshots-scp/netcup-scp-uebersicht_hu_bbfd35db587d3122.webp 480w, /en/tutorials/netcup-snapshots-scp/netcup-scp-uebersicht_hu_f14d52af6d3ebf4e.webp 768w, /en/tutorials/netcup-snapshots-scp/netcup-scp-uebersicht_hu_bd0829934b1aaf36.webp 1200w, /en/tutorials/netcup-snapshots-scp/netcup-scp-uebersicht_hu_a72f85ba080e7afb.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/netcup-snapshots-scp/netcup-scp-uebersicht_hu_4eaae45f34f5fc19.webp"
    alt="The server overview in the netcup SCP with the tabs; snapshots are under &ldquo;Media&rdquo;" title="The server overview in the SCP – the tabs lead to all functions"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The server overview in the SCP – the tabs lead to all functions</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Different login than SSH
  </p>
  <div class="prose-kitchen text-sm">The SCP has <strong>its own</strong> credentials (customer number + SCP password), independent of your
SSH access. Keep them in your password manager – via the SCP you can reach the server
even when SSH is stuck (e.g. via the VNC console under &ldquo;Display&rdquo;).</div>
</div>
<h3 id="step-2-create-a-snapshot">Step 2: Create a snapshot</h3>
<p>Go to <strong>Media → Snapshots</strong>. Under <strong>Create snapshot</strong> you fill in:</p>
<ul>
<li><strong>Name:</strong> name it after the <strong>why</strong>, not the date alone – e.g. <code>before-update-2026-07</code>.
That way you later know what it was for.</li>
<li><strong>Description:</strong> one sentence of context (optional, but helpful).</li>
<li><strong>Disk:</strong> usually <code>vda</code> (your system disk).</li>
<li><strong>Type:</strong> here lies the most important decision – see below.</li>
</ul>
<p><figure class="my-6"><img src="/en/tutorials/netcup-snapshots-scp/netcup-snapshot-erstellen_hu_ee0d5e35a238c588.webp" srcset="/en/tutorials/netcup-snapshots-scp/netcup-snapshot-erstellen_hu_6604a6e69c643091.webp 480w, /en/tutorials/netcup-snapshots-scp/netcup-snapshot-erstellen_hu_ee0d5e35a238c588.webp 768w, /en/tutorials/netcup-snapshots-scp/netcup-snapshot-erstellen_hu_a6d49e30eda0f52f.webp 1200w, /en/tutorials/netcup-snapshots-scp/netcup-snapshot-erstellen_hu_e1e85c70dd2d985b.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/netcup-snapshots-scp/netcup-snapshot-erstellen_hu_f5c77f2441eb89a8.webp"
    alt="The snapshot form in the SCP with name, description, disk and type" title="\&#34;Create snapshot\&#34; – give a name after the purpose, choose the type deliberately"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">\&#34;Create snapshot\&#34; – give a name after the purpose, choose the type deliberately</figcaption></figure></p>
<p><strong>Offline or online snapshot?</strong></p>
<ul>
<li><strong>Offline snapshot:</strong> the server is briefly <strong>shut down</strong> for the capture. This
guarantees a <strong>consistent</strong> state – the right choice when databases are running, and
when you can afford the brief downtime.</li>
<li><strong>Online snapshot:</strong> capture <strong>while running</strong>, without downtime. Convenient, but with
active writes (e.g. a database mid-write) the image can be <strong>inconsistent</strong>.</li>
</ul>
<p>A click on <strong>Create</strong> starts the process; depending on the amount of data it takes a few
minutes. It runs in the background – the creation date and type then appear in the list
above once it&rsquo;s done. Best schedule the brief downtime of an offline snapshot for a quiet
time; after that your return point is set and you can tackle the planned change.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Only one snapshot slot
  </p>
  <div class="prose-kitchen text-sm">A VPS usually has <strong>one</strong> snapshot slot. If you create a new one, it <strong>overwrites</strong> the
previous one. A snapshot is thus a <em>single</em> restore point, not a history – for multiple
version states you need real backups (see section &ldquo;Maintenance &amp; backups&rdquo;).</div>
</div>
<h3 id="step-3-restore-a-snapshot">Step 3: Restore a snapshot</h3>
<p>If something went wrong, you find the snapshot in the list under <strong>Snapshots</strong>; in the
<strong>Actions</strong> column there&rsquo;s <strong>Restore</strong>.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Restoring overwrites everything
  </p>
  <div class="prose-kitchen text-sm">A restore resets the disk <strong>completely</strong> to the snapshot state. <strong>Everything that has
happened since is lost</strong> – new data, changes, uploads. Use this only deliberately in an
emergency, not as a &ldquo;quick undo&rdquo;.</div>
</div>
<h3 id="step-4-the-other-safety-lines-in-the-scp">Step 4: The other safety lines in the SCP</h3>
<p>The SCP has more than snapshots – good to know for an emergency:</p>
<ul>
<li><strong>Display (VNC console):</strong> direct screen access to the server, even when SSH refuses –
the safety line from the <a href="/en/tutorials/harden-ssh/">SSH tutorial</a>.</li>
<li><strong>Rescue system:</strong> boots the server into an emergency Linux to repair a broken
installation.</li>
<li><strong>Media → Images / DVD drive:</strong> install a different operating system.</li>
</ul>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The new snapshot overwrote the old one.</strong> That&rsquo;s expected behavior – a VPS only has one
snapshot slot. If you want to keep a particular state, restore it or back up the data
separately <strong>before</strong> you create a new snapshot.</p>
<p><strong>After an online snapshot restore, the database is corrupted.</strong> An online snapshot may
catch a running write mid-way. For servers with a database, take an <strong>offline snapshot</strong>
or create a database dump beforehand.</p>
<p><strong>You can&rsquo;t get into the SCP.</strong> The SCP uses <strong>its own</strong> credentials (not SSH). They are
in the welcome email; a forgotten SCP password you reset in the netcup customer account
(CCP).</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Snapshot ≠ backup – the most important point.</strong> A snapshot lives at <strong>netcup</strong>, under
<strong>your</strong> account, in <strong>one</strong> slot and is meant as a momentary snapshot. If the account,
the data center or your access fails, it&rsquo;s gone too. It&rsquo;s a <strong>quick safety net</strong>, not
an off-site backup.</li>
<li><strong>Snapshot before every risky action.</strong> Before big updates, migrations or delicate
config changes: snapshot first, then start. Afterwards you can calmly replace it with
the next one.</li>
<li><strong>Snapshots age.</strong> A months-old snapshot brings you back to a long-outdated state – when
in doubt, worse than no snapshot. Renew it specifically before every larger action; a
fresh return point is worth more than an old one.</li>
<li><strong>For real safety you need both.</strong> Complement the snapshot with <strong>encrypted off-site
backups</strong> – how that works is shown in the tutorial on <a href="/en/tutorials/restic-backups/">backups with
Restic</a>. Rule of thumb: snapshot for the quick return,
Restic for &ldquo;my server is gone&rdquo;.</li>
</ul>
]]></content:encoded></item></channel></rss>