Skip to main content
Technical Blueprints

Code-server: Self-Hosted VS Code in Your Browser

How I deploy code-server for a portable VS Code in the browser: the Docker stack, the proxy in front, and the workspace-backup rule that saved a week of work.

Published Updated 9 min read

I run code-server on every server I personally develop on, and on the dev boxes I hand to the engineers who work on Webnestify projects. Code-server is the self-hosted VS Code in the browser project from Coder, and after two years of using it as my primary editor on the road, it has earned the trust I extend to the boxes that run my client work.

The deployment in this post is the actual stack I ship: linuxserver.io’s code-server image, Nginx Proxy Manager in front, a real TLS certificate, and a workspace volume mounted to a path I back up every night. About thirty minutes from a fresh VPS to a working browser-based editor with extensions, terminal, and git working as expected.

If you want VS Code on your phone, on a borrowed laptop, on a Chromebook, or on the desktop at the office while you work from a cafe on the other side of town, this is the path I take.

Why self-hosted code-server beats a roaming laptop

A laptop is a moving target. The battery degrades, the SSD fills up, the OS reboots during a deployment, and one airport security incident later you’ve lost an SSH key you generated for a client three months ago. I’ve watched all of those happen.

Code-server flips the problem. The editor lives on a server I control, with the storage I chose, the CPU I sized for compiles, and the backups I run on the schedule I picked. Every device I touch becomes a thin client. I open a browser, paste the URL, type a password, and I’m back in the same editor with the same open files and the same terminal history. My phone runs it. My iPad runs it. The borrowed Windows laptop in a hotel business centre runs it.

The other thing it gives me is consistency between machines. Local VS Code on macOS and Windows drifts apart over a year. Settings sync helps, but not with extensions that have native dependencies, or shell configurations, or dotfiles I never bothered to check into git. Code-server is one environment. Nothing to drift.

The code-server deployment stack

Here’s what runs on every code-server box I operate. Nothing exotic. Each piece earns its place.

Prerequisites

You need a VPS with at least 2GB of RAM for a single developer doing normal web work. Compiling anything substantial pushes that to 4GB plus a 2GB swap file. I run mine on a Hetzner CX22 with 4GB and have never wanted more.

You need DNS access for the domain you’ll use, ideally with API-driven Let’s Encrypt support. Cloudflare is my default. And you need a properly hardened server before you put any of this on it. If that’s not done, start with the Linux server security fundamentals post and come back. Code-server with sudo access on an unhardened box is exactly the kind of thing attackers love to find.

Docker engine

Install Docker from the official Docker repository. Follow the official Docker installation guide for Ubuntu. Skip the convenience scripts and the third-party tutorials; the official repo is the only source I use on production boxes.

Nginx Proxy Manager in front

Nginx Proxy Manager (NPM) is the GUI in front of every Docker stack I deploy. It handles TLS termination, automatic Let’s Encrypt renewal, and the HTTP-to-HTTPS redirect, which means I never touch an Nginx config file by hand for routine routing. The setup is identical to the one in the Portainer, NPM and Vaultwarden post, so I won’t repeat it here.

For code-server you’ll add a Proxy Host pointing your chosen subdomain (code.yourdomain.com works) to http://code-server:8443, request a certificate, and tick “Force SSL”, “HTTP/2 Support”, and the Websockets Support checkbox. The Websockets toggle is the one people forget, and without it the integrated terminal will refuse to connect.

Code-server itself

I deploy the linuxserver.io image, not the upstream codercom/code-server. The LinuxServer.io image handles PUID and PGID cleanly, ships tagged releases with predictable upgrade behaviour, and has the maintenance cadence I trust on long-running boxes. Both images run the same code-server underneath; the difference is operational hygiene.

version: "2.1"
services:
  code-server:
    image: lscr.io/linuxserver/code-server:latest
    container_name: code-server
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Bratislava
      - PASSWORD=replace-with-a-real-password
      - HASHED_PASSWORD= #optional
      - SUDO_PASSWORD=replace-with-a-different-password
      - SUDO_PASSWORD_HASH= #optional
      # - PROXY_DOMAIN=code.yourdomain.com #optional
      - DEFAULT_WORKSPACE=/config/workspace
    volumes:
      - /srv/docker/code-server/config:/config
    ports:
      - 8443:8443
    restart: unless-stopped
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

A few notes on the values I actually change:

  • TZ matters because the integrated terminal inherits it. A wrong timezone is harmless until you’re reading log timestamps at 2am.
  • PASSWORD is the editor login. SUDO_PASSWORD is what the integrated terminal asks for when you sudo apt install. Make them different.
  • DEFAULT_WORKSPACE is the folder code-server opens by default. I point it at /config/workspace so my code lives inside the same volume I back up.
  • The PROXY_DOMAIN line is commented out because NPM is doing TLS for me. Uncomment it only if code-server is the thing terminating TLS, which I don’t do.
  • The 8443 port is bound to all interfaces by default. If your VPS firewall already blocks 8443 from the public internet, that’s fine. If not, change the line to 127.0.0.1:8443:8443 so only NPM on the same host can reach it.

Bring the stack up:

docker compose -f /srv/docker/code-server/docker-compose.yml up -d

Then add the Proxy Host in NPM, request the certificate, and load the URL. You should land on the code-server password prompt. Type the editor password and you’re in.

Warning: Do not skip the Websockets checkbox in NPM. The editor will load, but the integrated terminal will silently fail to attach, and you’ll spend twenty minutes blaming Docker before you find the actual cause.

Cloudflare Tunnel as an alternative to NPM

For clients without a static office IP, I put code-server behind a Cloudflare Tunnel instead of opening 80 and 443 on the VPS. Same editor, same volume layout, but the tunnel daemon initiates an outbound connection and the public never touches an inbound port. Add Cloudflare Access in front and you get an extra auth layer before the code-server password prompt.

The one trap: Cloudflare’s free tier closes idle WebSocket connections after 100 seconds, so a long build streaming through the terminal can drop. Run unattended builds inside tmux and check the logs separately.

The workspace volume is the only thing that matters

Everything you actually care about lives in the /config volume:

  • VS Code settings and keybindings.
  • Installed extensions and their state.
  • Your ~/projects/ tree (or wherever your DEFAULT_WORKSPACE points).
  • The integrated-terminal shell history.
  • SSH keys you generated inside the editor and probably forgot about.
  • Git credentials cached by the helper.

Mount /config to a real path on the host. Never use an anonymous volume for it. And add that path to your existing backup job. If you don’t have one, build one before you put a week of work into the editor.

I run a nightly restic snapshot of /srv/docker/code-server/config to a Hetzner Storage Box. The job takes about 90 seconds, costs me about 1.20€ a month for the storage, and is the only reason a host migration last spring took 30 minutes instead of three days.

Sizing: what the editor actually wants

A single developer doing web work (Astro, React, Node, the occasional PHP, lots of git) lives comfortably on 2 vCPUs, 4GB of RAM, 2GB of swap, and 40GB of SSD. Extensions and node_modules add up fast.

I’ve benchmarked my own box at idle and during a TypeScript build of a medium project. Idle: roughly 350MB of RAM and 1-2% CPU across two cores. Mid-build: 2.4GB RAM peaking at 3.1GB, one core pinned at 100% for 90 seconds. The box never swaps with 4GB. With 2GB it would.

For a team, run one container per developer with separate /config volumes. The editor is a single-user tool by design; two people opening it from different locations will fight over file locks and terminal sessions.

The integrated terminal is the killer feature

What most “just edit in the browser” tools get wrong is the terminal. Code-server’s integrated terminal is the real shell, on the actual server, with the tools you’d install on any other Linux box. Same terminal I use over SSH, except it lives inside the editor and remembers sessions across browser refreshes.

What I install on every code-server box on day one:

  • git, curl, wget, jq, ripgrep, fzf, htop (most are already there).
  • A current Node via nvm or the official NodeSource repo.
  • The language runtimes I need (PHP, Python, Go, depending on the project).
  • tmux for sessions that need to outlive a browser tab.

Treat the code-server box like any other dev VM and the terminal becomes a native development environment, not a browser-based imitation of one.

What I don’t bother with

A few patterns that show up in code-server tutorials and that I’ve stripped from my own setup:

  • Running code-server as root. PUID 1000 / PGID 1000 maps to a normal user inside the container. There’s no scenario where running the editor as root is the right call.
  • Exposing port 8443 to the public internet. The editor has its own password, but the auth surface is too valuable to leave directly exposed. Always behind NPM or behind a tunnel.
  • One container, many users. One developer per container.
  • Putting code-server on the same VPS as production apps. Compiling a project shouldn’t be one CPU spike away from disturbing a customer-facing site.

If a setup violates one of these, I assume something will go wrong before the year is out, and so far I’ve been right more often than not.

Closing the loop

This stack of code-server, Nginx Proxy Manager, and a backed-up /config volume has been my daily editor for two years across three VPS providers and at least four host migrations. It survived a laptop theft, two airport-security delays, and one client emergency I solved from a hotel-room iPad. The editor gets out of the way, and that’s the whole point.

If you do the deployment, the one rule I’d carry into your week is the backup rule. The volume holds everything you’ll miss. Set up the snapshot tonight, not after the host swap that taught me the lesson.

Watch on YouTube

Video walkthrough

Prefer the screen-recording version of this guide? Watch it on YouTube — opens in a new tab so the player only loads when you ask for it.

Frequently Asked Questions

Want this handled, not just understood?

Reading the playbook is one thing. Running it on production at 2am is another. If you'd rather have me run it for you, the door is open.

Apply for Access