March 2025·8 min read·
devopsself-hostingdockerhomelab

Self-hosted code server & Docker build pipeline

I run VS Code on my own hardware, accessible from any browser. Here's how I set it up, how I connect remotely, and how I 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. Persistent. Fast.

Cloud IDEs are convenient until they're not — slow file trees, laggy terminals, storage limits, and the creeping feeling your dev environment is someone else's problem. I wanted something mine.

Running on Debian 12 Bookworm. Code-server is managed by systemd, Nginx handles TLS, and a Tailscale subnet router means every device on my tailnet can 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

Secure access from any browser.

Keep code-server local, let Nginx face the world.

From anywhere with a browser I can hit my code-server over HTTPS. The config is intentionally minimal — bound to localhost, Nginx handles everything 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 Even a Raspberry Pi 4 or an old laptop running Linux makes a capable code-server host for most web dev work.
ARCHITECTURE

Private access without port forwarding.

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's on the same LAN — no open ports, no exposed IP, no port forwarding.

[ Phone / Laptop ]── tailnet ──▶[ Subnet Router ]
│ 100.x.x.x
[ Nginx :443 ]── proxy ──▶[ code-server :8080 ]
[ Host FS ]+[ docker.sock ]
BUILDS

Docker makes the browser a build farm.

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

Mounting the Docker socket is where things get genuinely exciting. The terminal in my browser VS Code has full access to Docker — which means I can 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. No Android Studio, no local SDK installs. Mount your project and run.
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'

# build an AAB bundle for Play Store
$ docker run --rm \
    -v `pwd`:/project \
    mingc/android-build-box \
    bash -c 'cd /project; ./gradlew bundleRelease'
02
Build Flutter apps — same image
The same container ships Flutter. Switch Java versions with jenv, run flutter build apk, get your artifact. All from a browser tab on my phone if I need to.
bash — flutter build inside the container
$ docker run --rm \
    -v `pwd`:/project \
    -v "$HOME/.dockercache/gradle:/root/.gradle" \
    mingc/android-build-box \
    bash -c '. ~/.bash_profile; cd /project; flutter build apk --release'

# output: build/app/outputs/apk/release/app-release.apk

OTA updates — 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 Any build environment that exists as a Docker image works from this setup — 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 reliable.

Keep the host stable and 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 avoid exposed ports.

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, themes — all survive reinstalls and server migrations without any manual work.

03

Tailscale subnet router over port forwarding

Instead of punching holes in my router, I run Tailscale as a subnet router. Every device on my tailnet can reach it like it's 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 whole setup takes a weekend but the payoff is a dev environment that's identical on every device — laptop, tablet, phone. No syncing, no "works on my machine," no subscription. It's turned a homelab project into a genuinely solid day-to-day workflow — and the foundation for everything else I build from here, like my OTA updater pipeline.