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:
| Range | Reserved for |
|---|---|
8000–8099 | Web backends (PHP, FastAPI, Node APIs) |
5100–5199 | Frontend dev servers (Vite, Next) |
3300–3399 | MySQL-family databases |
5400–5499 | Postgres-family databases |
9000–9099 | Misc 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 insrc/are picked up by tests instantly. - A place to run
pytestin a loop while I edit. - Zero contamination of the host’s Python — no
pipon 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
pytestandpytest-watcher, then sit in a loop re-running tests every time a file changes. (I used to usepytest-watchhere, but it’s been unmaintained for years; pytest-watcher is the actively-maintained replacement and keeps the sameptwcommand name.) tty/stdin_open— so I can alsodocker compose exec mappy-dev bashand 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 windows —
File → Open Folderon 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-workspacefile 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 services —
node:20-alpine, mount the source, runnpm run dev. Same idea as the Vite container. - Go services —
golang:1.22for dev withairorreflexas 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:16container 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 the5400–5499range 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.