Search Tech Journey

Find topics, journeys and posts

6-month learning plan1 / 130
back to blog
systemsbeginner 45m read

S001 · Dev Environment — Linux/WSL, Terminal, VS Code

The 90 minutes that saves you 90 hours. Real environment, real editor, real terminal — no ‘works on my machine’ for the next six months.

🧰SetupM00 · Setup & Tools· Session 001 of 130 90 min

🎯 Build a dev environment you can rebuild from a bash script in under 15 minutes on any machine.

Why this session exists

Every senior engineer you'll ever meet has a scar tissue collection of "it worked on my laptop" stories. Almost all of them trace back to the same root cause: an environment set up by clicking around instead of by design. The good news is you can skip that scar collection entirely with a boring 90 minutes of setup. That's what this session is.

By the end you will:

You will be able to
  • Explain WSL, virtual environments, and dotfiles to a non-technical friend in under a minute each.
  • Reproduce your entire dev environment on a fresh machine in <15 min using a script you own.
  • Diagnose the top-3 environment bugs (CRLF, cross-filesystem I/O, missing venv) in under 60 seconds.
  • Boot VS Code connected to a real Linux shell, install Python via uv, run a script, and freeze deps.

Prerequisites

None. This is session one of one hundred and thirty — you don't need to know anything except how to type.



(a) Intuition · 5 min

What a dev environment actually is
🌍 Real world

You've just moved into a new professional kitchen. Before you cook anything, you sharpen the knives, arrange the spices so your hand can find cumin without looking, and check that the stove actually turns on. That's 90 boring minutes.

Then for the next six months, you cook 300 meals without a single "where did I put the salt?" moment. The 90 minutes paid for itself before dinner two.

💻 Code world

A dev environment is the same. You install the shell, the editor, the package manager, and the language runtime once, on purpose, with a script you keep in Git. Everything after that is coding.

Skip this step and you'll spend the next six months fighting Windows line endings, wrong Python versions, and "why is my hot-reload 8 seconds?" mysteries instead of learning ML.

The three problems it solves

Every environment tool exists to solve one of these
  • Reproducibility — a fresh machine (or a coworker) can rebuild what you have from a script, not from memory.
  • Isolation — this project's Python 3.12 + pandas 2.2 doesn't collide with next project's Python 3.9 + pandas 1.5.
  • Portability — Linux is the operating system every server runs. Your local machine should speak the same language so a bug on your laptop is a bug in production.

WSL (Windows Subsystem for Linux) exists because problem three used to require dual-booting or spinning up a VM every time. WSL 2 ships a real Linux kernel that runs quietly next to Windows, shares clipboard and files, and lets VS Code on Windows edit files inside Linux transparently. macOS users already have a Unix shell built in — you get the same benefit for free.

A quick history so you know why the world looks like this

  1. 1969
    Unix at Bell Labs
    Ken Thompson & Dennis Ritchie build Unix. The shell (sh) ships two years later.
  2. 1991
    Linux kernel · Linus Torvalds
    A free, open Unix-alike. Servers slowly adopt it; by 2010 it runs the internet.
  3. 2015
    VS Code released
    Microsoft ships a free, cross-platform Electron editor. Kills Atom in two years.
  4. 2019
    WSL 2 goes GA
    Real Linux kernel inside Windows. Microsoft finally admits the world runs on Unix.
  5. 2024
    uv released · Astral
    Rust-based Python installer + venv + resolver. 10× faster than pip. Replaces four legacy tools.

(b) Visual walkthrough · 15 min

The stack on Windows (delete WSL for macOS/Linux)

The install path — one time, ever

1
wsl --install

Run in PowerShell as admin. Installs Ubuntu + WSL 2 kernel; reboots.

2
Set username + password

This is your Linux identity. Independent of your Windows login.

3
apt update && upgrade

sudo apt update && sudo apt upgrade -y — refresh package index.

4
Install VS Code (Win side)

Then add the ‘WSL’ extension from the marketplace.

5
code . inside a project

Status bar should show ‘WSL: Ubuntu’ — you're connected.

Directory layout you'll use for six months

~/ your Linux home (aka /home/<user>) projects/ learning/ the 6-month plan lives here s001/ .venv/ isolated Python never committed hello.py requirements.txt s002/ s003/

Rules to internalise:

✅ Do this

Code in Linux filesystem

  • ~/projects/… (native ext4)
  • One folder per session
  • A venv per folder
  • requirements.txt in Git
  • dotfiles in a separate repo
❌ Not this

Code on Windows drive

  • /mnt/c/Users/… (10-100× slower)
  • One big Documents/Code folder
  • Global pip install for everything
  • ‘I'll remember what I installed’
  • config only in your head

The mental model to hold


Common misconception
✗ What most people think

"My dev environment is set up — Python is installed, VS Code works, I ran pip install and it worked. Environment setup is a one-time chore I can now forget about."

✓ What is actually true

An environment is not a state you reach, it's a specification you keep. The only environment that matters is the one you can reproduce on a machine you have never touched, from a file in version control, without you present.

Why the myth is so sticky

Because on day one it genuinely is one-time. You install once, it works, nothing breaks — for weeks. The cost is deferred, not absent. It arrives the day a teammate clones your repo and gets ModuleNotFoundError, or CI fails on a version you upgraded locally six months ago and forgot, or a notebook that trains fine on your laptop dies in the cluster. Every one of those is the same bug: the environment lived in your machine's history instead of in a file.

Prove it to yourself

The honest test of any environment — run it right now:

# 1. What is actually installed, and where did it come from?
which -a python python3 pip
python -c 'import sys; print(sys.executable, sys.version)'
pip list --not-required   # top-level packages only

# 2. The real test: can a stranger rebuild it?
python -m venv /tmp/fresh && /tmp/fresh/bin/pip install -r requirements.txt
# if that fails, your environment is undocumented, not 'set up'
From first principles
Start with the question

Why do virtual environments exist at all? Language ecosystems could have solved this with a smarter global installer — why did every serious one converge on per-project isolation instead?

  1. 1
    A program imports a library by name, not by version. import requests says nothing about which requests.
    forced by · source code is written once and must keep working as the world moves
  2. 2
    So the runtime must resolve that name at import time by searching an ordered list of directories (sys.path) and taking the first match.
    forced by · name resolution needs a deterministic winner, and "first on the path" is the cheapest rule
  3. 3
    If there is exactly one shared site-packages directory, then there is exactly one version of each library visible to every program on the machine.
    forced by · one directory can only hold one requests/ folder
  4. 4
    But two projects on the same machine legitimately need different versions — project A pinned to an old API, project B needing a new feature. This is not a mistake; it is the normal steady state of any codebase older than a year.
    forced by · libraries make breaking changes on their own schedule, not yours
  5. 5
    Therefore the conflict is unsolvable by any global installer. No dependency resolver can satisfy A and B simultaneously in one namespace — it is arithmetically impossible, not merely hard.
    forced by · the constraint set is genuinely unsatisfiable, so no algorithm helps
⇒ Therefore

Therefore the only fix is to stop sharing the namespace: give each project its own site-packages and its own front-of-path entry. That is a virtualenv — not a sandbox, not a container, just a directory prepended to sys.path.

And note what this predicts: activation must be a per-shell, per-process property (it only edits PATH), so a cron job or systemd unit that doesn't inherit your shell will silently use the system Python. It also predicts the fix — invoke /path/to/venv/bin/python directly and never rely on activation in automation.

Mental modelThe machine is a cache, the repo is the truth

Treat your laptop like a CPU cache in front of your repository. Anything valuable that exists only in the cache — an installed package, an exported variable, a hand-edited config, a one-off pip install — is not saved. It is a cache line that will be evicted, by a new laptop, a new teammate, or a CI runner.

The question for every setup action is not "does it work now?" but "which file recorded this?" If the answer is "none", you did not configure anything; you damaged reproducibility and got lucky.

  • Every install must be written down: requirements.txt / pyproject.toml / environment.yml. An undeclared dependency is a future outage with a delay fuse.
  • Pin versions for anything that ships or trains. Floating versions mean your build depends on the date it ran.
  • Secrets are the one thing that must not be in the repo — so they get an explicit mechanism (env vars, key vault), plus a committed .env.example naming them.
  • The definition of done for setup is a clean-machine rebuild, not a working laptop.
🔔 Fires when you see

Fire this model the moment you see: "works on my machine" · a pip install typed into a terminal and not into a file · a notebook that needs a magic cell to run · onboarding docs written in prose instead of a script · a CI failure that reproduces nowhere locally.

The tradeoff

How much isolation do you buy for a project — plain virtualenv, conda, or a container?

venv + pip
+ you gain near-zero overhead, ships with Python, instant activation, works identically on every OS, trivially understandable (it is a directory on sys.path)
− you pay isolates Python packages only — not the interpreter version, not system libraries like BLAS/CUDA/glibc; native wheels can still fail on a different OS
pick when pure-Python work, libraries, CLI tools, and anything where the team already shares an OS and interpreter version
conda / mamba
+ you gain manages the interpreter and non-Python binaries too (MKL, CUDA toolkit, geospatial C libs), which is exactly where scientific stacks break
− you pay heavier, slower solves, a second package universe that mixes badly with pip, and licence/channel considerations in a corporate setting
pick when numerical or ML work with compiled dependencies you do not want to build yourself — the classic case being a GPU stack
Container (Docker / devcontainer)
+ you gain isolates the whole userspace, so dev and production are the same artifact; the only option that makes "works on my machine" a true statement about production
− you pay build times, image size, a filesystem/networking boundary in your inner loop, GPU passthrough friction, and a second skill the team must maintain
pick when the code will run in production on someone else's infrastructure, or the environment involves system packages that would otherwise be documented in prose
What a senior engineer actually does

Senior engineers pick the lightest isolation that makes the rebuild reproducible, and escalate only when a real failure proves it insufficient. Start with venv; move to conda when a compiled dependency bites; move to containers when the deployment target stops matching your laptop.

The layers compose — a container that internally installs from a lockfile is common and correct. What is not correct is skipping the lockfile because you have a container: the image is then reproducible only until its base tag moves.


(c) Hands-on · 25 min

Run this end-to-end on a fresh Ubuntu / WSL / macOS terminal. Copy the whole thing into a script called setup.sh, chmod +x setup.sh, run it. It creates your first project, installs Python via uv, and prints a smoke test.

#!/usr/bin/env bash# setup.sh the "reproduce my environment" script.# Idempotent: safe to re-run any time. Prints what it's doing.set -euo pipefail WORK_DIR="$HOME/projects/learning/s001"PY_VERSION="3.12" log() { printf "\033[1;36m %s\033[0m\n" "$*"; } log "1/6 Creating workspace at $WORK_DIR"mkdir -p "$WORK_DIR"cd "$WORK_DIR" log "2/6 Installing uv if missing"

What each block does

Anatomy of the script

Line 3 · set -euo pipefail
Halt on error (-e), treat unset vars as errors (-u), fail early in pipelines (-o pipefail). One line, saves hours of silent failures.
safety
Line 15 · curl … | sh
The uv install script. Yes, piping to sh from the internet feels scary — for popular tools (uv, rustup, homebrew) it is the standard install path.
install
Line 22 · uv python install
Downloads a pre-compiled Python interpreter in seconds. No more ‘compile from source for 40 min’.
runtime
Line 26 · uv venv .venv
Creates an isolated virtual environment inside the project folder. Prompt changes to show (.venv).
isolation
Line 33 · cat > hello.py <<'PY'
Heredoc — writes multi-line content to a file without escaping. The single quotes around 'PY' mean no shell expansion inside.
shell
Line 45 · uv pip freeze
Dumps every installed package + exact version to requirements.txt. This is the artifact that makes your setup reproducible.
lock
Try itWatch a virtual environment do its job in 30 seconds

Add this line to the top of hello.py:

import requests
print("requests version:", requests.__version__)

Run python hello.py and watch it fail with ModuleNotFoundError: No module named 'requests'. That's a good failure — it proves the venv is isolated from your system Python (which almost certainly has requests installed globally).

💡 Hint · After the failure, run `uv pip install requests` and re-run. You've just experienced the entire ‘why virtual environments exist’ argument.

Bonus — a first dotfile

Every senior engineer keeps a ~/.bashrc (or ~/.zshrc) they've evolved over years. Yours starts today. Add these three lines:

# ~/.bashrc additions — first version, will grow
alias ll='ls -alF'
alias ..='cd ..'
export PATH="$HOME/.local/bin:$PATH"    # so uv is always found

Then source ~/.bashrc (or open a new terminal). ll now lists files with details; .. jumps up a directory. Session S004 (dotfiles) will grow this into a full config you can rebuild anywhere in 30 seconds.


(d) Production reality · 15 min

Every senior engineer has a horror story that starts with "it worked on my laptop." The three below are the ones you personally will hit in the first month.

War story Microsoft · internal WSL team· 2023top-3 support ticket
🔥 What broke

A Next.js dev on Windows cloned a repo to /mnt/c/work/ and reported hot-reload took 8 seconds per file save. He filed it as a WSL bug.

It wasn't a bug. Every file read had to cross the WSL↔Windows filesystem boundary — a 9P protocol translation on each syscall.

🧯 The fix
Moving the repo to ~/work/ (native ext4 inside Linux) dropped reload to 200 ms. He then complained he could still access ~ from Windows via \\wsl$\Ubuntu\home\…, so nothing was lost.
🎓 Lesson to steal
Cross-filesystem I/O in WSL is 10–100× slower. Rule: code lives in ~/projects, always. If you must edit from Windows Explorer, use the \\wsl$\ path.
War story A fintech startup · 20224-hour outage
🔥 What broke
A Windows dev committed a bash deploy script. In production it failed instantly with bash: /deploy.sh: /bin/bash^M: bad interpreter. That ^M is a carriage return Windows added to every line.
🧯 The fix

Emergency: run dos2unix deploy.sh on the server. Permanent: add to every repo:

# .gitattributes
* text=auto eol=lf
*.sh text eol=lf

And to every Windows machine: git config --global core.autocrlf input.

🎓 Lesson to steal
Line endings (CRLF vs LF) is a solved problem, but only if you configure it on day one of every machine you touch. Never trust a fresh Windows install's default.
War story Every junior dev · every weekuniversal
🔥 What broke
You install pandas, run your script, get ModuleNotFoundError: pandas. You reinstall. Same error. You reboot. Same error. You cry.
🧯 The fix
Your venv wasn't active. You installed pandas into system Python. Two ways to make this impossible: direnv (auto-activates venv when you cd into the folder) or uv run script.py (runs inside the project's venv without needing activation at all).
🎓 Lesson to steal
Never rely on human memory for activation. Adopt direnv or uv run by Session S005 at the latest.

How top teams handle this

Google, Microsoft, and Netflix ship devcontainers — a JSON file in the repo (.devcontainer/devcontainer.json) that describes the exact Ubuntu image, tool versions, and VS Code extensions. New engineer clones the repo, VS Code prompts "Reopen in container?", and 3 minutes later they have the exact environment everyone else has. Zero "works on my machine" tickets. Session S056 (Docker) is the foundation for this.

Where this shows up in the rest of the plan

Your dev environment feeds every session that follows
S002 · Git & GitHub
Uses the same shell, editor, and CRLF config you set up here.
S003 · Command line
Deepens the shell you just installed — pipes, grep, jq.
S004 · Dotfiles
Turns the ~/.bashrc line you added into a full portable config.
S005 · Python — data types
Runs inside the uv venv you just created.
S056 · Docker
Devcontainers = your setup.sh, but in a Dockerfile. Same idea, harder isolation.
S060 · Linux fundamentals
Explains the ext4 filesystem, syscalls, and processes your terminal already uses.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

If you can't teach these three to a friend without notes, redo the session:

  1. What is a dev environment? (one sentence, no jargon)
  2. What is a virtual environment, and why? (one concrete example)
  3. What is one thing you should never do in a dev environment? (and why)

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.