Self-Hosted Development Environment Setup Guide

by Marcus Chen

Your cloud bill doubled again. You're paying $180/month for a managed dev environment that goes to sleep when you're not looking and loses your terminal session every time the tab refreshes. I've been there — it's what pushed me off managed platforms entirely back in 2019.

A proper self-hosted development environment setup isn't just about saving money, though you will save money. It's about owning your workflow. No vendor deciding to deprecate your favorite feature. No "we're migrating to v2" emails that break your config. No latency between you and your tools because some AWS region is having a bad day.

This guide covers the actual setup — hardware choices, base OS, containerization, the editor layer, and remote access. I'll share what I run today and what I'd change if I were starting from scratch.

Why Self-Hosted Over Cloud IDEs in 2024

Gitpod's free tier dropped to 50 hours/month in 2023. GitHub Codespaces bills by the minute and the default 4-core machine runs you about $0.36/hour — that's $130/month if you code 6 hours a day. Coder.com is open-source but their cloud offering isn't cheap either.

A used Lenovo ThinkStation P330 with a Core i7 and 32GB RAM costs around $350 on eBay right now. A Raspberry Pi 5 with 8GB costs $80. Either one, running 24/7, costs you maybe $10-15/month in electricity. The math isn't complicated.

Beyond cost: latency to your own machine on your own network is effectively zero. Your data doesn't leave your building. And you can install literally anything without filing a support ticket.

Choosing Your Hardware

I'll skip the "it depends on your workload" waffle. Here's what I'd actually buy:

For a primary dev box: A used mini PC — Beelink SER5 Pro or a ThinkCentre M90q — with at least 32GB RAM and a 500GB NVMe. These run $200-400 and draw 15-35W at load. Plenty for running Docker, a language server, and a database or two simultaneously.

For a lightweight always-on server: A Raspberry Pi 5 (8GB) or an old laptop with the lid closed. The Pi handles light workloads fine — static sites, small APIs, CI runners. It won't compile a large Rust project quickly, but it's always on and always accessible.

What I actually run: A ThinkCentre M75q Gen 2 (Ryzen 5 Pro 5650GE, 32GB RAM, 1TB NVMe) sitting in my office closet. It's been on for 14 months without a reboot. Total investment: $280 used.

Base OS and Initial Configuration

Ubuntu Server 22.04 LTS is the boring correct answer. Not because it's the best Linux distro — it isn't — but because every tool you'll ever use has Ubuntu install instructions, and LTS means you won't be forced to upgrade mid-project.

If you want something more minimal, Debian 12 Bookworm is excellent. I've moved my personal server to Debian and I prefer it. Less bloat, more predictable.

After a fresh install, do this immediately:

# Update everything
sudo apt update && sudo apt upgrade -y

# Install essentials
sudo apt install -y \
  git curl wget unzip \
  build-essential \
  ufw fail2ban \
  htop tmux

# Configure firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw enable

# Harden SSH — disable password auth
sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

Key point on SSH: copy your public key over with ssh-copy-id before you disable password auth. I've locked myself out of a fresh install more than once.

Containerizing Your Dev Environment with Docker and Dev Containers

Here's where self-hosted development environment setup gets genuinely good. Instead of installing language runtimes directly on the host, containerize each project's environment. Your host OS stays clean. Switching from a Node 18 project to a Node 20 project is just switching containers.

Install Docker Engine (not Docker Desktop — you're on a server):

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in for group to take effect

Then use VS Code's Dev Containers spec (the .devcontainer/devcontainer.json format) even if you're not using VS Code. The spec is open and tools like devcontainer CLI can build and run them headlessly.

A minimal .devcontainer/devcontainer.json for a Go project:

{
  "name": "Go 1.22",
  "image": "mcr.microsoft.com/devcontainers/go:1.22-bookworm",
  "features": {
    "ghcr.io/devcontainers/features/git:1": {}
  },
  "postCreateCommand": "go mod download",
  "remoteUser": "vscode",
  "mounts": [
    "source=${localWorkspaceFolder},target=/workspace,type=bind"
  ]
}

This means any machine that can run Docker and has this repo cloned can spin up an identical environment in under two minutes. That's the actual value: reproducibility.

For projects where I need a database alongside the app, I use Docker Compose:

# docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - .:/workspace
    ports:
      - "8080:8080"
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp_dev
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpassword
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

One docker compose up and you have a full stack. Shut it down, the data persists in the named volume. This has replaced every "it works on my machine" conversation I've had.

The Editor Layer: VS Code Server or Neovim Remote

Two real options for editing code on a remote self-hosted machine:

Option 1: VS Code with Remote-SSH extension. Connect your local VS Code to the remote machine over SSH. The language servers, terminals, and extensions all run on the server. Your local machine just renders the UI. This is the path of least resistance and it works well.

Setup is trivial — add your server to ~/.ssh/config:

Host devbox
  HostName 192.168.1.50
  User marcus
  IdentityFile ~/.ssh/id_ed25519
  ServerAliveInterval 60

Then in VS Code: Remote-SSH → Connect to Host → devbox. Done. All your extensions install on the remote side.

Option 2: code-server or Openvscode-server. Run VS Code entirely in the browser. Useful if you want to code from a tablet or a machine where you can't install VS Code locally.

# Install code-server
curl -fsSL https://code-server.dev/install.sh | sh
sudo systemctl enable --now code-server@$USER

By default it listens on 127.0.0.1:8080 with a password in ~/.config/code-server/config.yaml. Expose it through a reverse proxy with TLS — don't expose it raw to the internet.

Option 3: Neovim over SSH. If you're already a Neovim user, this is zero additional setup. SSH in, open Neovim, done. I use this for quick edits and config changes. For heavy work I use Remote-SSH with VS Code because the language server integration is smoother for the languages I work in (Go, TypeScript).

For a deeper look at terminal multiplexing that pairs well with remote dev, check out our guide on tmux for solo developers.

Remote Access Without Exposing Your Home IP

This is the part people get wrong. You don't want to forward port 22 to the internet from your home router. Your fail2ban will be busy all day blocking brute force attempts.

Two approaches I'd recommend:

Tailscale (free for personal use, up to 100 devices). Install it on your dev server and your laptop. You get a private WireGuard mesh network. SSH to your server using its Tailscale IP from anywhere in the world, no port forwarding required.

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

That's genuinely the whole setup. Your machine gets a stable 100.x.x.x address that works from any network. I've been running this since Tailscale v1.20 and it's been rock solid.

Self-hosted WireGuard if you don't trust a third party (even one with a good privacy stance). More setup, but you own the entire path. I wrote about this in setting up WireGuard for remote dev access.

For the reverse proxy to expose web services (code-server, local apps), Caddy is the right tool:

# Caddyfile
code.yourdomain.com {
  reverse_proxy localhost:8080
  basicauth {
    marcus $2a$14$hashed_password_here
  }
}

Caddy handles TLS automatically via Let's Encrypt. No certbot cron jobs, no nginx SSL config blocks.

Backups: The Part Everyone Skips Until It's Too Late

A self-hosted setup with no backup strategy is just a slower way to lose your work. At minimum:

  1. Dotfiles in git. Your shell config, tmux config, Neovim/VS Code settings — all in a repo. Push it somewhere. I use a private GitHub repo.

  2. Project code in git. This should be obvious, but push your branches. Even work-in-progress branches. Your server dying shouldn't mean losing a week of work.

  3. Database dumps on a schedule. For any persistent data:

# /etc/cron.d/pg-backup
0 2 * * * postgres pg_dumpall | gzip > /backups/pg_$(date +\%Y\%m\%d).sql.gz
  1. Offsite copy. Restic to Backblaze B2 costs pennies for a dev setup. 100GB of backup storage on B2 runs about $0.60/month.
# Install restic, then:
restic -r b2:your-bucket-name:/backups init
restic -r b2:your-bucket-name:/backups backup /home/marcus /backups

I run this nightly via cron. If the house burns down, I lose at most 24 hours of work.

Conclusion: Your Self-Hosted Development Environment Setup Tomorrow

Here's what I'd do if I were starting a self-hosted development environment setup from scratch this week: buy a used mini PC for $250-300, install Ubuntu Server 22.04, set up Docker with the devcontainer CLI, connect via VS Code Remote-SSH over Tailscale, and back up with Restic to Backblaze B2.

Total monthly cost: ~$15 electricity + ~$1 backup storage. Total control: complete.

The managed cloud IDE platforms aren't bad products — they're just products optimized for their revenue, not your workflow. When you own the machine, you optimize for yourself.

Start with the hardware. Everything else is software you can change. Pick a machine, install Ubuntu, and SSH in. The rest follows naturally.

If you want to see how I manage multiple projects and switch contexts quickly on this setup, check out using tmux and direnv together for project isolation.