Zum Inhalt springen
Serverküche
Suche

Die Suche wird geladen … (nur in der veröffentlichten Seite verfügbar).

Anwendungen Schwierigkeit: Fortgeschritten

code-server: VS Code im Browser auf dem eigenen Server

code-server 4.141.0 hinter Traefik: zwei Schlösser davor, eigenes Image mit Werkzeugkette, persistente Erweiterungen aus Open VSX und harte Ressourcenlimits.

· 11 Min. Lesezeit ·Dauer: ca. 60 Minuten
Inhaltsverzeichnis

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.

Was bauen wir?

Wir betreiben code-server 4.141.0 (basiert auf VS Code 1.141.0) hinter Traefik, erreichbar über HTTPS und geschützt durch zwei unabhängige Schlösser: BasicAuth in Traefik und das eigene Passwort von code-server. Dazu bauen wir ein eigenes Image mit der Werkzeugkette, die du wirklich brauchst – das offizielle Image bringt nämlich keine mit.

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.

Was du hier wirklich aufmachst

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.

Voraussetzungen

  • Traefik als Reverse Proxy mit funktionierendem Let’s-Encrypt-Resolver und einer Domain, die auf den Server zeigt
  • BasicAuth aus dem Traefik-Härtungs-Tutorial – wir benutzen die Middleware hier direkt
  • Mindestens 2 GB freier Arbeitsspeicher. Mit einer offenen Datei und aktiver Python-Erweiterung braucht der Container 500 bis 650 MB, unter Last mehr – das Limit setzen wir auf 2 GB
  • Etwa 6 GB Plattenplatz: Das Basis-Image ist 1,51 GB groß, unser erweitertes 1,99 GB
🍳 Empfehlung Anzeige

VPS 2000 G12.5

8 vCore · 16 GB RAM · 256 GB SSD

ab 26,92 €/Monat

Ein Editor plus Sprachserver plus dein eigenes Projekt – hier lohnt der größere VPS.

Zu netcup →

💶 1 Monat gratis:

Einmal einlösbar, gilt für VPS 2000 G12.5. Im Warenkorb einlösen →

💶 5,95 € Gutschein für netcup-Neukunden: immer gültig · nicht für Domains und VPS Lite

Alle netcup-Gutscheine →

Schritt für Schritt

Schritt 1: Verzeichnisse anlegen und den Passwort-Hash erzeugen

code-server läuft im Container als Benutzer mit der UID 1000. Damit er in die gemounteten Ordner schreiben kann, müssen sie ihm auch gehören:

Terminal
mkdir -p /opt/code-server/{config,local,projekte}
chown -R 1000:1000 /opt/code-server
cd /opt/code-server

Die drei Ordner haben klar getrennte Aufgaben: config enthält die Konfiguration, local die installierten Erweiterungen und den Editor-Zustand, projekte deinen Code. Nur der dritte enthält Dinge, die du nicht neu erzeugen kannst.

Jetzt der Passwort-Hash. code-server versteht Argon2-Hashes, und Debian bringt das passende Werkzeug mit:

Terminal
apt-get install -y argon2
echo -n "DEIN_CODE_SERVER_PASSWORT" | argon2 "$(openssl rand -base64 12)" -e -id -k 65540 -t 3 -p 4
Ausgabe
$argon2id$v=19$m=65540,t=3,p=4$XXXXXXXXXXXXXXXXXXXXXXXX$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

openssl rand liefert das Salz, -id wählt die empfohlene Argon2id-Variante, -k 65540 sind rund 64 MB Speicherkosten pro Prüfung. Das macht Rateversuche teuer.

Hinweis

In der code-server-Dokumentation steht npx argon2-cli. Das Paket ist inzwischen als veraltet markiert und verweist selbst auf das Werkzeug der Distribution – nimm also argon2 aus dem Paketmanager.

Den Hash trägst du in code-servers eigene Konfigurationsdatei ein, nicht in eine Umgebungsvariable:

YAML
# /opt/code-server/config/code-server/config.yaml
bind-addr: 0.0.0.0:8080
auth: password
hashed-password: "$argon2id$v=19$m=65540,t=3,p=4$DEIN$HASH"
cert: false
disable-telemetry: true
disable-update-check: true
disable-workspace-trust: true
Terminal
mkdir -p config/code-server   # Datei dort ablegen
chown -R 1000:1000 config

Warum die Datei und nicht environment:? Weil Docker Compose jedes $ 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". cert: false ist richtig, weil Traefik das TLS übernimmt. disable-workspace-trust erspart dir bei jedem Ordner die Rückfrage „Vertraust du den Autoren?" – auf einem Server, auf dem nur dein eigener Code liegt, ist das nur Klickarbeit.

Schritt 2: Ein eigenes Image, weil das offizielle leer ist

Das ist der Punkt, an dem die meisten Anleitungen aufhören und der Ärger anfängt. Sieh dir an, was im offiziellen Image steckt:

Terminal
docker run --rm --entrypoint sh codercom/code-server:4.141.0 \
  -c 'for c in git curl python3 node gcc make; do printf "%-8s %s\n" $c "$(command -v $c || echo FEHLT)"; done'
Ausgabe
git      /usr/bin/git
curl     /usr/bin/curl
python3  FEHLT
node     FEHLT
gcc      FEHLT
make     FEHLT

Das Image enthält den Editor, sonst nichts. Eine .py-Datei bekommt Syntaxhervorhebung, aber python3 app.py im Terminal antwortet mit bash: python3: command not found. Deshalb bauen wir ein abgeleitetes Image mit genau der Werkzeugkette, die du brauchst:

Dockerfile
# /opt/code-server/Dockerfile
FROM codercom/code-server:4.141.0

USER root
RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      python3 python3-venv python3-pip build-essential jq \
 && rm -rf /var/lib/apt/lists/*
USER coder

Tausch die Paketliste gegen das aus, womit du arbeitest – nodejs npm für JavaScript, golang-go für Go. Das USER coder am Ende ist wichtig: Sonst startet der Editor als root.

Schritt 3: Der Compose-Stack

Zuerst den BasicAuth-Hash für Traefik erzeugen:

Terminal
docker run --rm httpd:2.4-alpine htpasswd -nbB DEIN_BENUTZER DEIN_BASICAUTH_PASSWORT
Ausgabe
DEIN_BENUTZER:$2y$05$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Achtung

In der Compose-Datei musst du jedes $ im Hash verdoppeln ($$). Compose würde $2y sonst als Variable lesen, leer ersetzen und dir einen kaputten Hash übergeben – der Login schlägt dann grundlos fehl.
YAML
# /opt/code-server/compose.yaml
services:
  code-server:
    build: .
    image: serverkueche/code-server:4.141.0
    container_name: code-server
    restart: unless-stopped
    user: "1000:1000"
    environment:
      TZ: Europe/Berlin
    volumes:
      - ./config:/home/coder/.config
      - ./local:/home/coder/.local
      - ./projekte:/home/coder/projekte
    command: ["/home/coder/projekte"]
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
    mem_limit: 2g
    pids_limit: 512
    networks: [proxy]
    labels:
      - traefik.enable=true
      - traefik.http.routers.code.rule=Host(`DEINE_DOMAIN`)
      - traefik.http.routers.code.entrypoints=websecure
      - traefik.http.routers.code.tls.certresolver=le
      - traefik.http.routers.code.middlewares=code-auth@docker
      - traefik.http.middlewares.code-auth.basicauth.users=DEIN_BENUTZER:$$2y$$05$$DEIN$$HASH
      - traefik.http.services.code.loadbalancer.server.port=8080

networks:
  proxy:
    external: true

Vier Zeilen verdienen eine Erklärung, weil sie hier mehr bewirken als üblich:

  • user: "1000:1000" – der Editor läuft nicht als root, und die Dateien in projekte gehören danach dir und nicht root.
  • cap_drop: [ALL] – code-server braucht keine einzige Linux-Capability. Editor, Terminal und Erweiterungen laufen damit vollständig.
  • no-new-privileges:true – die schärfste Zeile. Das offizielle Image enthält /etc/sudoers.d/nopasswd mit coder ALL=(ALL) NOPASSWD:ALL; ohne diesen Schalter ist jeder, der ins Terminal kommt, mit einem sudo root im Container. Mit dem Schalter scheitert sudo sauber: sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
  • mem_limit: 2g / pids_limit: 512 – ein durchgedrehter Sprachserver oder eine Endlosschleife im Terminal soll den Server nicht mitnehmen.

Niemals den Docker-Socket einhängen

Viele Beispiele im Netz mounten /var/run/docker.sock in code-server, damit man aus dem Editor heraus Container starten kann. Der Socket ist gleichbedeutend mit Root auf dem Host – 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.

Schritt 4: Starten und die Schlösser prüfen

Terminal
cd /opt/code-server && docker compose up -d --build
docker compose logs --tail 6
Ausgabe
info  code-server 4.141.0 8b41915d25547e2edd553bab03ea4cbd621f98c5
info  Using user-data-dir /home/coder/.local/share/code-server
info  Using config file /home/coder/.config/code-server/config.yaml
info  HTTP server listening on http://0.0.0.0:8080/
info    - Authentication is enabled
info      - Using hashed-password from /home/coder/.config/code-server/config.yaml
info    - Not serving HTTPS

Die Zeilen fünf und sechs sind die Bestätigung, dass der Hash gelesen wurde. Not serving HTTPS ist korrekt – das macht Traefik.

Jetzt die Gegenprobe von einem anderen Rechner. Ohne Zugangsdaten kommst du nicht einmal bis zum Login-Formular:

Terminal
curl -s -o /dev/null -w "%{http_code}\n" https://DEINE_DOMAIN/
Ausgabe
401

Mit BasicAuth leitet code-server auf seine eigene Anmeldung weiter:

Terminal
curl -s -o /dev/null -w "%{http_code}\n" -u DEIN_BENUTZER:DEIN_BASICAUTH_PASSWORT https://DEINE_DOMAIN/
Ausgabe
302

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:

Die Anmeldeseite von code-server mit dem Titel „Welcome to code-server" und einem einzelnen Passwortfeld.
Das zweite Schloss: code-servers eigene Passwortabfrage

Achtung

Nach dem Login setzt code-server ein Cookie, dessen Wert der Passwort-Hash selbst ist – ohne Ablaufdatum und ohne die Schutzflags Secure und HttpOnly. 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.

Schritt 5: Arbeiten – Datei, Terminal, Port

Nach der Anmeldung landest du direkt im Arbeitsverzeichnis, das wir per command: übergeben haben. Öffne eine Datei mit Strg+P:

Der code-server-Editor mit einer geöffneten Python-Datei, dem Dateibaum links und der erkannten Python-Version in der Statusleiste.
Vertraute Oberfläche – unten rechts erkennt die Python-Erweiterung den Interpreter aus unserem Image

Der eigentliche Test ist das Terminal – über die Befehlspalette (Strg+Shift+P) und „Terminal: Create New Terminal". Dort startest du dein Programm und rufst es gleich ab:

Terminal
cd rezepte-api && python3 app.py & sleep 2; curl -s localhost:8000/rezepte | jq .

Das integrierte Terminal in code-server: ein gestarteter Python-Server, ein curl-Aufruf und die formatierte JSON-Antwort.
Bearbeiten, starten, abrufen – alles in einem Browserfenster

VS Code erkennt den geöffneten Port automatisch und bietet ihn im Reiter PORTS 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.

Ein sudo 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 docker compose pull verschwindet.

Schritt 6: Erweiterungen aus Open VSX

code-server benutzt nicht den Marktplatz von Microsoft, sondern Open VSX. Das ist keine Einstellungssache, sondern eine Lizenzfrage: Der offizielle Marktplatz darf nur von Microsofts eigenen Produkten benutzt werden.

Die Erweiterungsansicht in code-server mit Suchergebnissen für „python" aus dem Open-VSX-Marktplatz.
Der Open-VSX-Marktplatz – die meisten großen Erweiterungen sind da, ein paar fehlen

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 Pylance, die C#-Erweiterung und Remote-Development. Für Python springt ms-python.python mit dem freien Sprachserver ein.

Erweiterungen lassen sich auch von der Kommandozeile installieren, was sich für ein reproduzierbares Setup lohnt:

Terminal
docker exec code-server code-server --install-extension ms-python.python
Ausgabe
Extension 'ms-python.vscode-python-envs' v1.38.0 was successfully installed.
Extension 'ms-python.debugpy' v2026.6.0 was successfully installed.
Extension 'ms-python.python' v2026.4.0 was successfully installed.

Sie landen im local-Volume und überstehen damit Neustart und Update:

Terminal
docker compose restart && sleep 10
docker exec code-server code-server --list-extensions
Ausgabe
ms-python.debugpy
ms-python.python
ms-python.vscode-python-envs

Wenn es nicht funktioniert

Der Login mit dem richtigen Passwort schlägt fehl, obwohl der Hash stimmt. Dann steht der Hash vermutlich in einer .env oder unter environment:. Docker Compose interpoliert $-Zeichen auch in Werten aus Env-Dateien, und aus $argon2id$v=19$m=65540,... wird dabei =19=65540,t=3,p=4/Ntxaqbs... – Compose warnt dabei nur mit The "argon2id" variable is not set. Entweder du verdoppelst jedes $ zu $$, oder du machst es wie hier und legst den Hash in config.yaml ab, wo Compose gar nicht hinschaut.

Traefik antwortet mit 401, obwohl du das richtige BasicAuth-Passwort eingibst. Gleiche Ursache an anderer Stelle: Im basicauth.users-Label muss jedes $ des bcrypt-Hashes verdoppelt werden. Prüf mit docker compose config, was tatsächlich ankommt – steht dort ein verstümmelter Hash, fehlt die Verdopplung.

Im Terminal kommt bash: python3: command not found. Das offizielle Image enthält nur den Editor, keine Sprachlaufzeiten – git, curl und sudo sind da, mehr nicht. Bau das abgeleitete Image aus Schritt 2 und starte mit docker compose up -d --build neu. Nachinstallieren im laufenden Container hilft nur bis zum nächsten Neustart.

code-server kann eine Datei nicht speichern und meldet Permission denied. Die Ordner unter projekte gehören root statt der UID 1000. Das passiert, sobald du auf dem Host als root ein Verzeichnis anlegst. chown -R 1000:1000 /opt/code-server/projekte behebt es; angewöhnen solltest du dir, neue Projekte im Editor selbst anzulegen statt auf dem Host.

sudo im Terminal bricht mit The "no new privileges" flag is set ab. Das ist kein Defekt, sondern die Wirkung von security_opt: no-new-privileges:true. Nimm die Zeile nicht heraus – sie ist der einzige Grund, warum das mitgelieferte coder ALL=(ALL) NOPASSWD:ALL aus /etc/sudoers.d/nopasswd niemandem mehr nützt. Was du dauerhaft brauchst, gehört in den Dockerfile.

Dein Problem ist nicht dabei? Alle Fehlermeldungen durchsuchen →

Wartung & Backups

Updates laufen über den Image-Tag, und weil wir selbst bauen, sind es zwei Schritte:

Terminal
cd /opt/code-server
sed -i 's/code-server:4.141.0/code-server:NEUE_VERSION/g' Dockerfile compose.yaml
docker compose up -d --build
docker exec code-server code-server --version

code-server folgt VS Code eng und veröffentlicht häufig – 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.

Sichern musst du nur einen der drei Ordner wirklich:

Ausgabe
/opt/code-server/projekte/   # dein Code – unersetzlich
/opt/code-server/config/     # config.yaml mit dem Passwort-Hash
/opt/code-server/Dockerfile  # deine Werkzeugkette
/opt/code-server/compose.yaml

local/ 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-server --list-extensions > extensions.txt – und sichere die mit. Alles zusammen gehört in dein Restic-Backup.

Besser noch: Nutz Git. Der Editor bringt Git mit, und dein Code liegt damit ohnehin doppelt. Auf einem eigenen Forgejo bleibt er dabei im Haus.

Realistischer Aufwand: 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.

Noch eine Stufe strenger

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 Dienste nur im VPN erreichbar machen.

Feedback per E-Mail: feedback@serverkueche.de

Das könnte dir auch schmecken