If you stumbeled over this post, you probably already of know solutions like Gitpod, Github Codespaces or Coder to quickly spin up a development environment on the go. While I was using them I got fascinated about their capabilites and wished that I could use them for everything I do. Unfortunately some of these solutions are tied to a specific platform and or repository or they don´t offer you the performance or control that you would like. That was when I stumbeled over code-server, a web-based build of Code OSS. That was the missing piece I needed to finally build my own codespace-like solution, but that time with full control.
So this post documents how I setup a server for remote coding usage. In case you’re more after a cloud-based solution with ephemeral reproducable machines (that are otherwise setup the same as here), I suggest you checkout tevbox, where I did exactly this with a lot of automation. Otherwise read on.
Well I’m not using a server, but an old desktop I had lying around. Phyiscal hardware is cumbersome to manage but for a static thing like that a code-server it works fairly well I found.
HP EliteDesk 800 G1 SFF
Intel(R) Core(TM) i5-4570 CPU @ 3.20GHz
32GB DDR3 (overkill, but I had it lying around)
120GB SSD for OS, 500GB SSD for Data (not formated/mounted but one day I’m glad I have it)
just some dump 1Gbit/s NIC, directly attached to the modem of our provider
The Operating System
I installed Ubuntu Server 22.04.3 LTS on the main disk using LMV to partition the disk automatically. Ubuntu Server is simple, widely adopted, fairly minimal and thus perfect for that kind of server. The manual install has to be done only once and LVM gives you the flexibility to tweak the disk-partitioning later on in case you want that.
Ubuntu comes with netplan by default, but I don’t like it, especially if systemd already has a preinstalled solution. So I purged netplan and replaced it with systemd-networkd:
Now that the server’s OS is configured, we get to the main part: the coding.
To install code-server run the following:
curl -fsSL https://code-server.dev/install.sh | sh
sudo systemctl enable --now code-server@technat
This installs the code-server as systemd-service and enables it for your current user. The default for code-server is to listen on 127.0.0.1:8080, but I’ll change that to something less common (to avoid conflicts):
sed -ei 's/^bind-addr.*/bind-addr: 127.0.0.1:65000/g' ~/.config/code-server/config.yaml
Don´t forget to restart the service with sudo systemctl restart code-server@technat after that change.
Configuring code on code-server
The Code OSS part of code-server has it’s config in .local/share/code-server/User/settings.json. That’s what you will automatically configured if you change settings on the web in code-server. I usually put some defaults in there:
As you have noticed so far, code-server listens on localhost, but we want to use it from everywhere. That’s a sane default code-server uses here because code-server itself isn’t meant to be exposed directly. Instead you should use a reverse-proxy as the docs suggest.
I got two options explained here: tailscale or caddy
sudo tailscale funnel --bg 65000
And read the docs about Funnel to learn about the prerequisites.
Please note that exposing local development services via Tailscale is only possible path-based this way.
And restart the service: sudo systemctl restart caddy.
What will happen?
Caddy will now automatically request a TLS certificate for the domain you defined and configure it. This will only work if you set DNS records, if not do so now. In my config Caddy listens on the specified IPv6 address which is configured on my host and allowed on the firewall of my router (and in ufw of course). This is because I’m lazy and didn’t wanted to setup DynDNS nor port-forwarding. If you want your code-server to be IPv4 capable you probably want to port-forward from your router to the server and setup a DynDNS record for the public IP of your router. Or you might also have a public static IP assigned to your server already to use. In any way remove the bind directive for Caddy to listen on all interfaces.
Please note: My box still has a private IPv4 address to reach IPv4 only endpoints (like Github which currently doesn’t support IPv6).
Because of accessing web-services exposed via code-server. It’s an easy way to reach a local development services from the internet. With the tailscale variant above that will work too, but on sub-paths which is sometimes not desired.
You could also do this with one wildcard DNS entry instead of defining all the domains seperately, but then you have to configure a DNS-01 challenge for Caddy which requires more effort and credentials (because wildcards can only be obtained using DNS-01 from Let’s Encrypt).
One last thing to do in this matter is to also add the domain in .config/code-server/config.yaml. This is required for code-server to forward the request to your local process properly:
sed -ei 's/^proxy-domain:.*/proxy-domain: technat.dev/g' ~/.config/code-server/config.yaml
And restart the service again. But be careful: A typo and the service won’t come up again.
Let’s quickly mention that: code-server generates a password that you can find in it’s config file. If you change it restart the service. But I don’t like this and replaced it with an OAuth Flow to sign-in with Github.
First thing to do for this is to disable the current authentication:
sed -ei 's/^auth:.*/auth: none/g' ~/.config/code-server/config.yaml
Be sure to replace your username in --github-user. It’s the only directive that’s currently somehow not supported in the config file.
Once it’s running we can create an OAuth app in Github according to this doc. I’ll use https://technat.dev/oauth2/callback as callback URL.
And then create the config file for oauth2-proxy:
cookie_domains=".technat.dev"cookie_secure=truecookie_expire="2h"http_address="127.0.0.1:65001"reverse_proxy=true # Are we running behind a reverse proxy? Will not accept headers like X-Real-Ip unless this is set.provider="github"client_id="REPLACE_ME"client_secret="REPLACE_ME"cookie_secret="$(openssl rand -base64 32 | tr -- '+/' '-_')" # generate new cookie secret with this commandemail_domains=["*"] upstreams=["http://127.0.0.1:65000/" ]
Note: this config allows everyone with a Github account to sign in, somehow the --github-user flag can’t be translated into a config directive, but as shown above this flag is set on the systemd service.
Don’t forget to restart the service afterwards. Once this is done, simply replace all reverse_proxy directives in caddy with 127.0.0.1:65001 (or omit it if you don’t want authentication for an endpoint) and you’re done!
Now that you have your code-server you might want to active extensions and install progamming languages. I’ll skip this topic as it highly depends on what you are using your code-serer for. I will just run: sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply the-technat and I’m done with it.