<?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/en/tags/code-server/</link><description>Code-Server – 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>Tue, 27 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/code-server/index.xml" rel="self" type="application/rss+xml"/><item><title>code-server: VS Code in the Browser on Your Own Server</title><link>https://serverkueche.de/en/tutorials/code-server-vscode-in-browser/</link><pubDate>Tue, 27 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/code-server-vscode-in-browser/</guid><description>code-server 4.141.0 behind Traefik: two locks in front, a custom image with a real toolchain, persistent Open VSX extensions and hard resource limits.</description><content:encoded><![CDATA[<p>VS Code in the browser, from a tablet, from someone else&rsquo;s machine, with no local setup – that is
the tempting part. The uncomfortable one: whoever logs in gets a terminal on your server. Which is
exactly why this tutorial is half a security tutorial.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>We run <strong>code-server 4.141.0</strong> (based on VS Code 1.141.0) behind Traefik, reachable over HTTPS and
protected by <strong>two independent locks</strong>: BasicAuth in Traefik and code-server&rsquo;s own password. On top
of that we build a <strong>custom image</strong> with the toolchain you actually need – because the official one
ships none.</p>
<p>At the end you can edit a file in the browser, run it in the integrated terminal and fetch the
result. Extensions survive a restart, the container runs without a single capability and cannot
grant itself root inside.</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>What you are really opening up
  </p>
  <div class="prose-kitchen text-sm">code-server has an integrated terminal. Whoever gets in can do anything the container user is
allowed to do – read files, pull packages, open outbound connections. A single password in front of
that is not enough. Treat this access like SSH access, not like a web dashboard.</div>
</div>
<h2 id="requirements">Requirements</h2>
<ul>
<li><a href="/en/tutorials/traefik-reverse-proxy/">Traefik as a reverse proxy</a> with a working Let&rsquo;s Encrypt
resolver and a domain pointing at the server</li>
<li><a href="/en/tutorials/traefik-middlewares-hardening/">BasicAuth from the Traefik hardening tutorial</a> – we
use that middleware directly here</li>
<li><strong>At least 2 GB of free memory.</strong> With one file open and the Python extension active the container
needs <strong>500 to 650 MB</strong>, more under load – we set the limit to 2 GB</li>
<li>Roughly <strong>6 GB of disk</strong>: the base image is 1.51 GB, our extended one 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 · /en/tutorials/code-server-vscode-in-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>🍳 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.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCores · 16 GB RAM · 256 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €26.92/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">An editor plus language servers plus your own project – the bigger VPS earns its keep here.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-2000-g12.5-iv-24m-eu?ref=44083#vps-2000-g12.5-iv-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">
  Go to 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="No code is available for this plan right now – use the €5.95 code." data-voucher-msg-limit="No more codes today – use the €5.95 code or try again tomorrow." data-voucher-msg-error="The code couldn&#39;t be fetched right now – use the €5.95 code."
     data-voucher-hint="Empty the cart first – the code adds the server itself.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 month free</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Click to copy"
            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">Show code</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">another code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Single use, valid for VPS 2000 G12.5.
    <a href="https://www.netcup.com/en/checkout/cart?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">Redeem in the cart →</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 voucher</span> for new netcup customers:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Click to copy" 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">always valid · not for domains or VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/en/netcup-voucher/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">All netcup vouchers →</a></p>
</div></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-create-the-directories-and-generate-the-password-hash">Step 1: Create the directories and generate the password hash</h3>
<p>code-server runs in the container as the user with <strong>UID 1000</strong>. For it to write into the mounted
directories, they have to belong to that user:</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>The three directories have clearly separated jobs: <code>config</code> holds the configuration, <code>local</code> the
installed extensions and editor state, <code>projekte</code> your code. Only the third one contains things you
cannot recreate.</p>
<p>Now the password hash. code-server understands Argon2 hashes, and Debian ships the right tool:</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;YOUR_CODE_SERVER_PASSWORD&#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> supplies the salt, <code>-id</code> selects the recommended Argon2id variant, and <code>-k 65540</code> is
roughly 64 MB of memory cost per verification. That makes guessing expensive.</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>Note
  </p>
  <div class="prose-kitchen text-sm">The code-server documentation suggests <code>npx argon2-cli</code>. That package is now marked deprecated and
points at your distribution&rsquo;s tool itself – so use <code>argon2</code> from the package manager.</div>
</div>
<p>You put the hash into code-server&rsquo;s own configuration file, <strong>not</strong> into an environment variable:</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$YOUR$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"># put the file there</span>
</span></span><span class="line"><span class="cl">chown -R 1000:1000 config</span></span></code></pre></div>
</div>
<p>Why the file and not <code>environment:</code>? Because Docker Compose reads every <code>$</code> in environment values
as the start of a variable and shreds your hash – a mistake you only notice at the login screen.
Details below under &ldquo;When it doesn&rsquo;t work&rdquo;. <code>cert: false</code> is correct because Traefik handles TLS.
<code>disable-workspace-trust</code> saves you the &ldquo;Do you trust the authors?&rdquo; prompt for every folder – on a
server that only holds your own code, that is pure clicking.</p>
<h3 id="step-2-a-custom-image-because-the-official-one-is-empty">Step 2: A custom image, because the official one is empty</h3>
<p>This is the point where most guides stop and the trouble starts. Look at what the official image
actually contains:</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 MISSING)&#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  MISSING
</span></span><span class="line"><span class="cl">node     MISSING
</span></span><span class="line"><span class="cl">gcc      MISSING
</span></span><span class="line"><span class="cl">make     MISSING</span></span></code></pre></div>
</div>
<p>The image contains <strong>the editor and nothing else</strong>. A <code>.py</code> file gets syntax highlighting, but
<code>python3 app.py</code> in the terminal answers <code>bash: python3: command not found</code>. So we build a derived
image with exactly the toolchain you need:</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>Swap the package list for whatever you work with – <code>nodejs npm</code> for JavaScript, <code>golang-go</code> for Go.
The trailing <code>USER coder</code> matters: without it the editor starts as root.</p>
<h3 id="step-3-the-compose-stack">Step 3: The Compose stack</h3>
<p>First generate the BasicAuth hash for Traefik:</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 YOUR_USER YOUR_BASICAUTH_PASSWORD</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">YOUR_USER:$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>Warning
  </p>
  <div class="prose-kitchen text-sm">In the Compose file you have to <strong>double every <code>$</code> in the hash</strong> (<code>$$</code>). Otherwise Compose reads
<code>$2y</code> as a variable, substitutes nothing and hands Traefik a broken hash – the login then fails for
no visible reason.</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(`YOUR_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=YOUR_USER:$$2y$$05$$YOUR$$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>Four lines deserve an explanation, because they do more here than usual:</p>
<ul>
<li><strong><code>user: &quot;1000:1000&quot;</code></strong> – the editor does not run as root, and the files under <code>projekte</code> end up
belonging to you instead of root.</li>
<li><strong><code>cap_drop: [ALL]</code></strong> – code-server needs not a single Linux capability. Editor, terminal and
extensions all work with this in place.</li>
<li><strong><code>no-new-privileges:true</code></strong> – the sharpest line. The official image ships
<code>/etc/sudoers.d/nopasswd</code> containing <code>coder ALL=(ALL) NOPASSWD:ALL</code>; without this flag anyone who
reaches the terminal is one <code>sudo</code> away from root <strong>inside the container</strong>. With it, <code>sudo</code> fails
cleanly: <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> – a runaway language server or an endless loop in the
terminal should not take the server down with it.</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>Never mount the Docker socket
  </p>
  <div class="prose-kitchen text-sm">Plenty of examples online mount <code>/var/run/docker.sock</code> into code-server so you can start containers
from the editor. That socket is equivalent to root on the <strong>host</strong> – it undoes every bit of
hardening in this setup. If you want to build containers, do it over SSH or in a separate
environment made for it.</div>
</div>
<h3 id="step-4-start-it-and-check-both-locks">Step 4: Start it and check both locks</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>Lines five and six confirm the hash was read. <code>Not serving HTTPS</code> is correct – Traefik does that.</p>
<p>Now the counter-test from another machine. Without credentials you do not even reach the login
form:</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://YOUR_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>With BasicAuth, code-server redirects you to its own login:</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 YOUR_USER:YOUR_BASICAUTH_PASSWORD https://YOUR_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>That is exactly right: two hurdles, two different passwords. In the browser you see code-server&rsquo;s
login after the BasicAuth dialog:</p>
<p><figure class="my-6"><img src="/en/tutorials/code-server-vscode-in-browser/cs-login_hu_239d03f0c3d51e61.webp" srcset="/en/tutorials/code-server-vscode-in-browser/cs-login_hu_9356e38f62abc23a.webp 480w, /en/tutorials/code-server-vscode-in-browser/cs-login_hu_239d03f0c3d51e61.webp 768w, /en/tutorials/code-server-vscode-in-browser/cs-login_hu_8c2ebdf4354fab91.webp 1200w, /en/tutorials/code-server-vscode-in-browser/cs-login_hu_6a1dc164fa783700.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/code-server-vscode-in-browser/cs-login_hu_7fe395b9bcb0d987.webp"
    alt="The code-server login page titled &ldquo;Welcome to code-server&rdquo; with a single password field." title="The second lock: code-server&#39;s own password prompt"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The second lock: code-server&#39;s own password prompt</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>Warning
  </p>
  <div class="prose-kitchen text-sm">After logging in, code-server sets a cookie whose value <strong>is the password hash itself</strong> – with no
expiry and without the <code>Secure</code> and <code>HttpOnly</code> protection flags. There is no server-side logout:
whoever holds the cookie stays in until you change the password. So run it over HTTPS
only, and clear browser storage afterwards on machines that are not yours.</div>
</div>
<h3 id="step-5-working--file-terminal-port">Step 5: Working – file, terminal, port</h3>
<p>After logging in you land directly in the working directory we passed via <code>command:</code>. Open a file
with <code>Ctrl</code>+<code>P</code>:</p>
<p><figure class="my-6"><img src="/en/tutorials/code-server-vscode-in-browser/cs-editor_hu_792fdf40020eff5c.webp" srcset="/en/tutorials/code-server-vscode-in-browser/cs-editor_hu_39daac332fd6036d.webp 480w, /en/tutorials/code-server-vscode-in-browser/cs-editor_hu_792fdf40020eff5c.webp 768w, /en/tutorials/code-server-vscode-in-browser/cs-editor_hu_ab0455e605043e23.webp 1200w, /en/tutorials/code-server-vscode-in-browser/cs-editor_hu_15b34492d4a7550a.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/code-server-vscode-in-browser/cs-editor_hu_25518830880e2d03.webp"
    alt="The code-server editor with a Python file open, the file tree on the left and the detected Python version in the status bar." title="The familiar interface – bottom right, the Python extension picks up the interpreter from our image"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The familiar interface – bottom right, the Python extension picks up the interpreter from our image</figcaption></figure></p>
<p>The real test is the terminal – via the command palette (<code>Ctrl</code>+<code>Shift</code>+<code>P</code>) and
&ldquo;Terminal: Create New Terminal&rdquo;. There you start your program and call it right away:</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="/en/tutorials/code-server-vscode-in-browser/cs-terminal_hu_1990b5b3f0847bd3.webp" srcset="/en/tutorials/code-server-vscode-in-browser/cs-terminal_hu_b35c4d3298ce57a8.webp 480w, /en/tutorials/code-server-vscode-in-browser/cs-terminal_hu_1990b5b3f0847bd3.webp 768w, /en/tutorials/code-server-vscode-in-browser/cs-terminal_hu_bad4459e85f40a9d.webp 1200w, /en/tutorials/code-server-vscode-in-browser/cs-terminal_hu_5ee305888979191e.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/code-server-vscode-in-browser/cs-terminal_hu_5b2260c89da6a553.webp"
    alt="The integrated terminal in code-server: a running Python server, a curl call and the formatted JSON response." title="Edit, run, fetch – all in one browser window"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Edit, run, fetch – all in one browser window</figcaption></figure></p>
<p>VS Code detects the opened port automatically and offers it under the <strong>PORTS</strong> tab. You cannot
reach those forwarded ports from outside through a reverse proxy though – they are meant for working
inside the editor, not for publishing.</p>
<p>A <code>sudo</code> in the terminal is refused now, by the way. That is intentional: the tools you need are
already in the image, and anything beyond that belongs in the Dockerfile – not in an ad-hoc
installation that disappears at the next <code>docker compose pull</code>.</p>
<h3 id="step-6-extensions-from-open-vsx">Step 6: Extensions from Open VSX</h3>
<p>code-server does <strong>not</strong> use Microsoft&rsquo;s marketplace but <strong>Open VSX</strong>. That is not a setting but a
licensing matter: the official marketplace may only be used by Microsoft&rsquo;s own products.</p>
<p><figure class="my-6"><img src="/en/tutorials/code-server-vscode-in-browser/cs-extensions_hu_23a589e7a2db8bbe.webp" srcset="/en/tutorials/code-server-vscode-in-browser/cs-extensions_hu_be7177863c312b15.webp 480w, /en/tutorials/code-server-vscode-in-browser/cs-extensions_hu_23a589e7a2db8bbe.webp 768w, /en/tutorials/code-server-vscode-in-browser/cs-extensions_hu_fc26ff208247aa51.webp 1200w, /en/tutorials/code-server-vscode-in-browser/cs-extensions_hu_b1ad0cc1bdf056d4.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/code-server-vscode-in-browser/cs-extensions_hu_50fd5f0e62505582.webp"
    alt="The extensions view in code-server showing search results for &ldquo;python&rdquo; from the Open VSX marketplace." title="The Open VSX marketplace – most of the big extensions are there, a few are not"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The Open VSX marketplace – most of the big extensions are there, a few are not</figcaption></figure></p>
<p>Day to day it is better than its reputation: Python, the debugger, Docker, GitLens and the common
themes are all available. What is missing are the extensions Microsoft licenses exclusively for VS
Code – above all <strong>Pylance</strong>, the C# extension and Remote Development. For Python,
<code>ms-python.python</code> steps in with the open language server.</p>
<p>Extensions can also be installed from the command line, which is worth it for a reproducible setup:</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>They land in the <code>local</code> volume and therefore survive restarts and updates:</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="when-it-doesnt-work">When it doesn&rsquo;t work</h2>
<div class="troubleshoot not-prose">
<p><strong>The login fails with the correct password even though the hash is right.</strong> Then the hash is probably in a <code>.env</code> file or under <code>environment:</code>. Docker Compose interpolates <code>$</code> characters in values from env files too, turning <code>$argon2id$v=19$m=65540,...</code> into <code>=19=65540,t=3,p=4/Ntxaqbs...</code> – and it only warns with <code>The &quot;argon2id&quot; variable is not set</code>. Either double every <code>$</code> to <code>$$</code>, or do it the way we did and put the hash in <code>config.yaml</code>, where Compose never looks.</p>
<p><strong>Traefik answers with 401 even though you typed the right BasicAuth password.</strong> Same cause, different place: every <code>$</code> of the bcrypt hash in the <code>basicauth.users</code> label has to be doubled. Check what actually arrives with <code>docker compose config</code> – a mangled hash there means the doubling is missing.</p>
<p><strong>The terminal says <code>bash: python3: command not found</code>.</strong> The official image contains only the editor, no language runtimes – <code>git</code>, <code>curl</code> and <code>sudo</code> are there, nothing more. Build the derived image from step 2 and restart with <code>docker compose up -d --build</code>. Installing into the running container only helps until the next restart.</p>
<p><strong>code-server cannot save a file and reports <code>Permission denied</code>.</strong> The folders under <code>projekte</code> belong to root instead of UID 1000. That happens as soon as you create a directory as root on the host. <code>chown -R 1000:1000 /opt/code-server/projekte</code> fixes it; the habit worth building is to create new projects from inside the editor rather than on the host.</p>
<p><strong><code>sudo</code> in the terminal aborts with <code>The &quot;no new privileges&quot; flag is set</code>.</strong> That is not a defect but the effect of <code>security_opt: no-new-privileges:true</code>. Do not remove that line – it is the only reason the bundled <code>coder ALL=(ALL) NOPASSWD:ALL</code> in <code>/etc/sudoers.d/nopasswd</code> is useless to anyone. Whatever you need permanently belongs in the Dockerfile.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Your problem is not listed?
  <a href="/en/errors/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Search all error messages →</a>
</p>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p><strong>Updates</strong> go through the image tag, and because we build ourselves it takes two steps:</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:NEW_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 tracks VS Code closely and releases <strong>often</strong> – roughly a new minor version every month.
You do not have to take every one; once a quarter is fine unless a security advisory shows up in
between. Keep in mind that an update also refreshes the base image and with it the Debian packages
from your Dockerfile – which is what you want.</p>
<p><strong>Only one of the three directories genuinely needs backing up:</strong></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/   # your code – irreplaceable
</span></span><span class="line"><span class="cl">/opt/code-server/config/     # config.yaml with the password hash
</span></span><span class="line"><span class="cl">/opt/code-server/Dockerfile  # your toolchain
</span></span><span class="line"><span class="cl">/opt/code-server/compose.yaml</span></span></code></pre></div>
</div>
<p><code>local/</code> with the extensions is deliberately left out: it grows with every extension – with the
Python extension it is around 60 MB – and is restored in two minutes by reinstalling your extension list. Keep that list as a file in the project instead
– <code>code-server --list-extensions &gt; extensions.txt</code> – and back that up. All of it belongs in your
<a href="/en/tutorials/restic-backups/">Restic backup</a>.</p>
<p><strong>Better still: use Git.</strong> The editor ships with it, and your code then exists twice by default. On
your own <a href="/en/tutorials/forgejo-git-server/">Forgejo instance</a> it stays in house while doing so.</p>
<p><strong>Realistic effort:</strong> little, once it is set up – one update per quarter, a glance at memory use
when the editor gets sluggish. The part that needs attention is not maintenance but access control:
rotate both passwords when a device goes missing, and check the Traefik logs now and then for who
has been trying the BasicAuth.</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>One step stricter
  </p>
  <div class="prose-kitchen text-sm">If two passwords are not enough for you, put code-server entirely inside the VPN and let Traefik
only pass requests from the VPN subnet. The editor is then invisible from the internet altogether –
the pattern for that is in
<a href="/en/tutorials/internal-services-vpn-only/">making services reachable only over VPN</a>.</div>
</div>
]]></content:encoded></item></channel></rss>