My Always-On Dev Environment pt. 2 – More Than One Stack

In part 1 I set up a Raspberry Pi 5 as an always-on dev box: code-server running as a systemd service, and a single PHP/MySQL/Vite project running inside Docker Compose. The whole point of keeping the host clean — no PHP, no Node, no MySQL installed directly on the Pi — was that the Pi was never going to stay a single-project machine. Sooner or later I’d want to run something else next to it without the two stacks fighting each other. This post is what happens when “sooner or later” arrives.

Running More Than One Stack on the Same Pi

The PHP/MySQL/Vite project is the one I’ve been using as an example, but the whole point of keeping the host clean is that it’s never just one project. The Pi doesn’t care what language a project is written in — it just needs a Compose file and a free range of ports.

The pattern I follow is simple and boring, which is exactly what you want:

  • One folder per project, each with its own docker-compose.yml.
  • One Docker network per project (Compose creates this automatically from the folder name), so services in different projects can’t accidentally see each other.
  • A port allocation note in my own head — or in a text file — so projects don’t collide on the host ports.

Here’s the rough port map I use:

RangeReserved for
8000–8099Web backends (PHP, FastAPI, Node APIs)
5100–5199Frontend dev servers (Vite, Next)
3300–3399MySQL-family databases
5400–5499Postgres-family databases
9000–9099Misc tooling (pytest watchers, notebooks)

Splitting MySQL and Postgres into separate ranges is a small thing, but it means each family can keep its native default port (3306, 5432) without me having to remember which project remapped what. With that discipline, spinning up a completely different kind of project next to the existing one is almost anticlimactic. Let me show you what that actually looks like with a real Python example.

Example: A Python Library Project (MapPy)

MapPy is a small open-source library I maintain — a lightweight JSON-to-object mapper for Python. It’s pure Python, standard library only, published on PyPI as mappy-json-object-mapper. There’s no web server, no database, no frontend. What it needs from a dev environment is very different from the PHP project:

  • A specific Python version (and ideally the ability to test against multiple).
  • An editable install (pip install -e .) so changes in src/ are picked up by tests instantly.
  • A place to run pytest in a loop while I edit.
  • Zero contamination of the host’s Python — no pip on the Pi itself, no fighting with system packages.

This is a perfect case for a throwaway container. I put this docker-compose.yml in the mappy-json-object-mapper/ folder next to the existing PHP project:

services:
  mappy-dev:
    image: python:3.12-slim
    working_dir: /workspace
    volumes:
      - .:/workspace
    command: >
      sh -c "pip install -e . &&
             pip install pytest pytest-watcher &&
             ptw . --runner 'pytest test/'"
    tty: true
    stdin_open: true

That’s the entire thing. Breaking it down:

  • python:3.12-slim — official Python image, small, no Dockerfile needed.
  • working_dir: /workspace + bind mount .:/workspace — the project folder on the Pi is mounted live into the container. Edits made through code-server land on the same files the container is running against.
  • The command — install MapPy in editable mode, install pytest and pytest-watcher, then sit in a loop re-running tests every time a file changes. (I used to use pytest-watch here, but it’s been unmaintained for years; pytest-watcher is the actively-maintained replacement and keeps the same ptw command name.)
  • tty / stdin_open — so I can also docker compose exec mappy-dev bash and poke around in a real shell when I want.

Starting it from the project folder:

cd ~/repo/mappy-json-object-mapper
docker compose up

Now the Pi is simultaneously running the full PHP/MySQL/Vite stack from one folder and a live Python test-watcher from another. They share nothing, know nothing about each other, and can be stopped independently. If I want to test MapPy against Python 3.11 instead, I change one line (python:3.11-slim) and run docker compose up --build — no pyenv, no virtualenvs, no system impact.

One thing worth calling out: this Compose file re-runs pip install every time the container starts. For a zero-dependency library like MapPy that’s a couple of seconds and I genuinely don’t care — the simplicity of not having a Dockerfile is worth it. If your project has a heavier dependency tree, there are two obvious upgrades: either write a proper Dockerfile that COPYs pyproject.toml and runs pip install at build time (so installs are cached in image layers), or add a named volume mounted at /root/.cache/pip so the pip cache survives between runs. Same pattern, just with the install moved from “every start” to “every rebuild” or “first start only.”

Example: A Python Web Service (Shape, Not Code)

If instead of a library I were running, say, a FastAPI service, the Compose file would look almost identical — just with a port exposed and uvicorn as the command:

services:
  api:
    image: python:3.12-slim
    working_dir: /app
    volumes:
      - .:/app
    ports:
      - "8010:8000"   # host:container — 8010 fits my "web backends" range
    command: >
      sh -c "pip install -r requirements.txt &&
             uvicorn main:app --host 0.0.0.0 --reload"

Two things to notice. First, --host 0.0.0.0 inside the container — the same lesson as the Vite --host flag from part 1. A process bound to 127.0.0.1 inside a container is only reachable from inside that container; to make it visible through the published port it has to listen on all interfaces. Second, the host port 8010 is deliberately not 8001, so it doesn’t collide with the PHP project’s Apache. That’s the port-range discipline paying off.

Handling Multiple Projects in code-server

From the IDE side, there are two ways I switch between projects, and I use both depending on mood:

  • Separate windowsFile → Open Folder on each project. code-server happily holds multiple browser tabs, each pointing at a different workspace, each with its own terminal and port-forwarding state.
  • Multi-root workspace — if two projects are actually related (for example, the PHP backend and a Python script that generates seed data for it), I add them both to a single .code-workspace file so they share one window.

Either way, the Docker stacks are still independent. The IDE layout and the runtime layout don’t have to match — and in my experience they usually shouldn’t, because the way I think about a project (two repos, one combined workspace) is rarely the same as the way it runs (two stacks, two networks, two port ranges).

Other Stack Shapes That Fit Comfortably

The same pattern covers most small-to-medium projects. Rough guide to what fits comfortably on a single Pi 5 alongside an existing stack:

  • Node/TypeScript servicesnode:20-alpine, mount the source, run npm run dev. Same idea as the Vite container.
  • Go servicesgolang:1.22 for dev with air or reflex as a live-reload loop.
  • Python data/ML exploration — a Jupyter container (jupyter/scipy-notebook) on an unused port is great for notebooks. Heavy model training is where the Pi starts to hurt.
  • Static site generators — Hugo, Eleventy, Astro — trivial; just a Node container with the dev command.
  • A personal Postgres — a shared postgres:16 container with multiple databases inside can serve as a “utility DB” for any project that needs one, instead of spinning up a new DB per stack. This is where the 5400–5499 range earns its place in the port map.

What I’d avoid stacking: a second MySQL or Postgres per project when one shared instance would do; anything that wants more than ~1 GB of RAM permanently resident unless you’re on the 8 GB Pi; and anything GPU-dependent, which just isn’t going to work at all — the Pi has a GPU, but it’s not the kind of GPU anyone means when they say “GPU.”

Wrapping Up

The thing I keep coming back to with this setup is how ordinary adding a new project has become. A new folder, a new docker-compose.yml, a glance at my port map, docker compose up — and the Pi is running one more thing, with zero impact on anything that was already running. No PHP version conflicts with Python, no MySQL vs Postgres arguments on the host, no virtualenv-of-virtualenvs. The host stays a dumb, predictable Debian box whose only job is to run Docker and code-server, and every project is a sealed little world next to its neighbours.

That was always the bet behind keeping the host clean in part 1, and now — a few stacks in — it’s paying off exactly the way I hoped it would. One Pi, many stacks, nothing contaminating anything else.

Leave a Reply

Your email address will not be published. Required fields are marked *