Remote Coding
This guide shows how I code / tinker.
Background
If you stumbeled over this post, you probably already heard 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 more or less the same as here), I suggest you checkout tevbox, where I did exactly this with a lot of automation.
The server
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 a code-server it works fairly well I think.
The specs:
Part | Description |
---|---|
Desktop | HP EliteDesk 800 G1 SFF |
CPU | Intel(R) Core(TM) i5-4570 CPU @ 3.20GHz |
Memory | 32GB DDR3 (overkill, but I had it lying around) |
Disk | 120GB SSD for OS, 500GB SSD for Data (not formated/mounted but one day I’m glad I have it) |
NIC | just some dump 1Gbit/s NIC |
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.
Networking
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:
sudo tee /etc/systemd/network/wired.network &>/dev/null <<EOF
[Match]
Name=enp2s0
[Network]
DHCP=yes
EOF
sudo systemctl enable --now systemd-networkd
sudo systemctl enable --now systemd-resolved
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo apt purge netplan.io -y
This works even with active VPN / SSH connections btw ;).
Tailscale
I always install Tailscale on my systems so that as long as they have egress connectivity I can connect to them somehow. Tailscale also has a feature called Funnel that allows you to expose service in the internet without a public IP or open ports. We will use this later on.
To install Tailscale, there’s a oneliner: curl -fsSL https://tailscale.com/install.sh | sh
.
To bring it up, there’s a oneliner too: sudo tailscale up --ssh
UFW
It’s always a good practice to use a host-firewall. I configured ufw
for that:
sudo ufw enable
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0
Unattended-Upgrades
To be secure all the time and since the server will later be exposed to the internet I’ll activate automatic security patches:
sudo tee /etc/apt/apt.conf.d/50unattended-upgrades &>/dev/null <<EOF
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
Unattended-Upgrade::SyslogEnable "true";
EOF
sudo systemctl enable --now unattended-upgrades
Coding Setup
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 in the web UI of code-server. I usually put some defaults in there:
{
"workbench.colorTheme": "Solarized Light",
"redhat.telemetry.enabled": false,
"workbench.sideBar.location": "right",
"workbench.startupEditor": "none",
"terminal.integrated.defaultProfile.linux": "zsh",
"explorer.confirmDragAndDrop": false
}
Exposing code-server
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.
As mentioned earlier on, that’s where Funnel comes into play. My machine is already connected to Tailscale, so I run:
sudo tailscale funnel --bg 65000
Funnel must be allowed for your machine, but when you run the command tailscale will tell you that. It’s also a good practice to read the docs about Funnel to learn about the prerequisites.
Please note that exposing local development services via Tailscale is only possible path-based. I haven’t yet found a better solution to that.
Authentication
Now that code-server is exposed in the internet, we should add some authentication. By default code-server has a builtin password authentication with a randomly generated password. 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
sudo systemctl restart code-server@technat
Then I install oauth2-proxy to my system:
ARCH=amd64
OS=linux
VERSION=v7.6.0
curl -fsSL -o /tmp/oauth2-proxy.tar.gz https://github.com/oauth2-proxy/oauth2-proxy/releases/download/$VERSION/oauth2-proxy-$VERSION."$OS"-"$ARCH".tar.gz
tar -C /tmp xzf /tmp/oauth2-proxy.tar.gz
sudo install /tmp/oauth2-proxy-$VERSION."$OS"-"$ARCH"/oauth2-proxy /usr/local/bin/oauth2-proxy
cat <<EOF | sudo tee /etc/systemd/system/oauth2-proxy.service
[Unit]
Description=oauth2-proxy daemon service
After=syslog.target network.target
[Service]
User=caddy
Group=caddy
ExecStart=/usr/local/bin/oauth2-proxy --config=/etc/oauth2-proxy.cfg --github-user=the-technat
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now oauth2-proxy
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://my-machine.blabla.ts.net/oauth2/callback
as callback URL.
And then we create the config file for oauth2-proxy:
footer = "-"
cookie_domains = ".ts.net"
cookie_secure = true
cookie_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 command
email_domains = ["*"]
upstreams = ["http://127.0.0.1:65000/" ]
Some notes:
footer
disables the version to be shown on the sign-in pagecookie_secret
run the command to generate your unique cookie secret--github-user
the 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 and mentioned before this flag is set on the systemd service.
Restart the service after you created the config file. Finally we can recreate our funnel to point to auth2-proxy instead of code-server:
sudo tailscale funnel reset
sudo tailscale funnel --bg 65001
Coding tools
Now that you have your code-server you might want to activate 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.