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:
TZmatters because the integrated terminal inherits it. A wrong timezone is harmless until you’re reading log timestamps at 2am.PASSWORDis the editor login.SUDO_PASSWORDis what the integrated terminal asks for when yousudo apt install. Make them different.DEFAULT_WORKSPACEis the folder code-server opens by default. I point it at/config/workspaceso my code lives inside the same volume I back up.- The
PROXY_DOMAINline 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
8443port 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 to127.0.0.1:8443:8443so 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
nvmor the official NodeSource repo. - The language runtimes I need (PHP, Python, Go, depending on the project).
tmuxfor 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.