All posts

code-server on a VPS

code-server 4.144.0: 8080 only on 127.0.0.1, argon2 HASHED_PASSWORD, /healthz without a login, one workspace mount. Idle at 539.0 MiB cgroup.

Written by NodeRings Inc.

NodeRings Inc. writes these notes. The company runs the marketplace and the agent that stays on the provider’s side of the hypervisor.

code-server 4.144.0 held 539.0 MiB idle on this lab host. That is cgroup memory.current, the median of twenty samples taken 30 seconds apart after login with one workspace file open. docker stats printed 536.1 MiB. PSS summed across the container PIDs was 465.0 MiB. Port 8080 listened on 127.0.0.1 only. /healthz returned JSON with no cookie. A curl to the public address on 8080 was refused.

code-server runs VS Code in the browser. Same editor UI, remote files on the VPS, integrated terminal, extensions from Open VSX. It is the path when you want a full IDE on a machine you SSH to anyway, without installing a desktop environment. The setup guide is blunt: never expose it to the internet without authentication and encryption, because a logged-in session is a shell on the host.

The install docs show Docker with a loopback publish. Other guides paste Compose and a plaintext password. Pin 4.144.0 by digest and set HASHED_PASSWORD. Mount one project folder. Close 8080 on the public address before DNS points at the box.

ReadingValue
cgroup memory.current539.0 MiB, median of 20
docker stats536.1 MiB
smem PSS (container PIDs)465.0 MiB
CPU percent2.51, median
Host listener127.0.0.1:8080
/healthz (no cookie)"status":"alive" after a session
Imageabout 375 MB
Skip thisWhat this run did
codercom/code-server:latest without a digestPinned 4.144.0 by digest
-p 8080:8080 on every interfacePublished 127.0.0.1:8080:8080
Plaintext PASSWORD= in ComposeHASHED_PASSWORD (argon2) in a mode 600 .env
Opening 8080 on the panel firewallLeft 8080 closed; Caddy on 80/443
Mounting /var/run/docker.sockOnly ./config and ./workspace
Bar chart. Idle memory.current 539.0 MiB, docker stats 536.1 MiB, smem PSS 465.0 MiB.
Idle after login with hello.txt open. Language servers and builds sit above this. The docs list 1 GB as a floor; plan headroom if you compile in the workspace.

How do you install code-server on Docker?

The image is codercom/code-server:4.144.0, digest sha256:b9e8056bb74bd0c4f90284c7559eddeccd38400cfedc28e64c9d3ca50fef1f83. On this pull, latest pointed at the same digest. Keep the version in the Compose file.

Generate an argon2 hash (apt package argon2):

echo -n 'YOUR_LONG_PASSWORD' | argon2 "$(openssl rand -hex 16)" -e -id -k 65536 -t 3 -p 4
mkdir -p ~/code-server/{config,workspace}
echo 'hello from the VPS workspace' > ~/code-server/workspace/hello.txt
chown -R 1000:1000 ~/code-server
cd ~/code-server

Put the full $argon2id$… string in a mode 600 .env. In Compose, every $ in that value becomes $$, or Compose interpolates the hash away (coder/code-server#5327). RDP.sh’s code-server VPS guide also skips publishing host 8080 when Caddy shares the Compose network and proxies code-server:8080 internally; either that pattern or 127.0.0.1:8080:8080 keeps the editor off the public address:

services:
  code-server:
    image: codercom/code-server:4.144.0@sha256:b9e8056bb74bd0c4f90284c7559eddeccd38400cfedc28e64c9d3ca50fef1f83
    container_name: code-server
    user: "1000:1000"
    environment:
      HASHED_PASSWORD: ${HASHED_PASSWORD}
      DOCKER_USER: coder
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - ./config:/home/coder/.config
      - ./workspace:/home/coder/project
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
docker compose up -d
curl -sS http://127.0.0.1:8080/healthz

Inside the container the process listens on 0.0.0.0:8080 (the image entrypoint). The loopback publish is what keeps the host's public address quiet. On this host the logs said Using password from $HASHED_PASSWORD. First start leaves a random password: line in config.yaml, and the login password stays HASHED_PASSWORD. Log rotation matches the Compose checklist.

Compose snippet for code-server 4.144.0 with 127.0.0.1:8080:8080 and HASHED_PASSWORD cropped.
Loopback publish, argon2 hash cropped, one project mount at /home/coder/project.

What can you do inside code-server?

Edit files under the mounted workspace, open a terminal as the container user, install language extensions, and run builds on the VPS CPU. Git works if you mount credentials carefully or forward an SSH agent. The Ports panel can proxy a local dev server so a React or API process on port 3000 is reachable through code-server’s auth at /proxy/3000/ (or via a subdomain when you set --proxy-domain). That is useful on an iPad or a locked-down laptop where plain SSH port forwards are awkward.

What it is not: a multi-user Coder deployment, a replacement for proper secrets management, or a place to mount the host Docker socket “for convenience.” This compose only mounts config and workspace. Rate limits on password auth are two attempts per minute plus twelve per hour, so a public hostname without HTTPS and a strong hash is still a bad idea.

Why must 8080 stay on loopback?

code-server is an editor with a terminal. Publishing 8080 on every interface puts that shell on the internet.

ss -H -lntup | grep 8080

On this host that printed 127.0.0.1:8080. Curl to the public address failed. Leave 8080 closed in the panel firewall. Open 80 and 443 for Caddy. Docker published ports often bypass UFW inside the guest, so the loopback bind is what kept the public address quiet on this lab. The public-address case is in provider-side firewall.

ss showing LISTEN on 127.0.0.1:8080 only.
Host view of the publish. The process inside may still bind 0.0.0.0.

What does /healthz prove?

curl -sS http://127.0.0.1:8080/healthz

That curl sends no cookie. You get HTTP 200 and JSON. Before any browser session this lab returned "status":"expired". After the editor had a heartbeat it returned "status":"alive". The health check answers in both states when the request has no cookie. The same URL on the public address did not connect.

curl of /healthz returning status alive with no cookie, and public curl refused.
Unauthenticated health on loopback. Public 8080 stays closed.

How do you log in and open the workspace?

Open http://127.0.0.1:8080 on the guest, or tunnel:

ssh -N -L 18080:127.0.0.1:8080 user@YOUR_VPS_IP

The login page on this lab said Password was set from $HASHED_PASSWORD. Type the plaintext password you hashed, then open /home/coder/project (the mounted workspace). This lab file hello.txt showed up in the explorer. Crop the password and the hash from screenshots.

code-server login page stating Password was set from $HASHED_PASSWORD.
Login confirms the hash env var. The random password in config.yaml stays unused.
code-server editor open on /home/coder/project with hello.txt contents visible.
One workspace folder. Restricted Mode prompts until you trust the folder.

How do you fine-tune access and proxies?

The docs prefer an SSH tunnel when you can: ssh -N -L 18080:127.0.0.1:8080 user@vps, optionally with auth: none only on that tunnel path. For a browser on the public hostname, keep password (or hashed) auth and put Caddy in front. WebSockets must pass the proxy or the editor stays blank. If you front with Cloudflare, turn Rocket Loader off for that hostname; it rewrites scripts and breaks CSP. Password auth is rate-limited to about two attempts per minute plus twelve per hour (setup guide); that is not a substitute for Fail2ban or SSO when the hostname is public. RDP.sh suggests Fail2ban on the Caddy log (use DOCKER-USER if you publish a container port, fail2ban.dev), Authentik/Authelia, or Tailscale when the editor does not need a public name. For port-forward previews, set --proxy-domain / PROXY_DOMAIN and a matching wildcard DNS+TLS so 3000.example.com can reach a process on the VPS.

Dev servers inside the workspace:

# path proxy (default)
https://code.example.com/proxy/3000/

# or teach the Ports panel a custom URI
# environment: VSCODE_PROXY_URI=https://{{port}}.apps.example.com

Frameworks that assume they own / (Create React App, some Vue/Angular setups) need /absproxy/ and matching PUBLIC_URL / publicPath settings from the guide. Prefer a subdomain proxy when you control DNS. Do not mount /var/run/docker.sock into this container; if you need containers, use a separate runner host or a Compose stack you exec into from SSH.

Notes on /proxy/3000 path proxy, VSCODE_PROXY_URI, and SSH tunnel to 8080.
Tunnel when you can. Path or subdomain proxy when the browser must reach a local app through code-server.

Where does Caddy go?

Daily use off localhost needs HTTPS so the service worker can register. Terminate TLS in Caddy and proxy to loopback:

code.example.com {
    reverse_proxy 127.0.0.1:8080
}

WebSockets ride that same reverse_proxy. TLS setup is in the Caddy HTTPS post. Leave cert: false in code-server and let Caddy hold the certificate.

Caddyfile site block reverse_proxy to 127.0.0.1:8080.
Public HTTPS on 443. code-server stays on loopback HTTP.

How do you back up the workspace?

workspace/ is your project tree; config/ holds code-server settings. Stop or quiesce editors, then archive both (or use Restic against the directory while you are not mid-write):

tar -C ~/code-server -czf code-server-$(date +%F).tar.gz config workspace

Extensions and UI state live under the config mount. Treat the argon2 password and .env as secrets in the same backup set with mode 600.

How do you work comfortably in the browser IDE?

Trust the workspace folder when prompted so extensions activate. Install language support from Open VSX as needed. Forward Git SSH via agent forwarding when cloning private repos. Keep secrets in env files that are gitignored inside the workspace. Close heavy extensions if the 2 GB guest swaps. For iPad use HTTPS + password auth; SSH tunnels need a real SSH client.

FAQ

Is 1 GB enough? The docs list about 1 GB of RAM. Idle here was about 540 MiB of memory.current with a tiny workspace. Builds and language servers need more. A 2 GB guest is safer for daily editing.

HASHED_PASSWORD or PASSWORD? Prefer the argon2 hash. HASHED_PASSWORD is the login password when plaintext PASSWORD is also set, and when first start has written a random password into config.yaml.

Why not auth none? That path is for an SSH tunnel only. On a public hostname leave password auth on.

Open VSX or the Microsoft marketplace? code-server uses Open VSX by default. Some Microsoft-marketplace extensions are absent.

PWA / service worker? Needs HTTPS on the hostname you use daily. Loopback HTTP is fine for a quick SSH tunnel test.

Does UFW block a published 8080? Often not inside the guest. Bind 127.0.0.1:8080:8080 and leave 8080 closed on the panel firewall.