Editing code from a Chromebook, a locked-down work laptop, an iPad, or a friend's machine usually means fighting with tooling that isn't yours. Self-hosting your editor fixes that. Your workspace, extensions, terminal, and Git credentials live on a VPS and follow you to any device with a browser.
code-server is the open-source project that runs Visual Studio Code as a web app. It's the same editor you know - extensions, integrated terminal, settings sync, the works - served over HTTPS from a server you control. It's light enough for a small VPS and pairs perfectly with a remote development workflow.
This guide walks through a production setup: Docker, Caddy for automatic HTTPS, a strong password gate, and a workspace that survives restarts.
code.example.com to your serverdocker-compose.ymlTotal time: about 15 minutes.
80 and 443 open to the internet (required by Let's Encrypt)code-server itself idles at around 100-150 MB of RAM. The headroom is for your language servers, compilers, and Node processes.
In your DNS provider, create an A record:
code.example.com → YOUR_VPS_IPV4
If you use IPv6, add an AAAA record too. Wait a minute, then verify:
dig +short code.example.com
The output should match your VPS IP. DNS must resolve before Caddy can issue a certificate.
On a fresh Ubuntu box:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify:
docker --version
docker compose version
If you use UFW:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Caddy needs 80 for the ACME HTTP challenge and 443 for HTTPS.
sudo mkdir -p /opt/code-server
cd /opt/code-server
sudo mkdir -p config project caddy-data caddy-config
Two folders matter for backups:
config/ holds your code-server settings, installed extensions, and UI state.project/ is your actual code. Point your repos here so nothing lives inside the throwaway container layer.You can log in with a plaintext PASSWORD, but a hashed password keeps the secret out of your environment. Generate one with argon2:
sudo apt install -y argon2
echo -n "your-long-random-password" | \
argon2 "$(openssl rand -hex 16)" -e -id -k 65540 -t 3 -p 4
Copy the full $argon2id$... string it prints. You'll paste it into the compose file next. Keep the plaintext password somewhere safe - that's what you type at the login screen.
Create /opt/code-server/docker-compose.yml:
services:
code-server:
image: codercom/code-server:latest
container_name: code-server
restart: unless-stopped
environment:
HASHED_PASSWORD: "$argon2id$v=19$m=65540,t=3,p=4$PASTE_YOUR_HASH_HERE"
DOCKER_USER: coder
volumes:
- ./config:/home/coder/.config
- ./project:/home/coder/project
user: "1000:1000"
networks:
- codenet
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- codenet
networks:
codenet:
A few notes:
HASHED_PASSWORD, including the leading $argon2id$. Wrap it in double quotes.8080 on the host. Caddy reaches it over the internal Docker network, so the editor is never directly exposed.user: "1000:1000" line makes files in project/ owned by your host user, which avoids permission headaches when you git clone or chmod from the terminal.Create /opt/code-server/Caddyfile:
code.example.com {
encode zstd gzip
reverse_proxy code-server:8080
}
Caddy requests a Let's Encrypt certificate for code.example.com on first boot. code-server speaks plain HTTP internally and handles WebSockets on the same port, so this one line is all the proxy config you need.
cd /opt/code-server
sudo docker compose up -d
sudo docker compose logs -f
Watch the Caddy logs until the certificate is issued, then open https://code.example.com. You'll get a password prompt. Type the plaintext password from Step 5 (not the hash), and the familiar VS Code interface loads in your browser.
Open the built-in terminal with Ctrl+` and confirm you're inside the container:
whoami # coder
pwd # /home/coder
ls project
Open the project folder from File > Open Folder so it becomes your default workspace. Clone a repo from the integrated terminal:
cd ~/project
git clone https://github.com/you/your-app.git
Install extensions the usual way from the Extensions panel. code-server pulls from the Open VSX registry rather than Microsoft's Marketplace, so a handful of proprietary extensions (like the official C# or Pylance builds) aren't available. The vast majority - GitLens, ESLint, Prettier, language packs, themes - are all there.
Your settings and extensions persist in the mounted config/ folder, so a container restart or image update never wipes them.
code-server ships with a minimal base image. Add whatever runtimes you need from the terminal. For example, Node via nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
These installs live under /home/coder, which is mapped to your config/ volume, so they survive restarts too. For heavier setups, build your own image FROM codercom/code-server with the toolchains baked in.
A password gate is the minimum. Two strong additions:
code.example.com only resolves and connects over your tailnet. Then even a leaked password is useless to an outsider.The state you care about is under /opt/code-server/config and /opt/code-server/project. Your code should already live in Git, but settings, extensions, and uncommitted work are worth snapshotting.
Create /usr/local/bin/code-server-backup.sh:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/code-server"
DATE="$(date +%F)"
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/code-server-$DATE.tar.gz" \
-C /opt/code-server config project
find "$BACKUP_DIR" -name "code-server-*.tar.gz" -mtime +14 -delete
Make it executable and schedule a daily run:
sudo chmod +x /usr/local/bin/code-server-backup.sh
echo "20 3 * * * root /usr/local/bin/code-server-backup.sh" | \
sudo tee /etc/cron.d/code-server-backup
For off-site safety, push the backup directory to object storage on the same schedule - see our restic to S3 guide.
Caddy cannot issue a certificate. DNS hasn't propagated, or ports 80/443 are blocked. Check with dig +short code.example.com and your provider firewall.
The editor loads but the page keeps reconnecting. That's a broken WebSocket. Make sure you're reaching the site over https:// through Caddy, not hitting port 8080 directly. Any proxy in front of Caddy must forward Upgrade and Connection headers.
Password is rejected. You pasted the hash into the login form. The prompt wants the plaintext password you hashed in Step 5, not the $argon2id$ string.
Permission denied when cloning into project. The container user doesn't own the folder. Run sudo chown -R 1000:1000 /opt/code-server/project on the host and restart the stack.
An extension won't install. It's probably Marketplace-only. Search Open VSX for an equivalent, or sideload the .vsix from the command palette with Install from VSIX.
--link feature or a reverse tunnel to reach the editor without opening any ports.app1.code.example.com, app2.code.example.com.That's it. A full VS Code environment on a VPS means your dev setup is one URL away from any device, and the heavy lifting happens on the server instead of a borrowed laptop.
Need a VPS with the headroom to compile and run your projects? Our Linux plans include fast NVMe storage and IPv6 out of the box. See the options.