<?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>Code-Server – Serverküche</title><link>https://serverkueche.de/tags/code-server/</link><description>Code-Server – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Tue, 27 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/code-server/index.xml" rel="self" type="application/rss+xml"/><item><title>code-server: VS Code im Browser auf dem eigenen Server</title><link>https://serverkueche.de/tutorials/code-server-vscode-im-browser/</link><pubDate>Tue, 27 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/code-server-vscode-im-browser/</guid><description>code-server 4.141.0 hinter Traefik: zwei Schlösser davor, eigenes Image mit Werkzeugkette, persistente Erweiterungen aus Open VSX und harte Ressourcenlimits.</description><content:encoded><![CDATA[<p>VS Code im Browser, vom Tablet aus, vom fremden Rechner, ohne lokale Einrichtung – das ist der
verlockende Teil. Der unbequeme: Wer sich dort anmeldet, bekommt ein Terminal auf deinem Server.
Genau deshalb ist dieses Tutorial zur Hälfte ein Sicherheits-Tutorial.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Wir betreiben <strong>code-server 4.141.0</strong> (basiert auf VS Code 1.141.0) hinter Traefik, erreichbar über
HTTPS und geschützt durch <strong>zwei unabhängige Schlösser</strong>: BasicAuth in Traefik und das eigene
Passwort von code-server. Dazu bauen wir ein <strong>eigenes Image</strong> mit der Werkzeugkette, die du
wirklich brauchst – das offizielle Image bringt nämlich keine mit.</p>
<p>Am Ende kannst du im Browser eine Datei bearbeiten, sie im integrierten Terminal ausführen und das
Ergebnis abrufen. Erweiterungen überleben den Neustart, der Container läuft ohne Capabilities und
kann sich intern keine Root-Rechte mehr holen.</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>Was du hier wirklich aufmachst
  </p>
  <div class="prose-kitchen text-sm">code-server hat ein integriertes Terminal. Wer hineinkommt, kann alles tun, was der Container-Nutzer
darf – Dateien lesen, Pakete nachladen, Verbindungen nach außen aufbauen. Ein einzelnes Passwort vor
so etwas ist zu wenig. Behandle den Zugang wie einen SSH-Zugang, nicht wie ein Web-Dashboard.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li><a href="/tutorials/reverse-proxy-traefik/">Traefik als Reverse Proxy</a> mit funktionierendem
Let&rsquo;s-Encrypt-Resolver und einer Domain, die auf den Server zeigt</li>
<li><a href="/tutorials/traefik-middlewares-absichern/">BasicAuth aus dem Traefik-Härtungs-Tutorial</a> – wir
benutzen die Middleware hier direkt</li>
<li><strong>Mindestens 2 GB freier Arbeitsspeicher.</strong> Mit einer offenen Datei und aktiver
Python-Erweiterung braucht der Container <strong>500 bis 650 MB</strong>, unter Last mehr – das Limit setzen
wir auf 2 GB</li>
<li>Etwa <strong>6 GB Plattenplatz</strong>: Das Basis-Image ist 1,51 GB groß, unser erweitertes 1,99 GB</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"
     data-track-content data-content-name="Affiliate-Box · /tutorials/code-server-vscode-im-browser/" data-content-piece="VPS 2000 G12.5">
  <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>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</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.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCore · 16 GB RAM · 256 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 26,92 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Ein Editor plus Sprachserver plus dein eigenes Projekt – hier lohnt der größere VPS.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-2000-g12.5-24m-eu?ref=44083#vps-2000-g12.5-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 2000 G12.5 · {page}"
   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">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 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"
     data-voucher-api="vps-2000" data-voucher-msg-empty="Gerade ist für diesen Tarif kein Code frei – nimm den 5,95-€-Code." data-voucher-msg-limit="Für heute sind keine weiteren Codes drin – nimm den 5,95-€-Code oder versuch es morgen." data-voucher-msg-error="Der Code ließ sich gerade nicht abrufen – nimm den 5,95-€-Code."
     data-voucher-hint="Warenkorb vorher leeren – der Code legt den Server selbst hinein.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 Monat gratis</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Zum Kopieren klicken"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Code anzeigen</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">anderer Code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Einmal einlösbar, gilt für VPS 2000 G12.5.
    <a href="https://www.netcup.com/de/checkout/warenkorb?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 2000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Im Warenkorb einlösen →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">5,95 € Gutschein</span> für netcup-Neukunden:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Zum Kopieren klicken" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">immer gültig · nicht für Domains und VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/netcup-gutschein/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Alle netcup-Gutscheine →</a></p>
</div></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-verzeichnisse-anlegen-und-den-passwort-hash-erzeugen">Schritt 1: Verzeichnisse anlegen und den Passwort-Hash erzeugen</h3>
<p>code-server läuft im Container als Benutzer mit der <strong>UID 1000</strong>. Damit er in die gemounteten
Ordner schreiben kann, müssen sie ihm auch gehören:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/code-server/<span class="o">{</span>config,local,projekte<span class="o">}</span>
</span></span><span class="line"><span class="cl">chown -R 1000:1000 /opt/code-server
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /opt/code-server</span></span></code></pre></div>
</div>
<p>Die drei Ordner haben klar getrennte Aufgaben: <code>config</code> enthält die Konfiguration, <code>local</code> die
installierten Erweiterungen und den Editor-Zustand, <code>projekte</code> deinen Code. Nur der dritte enthält
Dinge, die du nicht neu erzeugen kannst.</p>
<p>Jetzt der Passwort-Hash. code-server versteht Argon2-Hashes, und Debian bringt das passende
Werkzeug mit:</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">apt-get install -y argon2
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;DEIN_CODE_SERVER_PASSWORT&#34;</span> <span class="p">|</span> argon2 <span class="s2">&#34;</span><span class="k">$(</span>openssl rand -base64 12<span class="k">)</span><span class="s2">&#34;</span> -e -id -k <span class="m">65540</span> -t <span class="m">3</span> -p <span class="m">4</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$argon2id$v=19$m=65540,t=3,p=4$XXXXXXXXXXXXXXXXXXXXXXXX$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</span></span></code></pre></div>
</div>
<p><code>openssl rand</code> liefert das Salz, <code>-id</code> wählt die empfohlene Argon2id-Variante, <code>-k 65540</code> sind rund
64 MB Speicherkosten pro Prüfung. Das macht Rateversuche teuer.</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>Hinweis
  </p>
  <div class="prose-kitchen text-sm">In der code-server-Dokumentation steht <code>npx argon2-cli</code>. Das Paket ist inzwischen als veraltet
markiert und verweist selbst auf das Werkzeug der Distribution – nimm also <code>argon2</code> aus dem
Paketmanager.</div>
</div>
<p>Den Hash trägst du in code-servers eigene Konfigurationsdatei ein, <strong>nicht</strong> in eine
Umgebungsvariable:</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="c"># /opt/code-server/config/code-server/config.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">bind-addr</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="p">:</span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">auth</span><span class="p">:</span><span class="w"> </span><span class="l">password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">hashed-password</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;$argon2id$v=19$m=65540,t=3,p=4$DEIN$HASH&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">cert</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">disable-telemetry</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">disable-update-check</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">disable-workspace-trust</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></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">mkdir -p config/code-server   <span class="c1"># Datei dort ablegen</span>
</span></span><span class="line"><span class="cl">chown -R 1000:1000 config</span></span></code></pre></div>
</div>
<p>Warum die Datei und nicht <code>environment:</code>? Weil Docker Compose jedes <code>$</code> in Umgebungsvariablen als
Variablenbeginn liest und dir den Hash zerlegt – ein Fehler, den man erst beim Login bemerkt.
Details dazu unten bei „Wenn es nicht funktioniert&quot;. <code>cert: false</code> ist richtig, weil Traefik das TLS
übernimmt. <code>disable-workspace-trust</code> erspart dir bei jedem Ordner die Rückfrage „Vertraust du den
Autoren?&quot; – auf einem Server, auf dem nur dein eigener Code liegt, ist das nur Klickarbeit.</p>
<h3 id="schritt-2-ein-eigenes-image-weil-das-offizielle-leer-ist">Schritt 2: Ein eigenes Image, weil das offizielle leer ist</h3>
<p>Das ist der Punkt, an dem die meisten Anleitungen aufhören und der Ärger anfängt. Sieh dir an, was
im offiziellen Image steckt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm --entrypoint sh codercom/code-server:4.141.0 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -c <span class="s1">&#39;for c in git curl python3 node gcc make; do printf &#34;%-8s %s\n&#34; $c &#34;$(command -v $c || echo FEHLT)&#34;; done&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">git      /usr/bin/git
</span></span><span class="line"><span class="cl">curl     /usr/bin/curl
</span></span><span class="line"><span class="cl">python3  FEHLT
</span></span><span class="line"><span class="cl">node     FEHLT
</span></span><span class="line"><span class="cl">gcc      FEHLT
</span></span><span class="line"><span class="cl">make     FEHLT</span></span></code></pre></div>
</div>
<p>Das Image enthält <strong>den Editor, sonst nichts</strong>. Eine <code>.py</code>-Datei bekommt Syntaxhervorhebung, aber
<code>python3 app.py</code> im Terminal antwortet mit <code>bash: python3: command not found</code>. Deshalb bauen wir
ein abgeleitetes Image mit genau der Werkzeugkette, die du brauchst:</p>
<div class="sk-code">
  <span class="sk-code-head">Dockerfile</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># /opt/code-server/Dockerfile</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">codercom/code-server:4.141.0</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">USER</span><span class="w"> </span><span class="s">root</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get update <span class="se">\
</span></span></span><span class="line"><span class="cl"> <span class="o">&amp;&amp;</span> apt-get install -y --no-install-recommends <span class="se">\
</span></span></span><span class="line"><span class="cl">      python3 python3-venv python3-pip build-essential jq <span class="se">\
</span></span></span><span class="line"><span class="cl"> <span class="o">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">USER</span><span class="w"> </span><span class="s">coder</span></span></span></code></pre></div>
</div>
<p>Tausch die Paketliste gegen das aus, womit du arbeitest – <code>nodejs npm</code> für JavaScript, <code>golang-go</code>
für Go. Das <code>USER coder</code> am Ende ist wichtig: Sonst startet der Editor als root.</p>
<h3 id="schritt-3-der-compose-stack">Schritt 3: Der Compose-Stack</h3>
<p>Zuerst den BasicAuth-Hash für Traefik erzeugen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm httpd:2.4-alpine htpasswd -nbB DEIN_BENUTZER DEIN_BASICAUTH_PASSWORT</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">DEIN_BENUTZER:$2y$05$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</span></span></code></pre></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>Achtung
  </p>
  <div class="prose-kitchen text-sm">In der Compose-Datei musst du <strong>jedes <code>$</code> im Hash verdoppeln</strong> (<code>$$</code>). Compose würde <code>$2y</code> sonst als
Variable lesen, leer ersetzen und dir einen kaputten Hash übergeben – der Login schlägt dann
grundlos fehl.</div>
</div>
<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="c"># /opt/code-server/compose.yaml</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">code-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">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</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">serverkueche/code-server:4.141.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">code-server</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 class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1000:1000&#34;</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">TZ</span><span class="p">:</span><span class="w"> </span><span class="l">Europe/Berlin</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">./config:/home/coder/.config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./local:/home/coder/.local</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./projekte:/home/coder/projekte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/home/coder/projekte&#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">cap_drop</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">ALL]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="kc">no</span>-<span class="l">new-privileges:true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mem_limit</span><span class="p">:</span><span class="w"> </span><span class="l">2g</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pids_limit</span><span class="p">:</span><span class="w"> </span><span class="m">512</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">proxy]</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="l">traefik.enable=true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.code.rule=Host(`DEINE_DOMAIN`)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.code.entrypoints=websecure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.code.tls.certresolver=le</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.code.middlewares=code-auth@docker</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.middlewares.code-auth.basicauth.users=DEIN_BENUTZER:$$2y$$05$$DEIN$$HASH</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.services.code.loadbalancer.server.port=8080</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">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>Vier Zeilen verdienen eine Erklärung, weil sie hier mehr bewirken als üblich:</p>
<ul>
<li><strong><code>user: &quot;1000:1000&quot;</code></strong> – der Editor läuft nicht als root, und die Dateien in <code>projekte</code> gehören
danach dir und nicht root.</li>
<li><strong><code>cap_drop: [ALL]</code></strong> – code-server braucht keine einzige Linux-Capability. Editor, Terminal und
Erweiterungen laufen damit vollständig.</li>
<li><strong><code>no-new-privileges:true</code></strong> – die schärfste Zeile. Das offizielle Image enthält
<code>/etc/sudoers.d/nopasswd</code> mit <code>coder ALL=(ALL) NOPASSWD:ALL</code>; ohne diesen Schalter ist jeder, der
ins Terminal kommt, mit einem <code>sudo</code> root <strong>im Container</strong>. Mit dem Schalter scheitert <code>sudo</code>
sauber: <code>sudo: The &quot;no new privileges&quot; flag is set, which prevents sudo from running as root.</code></li>
<li><strong><code>mem_limit: 2g</code> / <code>pids_limit: 512</code></strong> – ein durchgedrehter Sprachserver oder eine Endlosschleife
im Terminal soll den Server nicht mitnehmen.</li>
</ul>
<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>Niemals den Docker-Socket einhängen
  </p>
  <div class="prose-kitchen text-sm">Viele Beispiele im Netz mounten <code>/var/run/docker.sock</code> in code-server, damit man aus dem Editor
heraus Container starten kann. Der Socket ist gleichbedeutend mit Root auf dem <strong>Host</strong> – damit
hebelst du jede Härtung dieses Setups aus. Wenn du Container bauen willst, tu das per SSH oder in
einer separaten, dafür gedachten Umgebung.</div>
</div>
<h3 id="schritt-4-starten-und-die-schlösser-prüfen">Schritt 4: Starten und die Schlösser prüfen</h3>
<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">cd</span> /opt/code-server <span class="o">&amp;&amp;</span> docker compose up -d --build
</span></span><span class="line"><span class="cl">docker compose logs --tail <span class="m">6</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">info  code-server 4.141.0 8b41915d25547e2edd553bab03ea4cbd621f98c5
</span></span><span class="line"><span class="cl">info  Using user-data-dir /home/coder/.local/share/code-server
</span></span><span class="line"><span class="cl">info  Using config file /home/coder/.config/code-server/config.yaml
</span></span><span class="line"><span class="cl">info  HTTP server listening on http://0.0.0.0:8080/
</span></span><span class="line"><span class="cl">info    - Authentication is enabled
</span></span><span class="line"><span class="cl">info      - Using hashed-password from /home/coder/.config/code-server/config.yaml
</span></span><span class="line"><span class="cl">info    - Not serving HTTPS</span></span></code></pre></div>
</div>
<p>Die Zeilen fünf und sechs sind die Bestätigung, dass der Hash gelesen wurde. <code>Not serving HTTPS</code> ist
korrekt – das macht Traefik.</p>
<p>Jetzt die Gegenprobe von einem anderen Rechner. Ohne Zugangsdaten kommst du nicht einmal bis zum
Login-Formular:</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">curl -s -o /dev/null -w <span class="s2">&#34;%{http_code}\n&#34;</span> https://DEINE_DOMAIN/</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">401</span></span></code></pre></div>
</div>
<p>Mit BasicAuth leitet code-server auf seine eigene Anmeldung weiter:</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">curl -s -o /dev/null -w <span class="s2">&#34;%{http_code}\n&#34;</span> -u DEIN_BENUTZER:DEIN_BASICAUTH_PASSWORT https://DEINE_DOMAIN/</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">302</span></span></code></pre></div>
</div>
<p>Genau so soll es sein: zwei Hürden, zwei verschiedene Passwörter. Im Browser siehst du nach dem
BasicAuth-Dialog die Anmeldung von code-server:</p>
<p><figure class="my-6"><img src="/tutorials/code-server-vscode-im-browser/cs-login_hu_239d03f0c3d51e61.webp" srcset="/tutorials/code-server-vscode-im-browser/cs-login_hu_9356e38f62abc23a.webp 480w, /tutorials/code-server-vscode-im-browser/cs-login_hu_239d03f0c3d51e61.webp 768w, /tutorials/code-server-vscode-im-browser/cs-login_hu_8c2ebdf4354fab91.webp 1200w, /tutorials/code-server-vscode-im-browser/cs-login_hu_6a1dc164fa783700.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/code-server-vscode-im-browser/cs-login_hu_7fe395b9bcb0d987.webp"
    alt="Die Anmeldeseite von code-server mit dem Titel „Welcome to code-server&quot; und einem einzelnen Passwortfeld." title="Das zweite Schloss: code-servers eigene Passwortabfrage"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das zweite Schloss: code-servers eigene Passwortabfrage</figcaption></figure></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>Achtung
  </p>
  <div class="prose-kitchen text-sm">Nach dem Login setzt code-server ein Cookie, dessen Wert <strong>der Passwort-Hash selbst</strong> ist – ohne
Ablaufdatum und ohne die Schutzflags <code>Secure</code> und <code>HttpOnly</code>. Es gibt kein serverseitiges Abmelden:
Wer das Cookie hat, ist drin, bis du das Passwort änderst. Deshalb ausschließlich über HTTPS
betreiben und auf fremden Rechnern hinterher den Browser-Speicher leeren.</div>
</div>
<h3 id="schritt-5-arbeiten--datei-terminal-port">Schritt 5: Arbeiten – Datei, Terminal, Port</h3>
<p>Nach der Anmeldung landest du direkt im Arbeitsverzeichnis, das wir per <code>command:</code> übergeben haben.
Öffne eine Datei mit <code>Strg</code>+<code>P</code>:</p>
<p><figure class="my-6"><img src="/tutorials/code-server-vscode-im-browser/cs-editor_hu_792fdf40020eff5c.webp" srcset="/tutorials/code-server-vscode-im-browser/cs-editor_hu_39daac332fd6036d.webp 480w, /tutorials/code-server-vscode-im-browser/cs-editor_hu_792fdf40020eff5c.webp 768w, /tutorials/code-server-vscode-im-browser/cs-editor_hu_ab0455e605043e23.webp 1200w, /tutorials/code-server-vscode-im-browser/cs-editor_hu_15b34492d4a7550a.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/code-server-vscode-im-browser/cs-editor_hu_25518830880e2d03.webp"
    alt="Der code-server-Editor mit einer geöffneten Python-Datei, dem Dateibaum links und der erkannten Python-Version in der Statusleiste." title="Vertraute Oberfläche – unten rechts erkennt die Python-Erweiterung den Interpreter aus unserem Image"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Vertraute Oberfläche – unten rechts erkennt die Python-Erweiterung den Interpreter aus unserem Image</figcaption></figure></p>
<p>Der eigentliche Test ist das Terminal – über die Befehlspalette (<code>Strg</code>+<code>Shift</code>+<code>P</code>) und
„Terminal: Create New Terminal&quot;. Dort startest du dein Programm und rufst es gleich ab:</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">cd</span> rezepte-api <span class="o">&amp;&amp;</span> python3 app.py <span class="p">&amp;</span> sleep 2<span class="p">;</span> curl -s localhost:8000/rezepte <span class="p">|</span> jq .</span></span></code></pre></div>
</div>
<p><figure class="my-6"><img src="/tutorials/code-server-vscode-im-browser/cs-terminal_hu_1990b5b3f0847bd3.webp" srcset="/tutorials/code-server-vscode-im-browser/cs-terminal_hu_b35c4d3298ce57a8.webp 480w, /tutorials/code-server-vscode-im-browser/cs-terminal_hu_1990b5b3f0847bd3.webp 768w, /tutorials/code-server-vscode-im-browser/cs-terminal_hu_bad4459e85f40a9d.webp 1200w, /tutorials/code-server-vscode-im-browser/cs-terminal_hu_5ee305888979191e.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/code-server-vscode-im-browser/cs-terminal_hu_5b2260c89da6a553.webp"
    alt="Das integrierte Terminal in code-server: ein gestarteter Python-Server, ein curl-Aufruf und die formatierte JSON-Antwort." title="Bearbeiten, starten, abrufen – alles in einem Browserfenster"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Bearbeiten, starten, abrufen – alles in einem Browserfenster</figcaption></figure></p>
<p>VS Code erkennt den geöffneten Port automatisch und bietet ihn im Reiter <strong>PORTS</strong> an. Über einen
Reverse Proxy erreichst du diese weitergeleiteten Ports allerdings nicht von außen – sie sind für
die Arbeit im Editor gedacht, nicht zum Veröffentlichen.</p>
<p>Ein <code>sudo</code> im Terminal wird jetzt übrigens abgewiesen. Das ist Absicht: Die Werkzeuge, die du
brauchst, stecken schon im Image, und alles darüber hinaus gehört in den Dockerfile – nicht in eine
Ad-hoc-Installation, die beim nächsten <code>docker compose pull</code> verschwindet.</p>
<h3 id="schritt-6-erweiterungen-aus-open-vsx">Schritt 6: Erweiterungen aus Open VSX</h3>
<p>code-server benutzt <strong>nicht</strong> den Marktplatz von Microsoft, sondern <strong>Open VSX</strong>. Das ist keine
Einstellungssache, sondern eine Lizenzfrage: Der offizielle Marktplatz darf nur von Microsofts
eigenen Produkten benutzt werden.</p>
<p><figure class="my-6"><img src="/tutorials/code-server-vscode-im-browser/cs-extensions_hu_23a589e7a2db8bbe.webp" srcset="/tutorials/code-server-vscode-im-browser/cs-extensions_hu_be7177863c312b15.webp 480w, /tutorials/code-server-vscode-im-browser/cs-extensions_hu_23a589e7a2db8bbe.webp 768w, /tutorials/code-server-vscode-im-browser/cs-extensions_hu_fc26ff208247aa51.webp 1200w, /tutorials/code-server-vscode-im-browser/cs-extensions_hu_b1ad0cc1bdf056d4.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/code-server-vscode-im-browser/cs-extensions_hu_50fd5f0e62505582.webp"
    alt="Die Erweiterungsansicht in code-server mit Suchergebnissen für „python&quot; aus dem Open-VSX-Marktplatz." title="Der Open-VSX-Marktplatz – die meisten großen Erweiterungen sind da, ein paar fehlen"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Der Open-VSX-Marktplatz – die meisten großen Erweiterungen sind da, ein paar fehlen</figcaption></figure></p>
<p>Der Alltag ist besser als sein Ruf: Python, Debugger, Docker, GitLens und die verbreiteten Themes
sind vorhanden. Was fehlt, sind die Erweiterungen, die Microsoft ausschließlich für VS Code
lizenziert – allen voran <strong>Pylance</strong>, die C#-Erweiterung und Remote-Development. Für Python springt
<code>ms-python.python</code> mit dem freien Sprachserver ein.</p>
<p>Erweiterungen lassen sich auch von der Kommandozeile installieren, was sich für ein reproduzierbares
Setup lohnt:</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 <span class="nb">exec</span> code-server code-server --install-extension ms-python.python</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">Extension &#39;ms-python.vscode-python-envs&#39; v1.38.0 was successfully installed.
</span></span><span class="line"><span class="cl">Extension &#39;ms-python.debugpy&#39; v2026.6.0 was successfully installed.
</span></span><span class="line"><span class="cl">Extension &#39;ms-python.python&#39; v2026.4.0 was successfully installed.</span></span></code></pre></div>
</div>
<p>Sie landen im <code>local</code>-Volume und überstehen damit Neustart und Update:</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 restart <span class="o">&amp;&amp;</span> sleep <span class="m">10</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> code-server code-server --list-extensions</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">ms-python.debugpy
</span></span><span class="line"><span class="cl">ms-python.python
</span></span><span class="line"><span class="cl">ms-python.vscode-python-envs</span></span></code></pre></div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Der Login mit dem richtigen Passwort schlägt fehl, obwohl der Hash stimmt.</strong> Dann steht der Hash vermutlich in einer <code>.env</code> oder unter <code>environment:</code>. Docker Compose interpoliert <code>$</code>-Zeichen auch in Werten aus Env-Dateien, und aus <code>$argon2id$v=19$m=65540,...</code> wird dabei <code>=19=65540,t=3,p=4/Ntxaqbs...</code> – Compose warnt dabei nur mit <code>The &quot;argon2id&quot; variable is not set</code>. Entweder du verdoppelst jedes <code>$</code> zu <code>$$</code>, oder du machst es wie hier und legst den Hash in <code>config.yaml</code> ab, wo Compose gar nicht hinschaut.</p>
<p><strong>Traefik antwortet mit 401, obwohl du das richtige BasicAuth-Passwort eingibst.</strong> Gleiche Ursache an anderer Stelle: Im <code>basicauth.users</code>-Label muss jedes <code>$</code> des bcrypt-Hashes verdoppelt werden. Prüf mit <code>docker compose config</code>, was tatsächlich ankommt – steht dort ein verstümmelter Hash, fehlt die Verdopplung.</p>
<p><strong>Im Terminal kommt <code>bash: python3: command not found</code>.</strong> Das offizielle Image enthält nur den Editor, keine Sprachlaufzeiten – <code>git</code>, <code>curl</code> und <code>sudo</code> sind da, mehr nicht. Bau das abgeleitete Image aus Schritt 2 und starte mit <code>docker compose up -d --build</code> neu. Nachinstallieren im laufenden Container hilft nur bis zum nächsten Neustart.</p>
<p><strong>code-server kann eine Datei nicht speichern und meldet <code>Permission denied</code>.</strong> Die Ordner unter <code>projekte</code> gehören root statt der UID 1000. Das passiert, sobald du auf dem Host als root ein Verzeichnis anlegst. <code>chown -R 1000:1000 /opt/code-server/projekte</code> behebt es; angewöhnen solltest du dir, neue Projekte im Editor selbst anzulegen statt auf dem Host.</p>
<p><strong><code>sudo</code> im Terminal bricht mit <code>The &quot;no new privileges&quot; flag is set</code> ab.</strong> Das ist kein Defekt, sondern die Wirkung von <code>security_opt: no-new-privileges:true</code>. Nimm die Zeile nicht heraus – sie ist der einzige Grund, warum das mitgelieferte <code>coder ALL=(ALL) NOPASSWD:ALL</code> aus <code>/etc/sudoers.d/nopasswd</code> niemandem mehr nützt. Was du dauerhaft brauchst, gehört in den Dockerfile.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Dein Problem ist nicht dabei?
  <a href="/fehler/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Alle Fehlermeldungen durchsuchen →</a>
</p>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<p><strong>Updates</strong> laufen über den Image-Tag, und weil wir selbst bauen, sind es zwei Schritte:</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">cd</span> /opt/code-server
</span></span><span class="line"><span class="cl">sed -i <span class="s1">&#39;s/code-server:4.141.0/code-server:NEUE_VERSION/g&#39;</span> Dockerfile compose.yaml
</span></span><span class="line"><span class="cl">docker compose up -d --build
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> code-server code-server --version</span></span></code></pre></div>
</div>
<p>code-server folgt VS Code eng und veröffentlicht <strong>häufig</strong> – etwa monatlich eine neue Minor-Version.
Du musst nicht jede mitnehmen; einmal im Quartal reicht, solange keine Sicherheitsmeldung dazwischen
kommt. Denk daran, dass ein Update auch das Basis-Image erneuert und damit die Debian-Pakete aus
deinem Dockerfile – das ist erwünscht.</p>
<p><strong>Sichern</strong> musst du nur einen der drei Ordner wirklich:</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">/opt/code-server/projekte/   # dein Code – unersetzlich
</span></span><span class="line"><span class="cl">/opt/code-server/config/     # config.yaml mit dem Passwort-Hash
</span></span><span class="line"><span class="cl">/opt/code-server/Dockerfile  # deine Werkzeugkette
</span></span><span class="line"><span class="cl">/opt/code-server/compose.yaml</span></span></code></pre></div>
</div>
<p><code>local/</code> mit den Erweiterungen lässt du bewusst weg: Der Ordner wächst mit jeder Erweiterung – mit
der Python-Erweiterung sind es rund 60 MB – und ist in zwei Minuten wiederhergestellt, indem du
deine Erweiterungsliste durchinstallierst. Halt die
Liste stattdessen als Datei im Projekt fest – <code>code-server --list-extensions &gt; extensions.txt</code> – und
sichere die mit. Alles zusammen gehört in dein <a href="/tutorials/backups-mit-restic/">Restic-Backup</a>.</p>
<p><strong>Besser noch: Nutz Git.</strong> Der Editor bringt Git mit, und dein Code liegt damit ohnehin doppelt.
Auf einem <a href="/tutorials/forgejo-git-server/">eigenen Forgejo</a> bleibt er dabei im Haus.</p>
<p><strong>Realistischer Aufwand:</strong> Nach dem Aufsetzen wenig – ein Update pro Quartal, ein Blick auf den
Speicherverbrauch, wenn der Editor träge wird. Der Punkt, der Aufmerksamkeit braucht, ist nicht die
Wartung, sondern die Zugangskontrolle: Wechsle beide Passwörter, wenn ein Gerät abhandenkommt, und
prüf regelmäßig in den Traefik-Logs, wer sich am BasicAuth versucht hat.</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>Noch eine Stufe strenger
  </p>
  <div class="prose-kitchen text-sm">Wem zwei Passwörter nicht reichen, der stellt code-server komplett ins VPN und lässt Traefik nur
noch Anfragen aus dem VPN-Subnetz durch. Der Editor ist dann aus dem Internet gar nicht mehr
sichtbar – das Muster dafür steht in
<a href="/tutorials/interne-dienste-nur-im-vpn/">Dienste nur im VPN erreichbar machen</a>.</div>
</div>
]]></content:encoded></item></channel></rss>