March 2025·8 min read·
devopsself-hostingdockerhomelab

A personal code server and build pipeline that stays mine

The editor should feel like a place, not a tab. I run VS Code on my own hardware, reachable from any browser. Here is how I set it up, connect securely, and trigger Docker builds straight from the IDE.

bash — code-server terminal
# pull the android build box image
$ docker pull mingc/android-build-box:latest

# mount project and run a build — all from the browser IDE
$ docker run --rm \
    -v `pwd`:/project \
    -v "$HOME/.dockercache/gradle:/root/.gradle" \
    mingc/android-build-box \
    bash -c 'cd /project; ./gradlew assembleRelease'

# output apk sitting in build/outputs/apk/release/
OVERVIEW

Mine. Predictable. Fast.

Cloud IDEs are convenient until they are not. Slow file trees, laggy terminals, storage limits, and the sense that your environment is someone else's problem. I wanted something mine.

It runs on Debian 12 Bookworm. systemd keeps it alive, Nginx handles TLS, and a Tailscale subnet router lets every device on my tailnet reach it without touching the firewall.

Stack
code-server v4Docker EngineNginxTailscaleSubnet RouterDebian 12 Bookwormsystemd
0open ports
any browseraccess from
$0subscription cost
1 weekendto set up
running on
ThinkPad E560
Debian 12 Bookworm · systemd · Tailscale subnet router
CONNECTING

Access that feels local, even when I am not.

Keep code-server private, let Nginx be the edge.

From anywhere with a browser I can reach code-server over HTTPS. The config stays intentionally minimal — bound to localhost, with Nginx handling anything public-facing.

~/.config/code-server/config.yaml
bind-addr: 127.0.0.1:8080
auth: password
password: your-secure-passphrase
cert: false  # nginx handles TLS
bash
# devices on your tailnet hit the subnet router
# then access code-server directly via Tailscale IP
$ open http://100.x.x.x:8080
tip A Raspberry Pi 4 or an old laptop running Linux can still be a solid code-server host for most web work.
ARCHITECTURE

Private access without punching holes in the router.

The subnet router bridges tailnet and LAN.

My server runs a Tailscale subnet router, advertising its local network to my tailnet. Any device I own can reach it like it is on the same LAN — no open ports, no exposed IP, no port forwarding.

Laptop
Phone
Tablet / other
Tailscale encrypted tunnel · no open ports
ThinkPad E560Debian 12 · systemd · 100.x.x.x
host boundary
edge
Tailscale subnet routeradvertises LAN · no firewall rules · WireGuard encrypted
100.x.x.x
proxy
NginxTLS termination · proxies to 127.0.0.1:8080
:443
IDE
code-server v4VS Code in the browser · systemd managed · password auth
127.0.0.1:8080
accessible from terminal inside code-server
Host filesystem
projects · config · dotfiles
+
docker.sock
Docker Engine · full build surface
Build containers
Android SDK · Flutter · any image
BUILDS

Docker turns the browser into a build surface.

Any image, any pipeline, right from the IDE terminal.

Mounting the Docker socket is where this becomes more than a remote editor. The terminal in my browser VS Code has full access to Docker — spin up any build environment without installing a single thing on the host.

01

Build Android APKs from the browser.mingchen/docker-android-build-box ships Android SDK, Flutter, Kotlin, Node, and fastlane. The toolchain lives in the image, so the host stays clean. Mount your project and run.

02

Build Flutter apps with the same image. Switch Java versions with jenv, run flutter build apk, get your artifact. It even works from a browser tab on a phone if needed.

bash — build an APK from the browser IDE
# pull once, reuse forever
$ docker pull mingc/android-build-box:latest

# assemble a release APK — gradle cache mounted for speed
$ docker run --rm \
    -v `pwd`:/project \
    -v "$HOME/.dockercache/gradle:/root/.gradle" \
    mingc/android-build-box \
    bash -c 'cd /project; ./gradlew assembleRelease'

# flutter build in the same container
$ docker run --rm \
    -v `pwd`:/project \
    mingc/android-build-box \
    bash -c '. ~/.bash_profile; cd /project; flutter build apk --release'

OTA updates are this pipeline in production.

The most concrete use of this setup is my Capacitor OTA updater. Both shell scripts — deploy-ota.sh and build-apk.sh — run directly in this terminal and upload to my Hono API on Cloudflare Workers. No CI, no external build service.

built with this pipeline
Capacitor OTA updater
Hono API on Cloudflare Workers · D1 + KV · JWT auth · two shell scripts that run right here in this terminal.
View project →
what else fits here If it can be a Docker image, it can live here — React Native, Rust cross-compilation, Python ML pipelines, Laravel deployments.
heads up Mounting /var/run/docker.sock gives root-equivalent access to the host. Only do this on hardware you fully own, behind strong authentication.
LEARNINGS

The habits that keep it boring, in the best way.

Keep the host stable, keep the workflow portable.

After a few rebuilds and late-night fixes, a few practices made the setup predictable: systemd for reliability, Settings Sync for instant portability, and Tailscale to keep ports closed.

01

Use systemd, not screen. When the server reboots at 3am for a kernel update, you want code-server back automatically. A proper systemd unit handles that without you thinking about it.

02

Settings Sync is a must. Turn on VS Code Settings Sync backed by GitHub. Extensions, keybindings, and themes survive reinstalls and server migrations without any manual work.

03

Tailscale subnet router over port forwarding. Instead of punching holes in my router, Tailscale as a subnet router means every device on my tailnet can reach it like it is on the same LAN — zero exposed ports, end-to-end encrypted, free for personal use.

tip Try Caddy instead of Nginx if you want automatic HTTPS with near-zero config. It handles Let's Encrypt cert renewal on its own.

The setup takes a weekend but the payoff is a dev environment that is identical on every device — laptop, tablet, phone. No syncing, no "works on my machine," no subscription. It turned a homelab project into a calm day-to-day workflow and the foundation for everything else I build, like my OTA updater pipeline.