Turning an Old ThinkPad into a Remote Development Machine

I have a MacBook Pro with an M4 Pro and 24 GB of RAM — objectively a ridiculous machine to complain about. Then my codebase got ambitious.

As the project grew, my development environment got heavy: PyCharm indexing a large codebase, Docker running a stack of containers — Postgres, Redis, workers, dev servers, and everything else a serious app accumulates. The M4 Pro could handle it. But "can handle it" and "is pleasant to use while handling it" are two different things. The laptop ran hot, Docker and PyCharm ate a real chunk of memory, and opening a browser with a few tabs started to feel like an imposition.

Before you say it: more RAM wouldn't have fixed this. The cores were never the bottleneck — memory pressure was, and buying my way to 48 GB would only have raised the ceiling. The actual problem was design. I was asking one machine to be my development workstation, Docker host, database server, background-worker host, and everyday computer, all at the same time. And I didn't want to fix that by micromanaging Docker's resource limits or shutting down half my stack every time I wanted the laptop to behave like a laptop.

I also had an old ThinkPad X1 Carbon collecting dust. So I wondered whether I could move the entire development environment onto the ThinkPad and keep the experience on my MacBook essentially unchanged.

What I wanted was fairly specific:

  • Docker should run on the ThinkPad.
  • My repository should live on the ThinkPad.
  • PostgreSQL, Redis and the rest of my stack should run there.
  • PyCharm should still feel local.
  • Docker Compose should work exactly as it always had.
  • Bind mounts and hot reload should keep working.
  • Development servers should be reachable from my main laptop.
  • And ideally, the whole thing should be reachable when I'm away from home.

The result is a capable little remote development environment, and the architecture ends up looking like this:

Architecture diagram: the main MacBook Pro connects over Tailscale (and the internet) to a ThinkPad X1 Carbon running Ubuntu. PyCharm's UI stays on the laptop while its backend, Docker, Docker Compose, the API, PostgreSQL, Redis and the source code all live on the ThinkPad.

The whole setup: the same development experience, on a different machine. The laptop is just the screen and keyboard — everything heavy runs on the ThinkPad.

The one decision everything else follows from is that the development environment lives entirely on the ThinkPad — which quietly dissolves the problems you run into trying to use Docker remotely.

Why not just use a remote Docker context?

Docker has a built-in way to talk to a remote daemon:

docker context create remote \
  --docker "host=ssh://user@remote-machine"

docker context use remote

That's useful for some workloads, but not for a bind-mounted development stack. Suppose a Compose file contains:

volumes:
  - ../apps/api/src:/app/src

With a remote daemon, that path is resolved on the remote machine — where my local source tree doesn't exist. So now I need a second system to sync the source across:

Diagram showing a local filesystem being synchronized to a remote filesystem, which then feeds a Docker bind mount — an extra moving part between your source and the container.

A remote daemon means your source has to be copied to the remote machine before the bind mount can see it — one more thing that breaks on its own.

You can do that with rsync, Mutagen, NFS or SSHFS, but now the environment has another moving part that can fail independently. The ports don't line up either: if the app serves on localhost:8001, that's actually listening on the remote box, and host-side tools reaching for localhost:5432 end up talking to the wrong localhost. Credentials and host tooling have to be duplicated over there too.

At that point I realized I didn't want a remote Docker daemon.

I wanted a remote development machine.

The simpler model

The ThinkPad is the development machine; my main laptop is just the display and keyboard.

Diagram of the simpler model: only PyCharm's UI runs on the main laptop, which connects over SSH to the ThinkPad. The ThinkPad runs the PyCharm backend, the source code, Docker, PostgreSQL, Redis and everything else.

The laptop keeps only the PyCharm UI. Source and containers sit on the same machine, so there's no synchronization layer to go wrong.

There's no filesystem sync layer, because the source code and the containers are on the same machine.

Hardware

The server is a ThinkPad X1 Carbon (6th gen) running Ubuntu — an Intel Core i7-8650U (quad-core, turbo up to 4.2 GHz), 16 GB of LPDDR3 soldered to the board (so 16 GB is the hard ceiling), and a 512 GB NVMe SSD. Not a powerful workstation, and that's part of the experiment: the goal isn't a compute monster, it's to move the persistent, resource-heavy parts of my environment off my primary laptop.

Ubuntu already had most of what I needed — OpenSSH, Docker Engine and Docker Compose. I didn't even install an IDE on it: JetBrains Gateway downloads and manages the remote backend for you, so the ThinkPad never needs PyCharm installed by hand.

Setup

1. Enable SSH

Ubuntu already had OpenSSH. I confirmed the socket was listening:

systemctl status ssh.socket
Active: active (listening)
Listen: 0.0.0.0:22
        [::]:22

Then I connected from my main laptop, and the X1 was just another machine on my LAN:

ssh naad@192.168.1.75

2. PyCharm Remote Development

This is the key part. PyCharm Professional does remote development over SSH (I connect through JetBrains Gateway), so I pointed a Remote Development connection at the ThinkPad, where the project lives:

/home/naad/project

PyCharm runs its backend on the ThinkPad and the UI on my laptop. Open the PyCharm terminal and hostname returns the ThinkPad's name; docker ps shows the ThinkPad's containers. That's the whole point.

Worth addressing the obvious worry — does typing feel bad over a remote link? Not really, because JetBrains ships an actual backend to the remote machine rather than streaming a screen: the UI stays local and only project state crosses the wire, so editing feels native even over the internet. In practice latency sits under 30 ms on the same network and under 80–100 ms over any other decent connection — low enough that I stop noticing it.

3. Docker stays completely normal

I didn't put Docker into a remote context. The ThinkPad just runs Docker Engine locally:

docker context show
default

Compose works normally too:

docker compose version

So the existing workflow doesn't need to know the screen is somewhere else — make up still means what it always did. The only difference is that it runs on the ThinkPad.

4. Hot reload still works

This was one of the most important requirements. A typical stack has a bind mount like:

volumes:
  - ./src:/app/src

Because the source and Docker are on the same machine, hot reload keeps working with no network filesystem in the loop:

Flow diagram: editing a file in PyCharm hits the PyCharm backend on the ThinkPad, which writes the file on the ThinkPad, which the Docker bind mount picks up, which triggers the development server to hot reload — all on one machine, no network filesystem involved.

Edit → backend on the ThinkPad → file on disk → bind mount → reload, all on one box.

Much nicer than trying to sync a local source tree into a remote Docker host.

5. Accessing development servers

One problem remains: the dev server runs on the ThinkPad (say 192.168.1.75:8001) but I want to open http://localhost:8001 on my laptop. PyCharm's remote environment forwards the relevant ports for me:

Diagram: localhost:8001 on the main laptop is forwarded through an SSH tunnel to port 8001 on the ThinkPad, which reaches the Docker container serving the app.

An SSH tunnel maps the ThinkPad's port onto localhost on my laptop.

So FastAPI /docs, admin panels and frontend dev servers all stay reachable from the browser on my main machine, and the app never has to know it's remote.

6. Making it work outside the LAN

The LAN setup was fine at my desk, but I didn't want the ThinkPad reachable only from home. I could have set up router port forwarding — I'd rather not poke a hole in my router, so I installed Tailscale instead.

On Ubuntu:

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

After authenticating, the ThinkPad picked up a Tailscale address:

100.102.25.26

I installed Tailscale on my laptop, signed into the same tailnet, and connected directly:

ssh naad@100.102.25.26

The point is that SSH, Docker, Postgres and my dev services are never exposed to the public internet — Tailscale gives the machines a private network between them. One caveat worth knowing: Tailscale usually makes a direct connection, but when it can't punch through a NAT it falls back to a relay (DERP), which adds latency. That's the one situation where remote dev can feel sluggish.

7. PyCharm over Tailscale

Once SSH worked over Tailscale, I pointed the Remote Development connection at the ThinkPad's Tailscale address:

Diagram: the main laptop connects over Tailscale to the ThinkPad, which runs the PyCharm backend, Docker and the source — the same regardless of which physical network the laptop is on.

With Tailscale in the middle, the physical network stops mattering.

Now it doesn't matter whether the two machines are on the same physical network:

Side-by-side diagram: at home the laptop reaches the ThinkPad over home Wi-Fi through Tailscale; away from home it reaches the same ThinkPad over any internet connection through Tailscale. Either way the connection and the stack on the ThinkPad are identical.

Home wifi or a café, the connection and the stack behind it are identical.

8. MagicDNS

Remembering 100.102.25.26 isn't pleasant, so I used Tailscale's MagicDNS to address the machine by name. I renamed the ThinkPad to x1-dev, which lets me use it for both SSH and the PyCharm connection:

ssh naad@x1-dev

9. SSH keys

Once the basics worked, I switched from passwords to SSH keys:

ssh-keygen -t ed25519
ssh-copy-id naad@x1-dev

I've left password auth on as a fallback, and I'm comfortable with that because sshd isn't reachable from the public internet — the only networks that can see it are my LAN and my tailnet. If I wanted to lock it down further I'd bind sshd to the tailscale0 interface (or block port 22 with ufw), since right now systemctl status shows it listening on 0.0.0.0:22, i.e. open to every network I plug into.

10. Keeping the ThinkPad awake

A remote dev server is useless if it goes to sleep. The lid config is only half of it — I created /etc/systemd/logind.conf.d/10-dev-server.conf:

[Login]
HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore

To stop it suspending at all, mask the sleep targets, then reload logind so the change takes effect:

sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
sudo systemctl restart systemd-logind

On a desktop install, GNOME's own power settings can still override this, so it's worth checking there too. And since the machine now lives plugged in around the clock: a battery pinned at 100% degrades fast, so I capped charging at 80% using the ThinkPad's charge threshold:

echo 80 | sudo tee /sys/class/power_supply/BAT0/charge_control_end_threshold

The operating rule is simple — keep it on power and on the network. Lid closed, display off, still reachable over Tailscale.

The resulting workflow

Day to day, nothing about how I work changed. From PyCharm I run make up, make down, make migrate, make test; I edit code, hot reload fires, the database is where the app expects it, bind mounts work, FastAPI /docs loads, ports forward to my laptop, and I can reach all of it from outside my house. The physical topology moved; the workflow didn't.

The reason for the whole exercise was resource pressure, so here's the part that actually matters, with the full stack running:

With the full stack upMacBook Pro (before)ThinkPad X1 (now)
Memory in usepinned near 98% of 24 GB~15 GB of 16 GB (≈10 GB with just PyCharm)
CPU under loadnever the bottleneck5–25%

The number that actually stung was memory: the Mac sat pinned near 98% with the stack up, which is exactly the pressure that made everything else feel slow. The X1 runs close to its own 16 GB ceiling, but that's fine — it's a dedicated box, and the point is what's no longer on the Mac. My laptop now carries only the IDE UI and a browser, so the pressure that started all this is simply gone.

Why this beats the setup it replaced

The interesting part isn't that a ThinkPad can run Docker — of course it can. It's that the boundary between "local" and "remote" development almost disappears. My laptop no longer has to run containers, keep databases alive, hold large Docker volumes, store dev datasets, run background jobs, or burn CPU while I'm doing something else. It becomes a thin client. Meanwhile the X1 sits on a desk, plugged in, lid closed — and if I replace my laptop tomorrow, the environment doesn't move. I just install Gateway and Tailscale and connect to the same machine.

There's an honest tradeoff, though: the entire environment now lives on one old SSD. Git covers the repo, but not my Postgres volumes or dev datasets, so a dead disk would take those with it. For now that's a risk I'm fine with — the local data is reproducible enough that I haven't bothered with a backup — but the obvious fix when it matters is a nightly pg_dump cron off the ThinkPad.

Where this could go

The obvious next step is adding machines. Tailscale gives me the network layer — it doesn't pool CPU and RAM for me, but it makes a distributed setup straightforward to build on top of:

Diagram: Tailscale connecting three ThinkPads — X1 for dev and storage, X2 and X3 as workers — all feeding a shared job queue.

Where it could scale next: more machines on the same tailnet, one for dev and storage and the rest as workers pulling from a shared job queue.

For anything parallelizable — data processing, scraping, test execution, ML inference, embeddings, batch jobs, simulations — the extra machines could become workers, and that's where Ray, Celery, Nomad or Kubernetes start to earn their place depending on the workload.

But I'm not starting there. The nice thing about this setup is that every layer is independently useful: I don't need a cluster to benefit from the X1, I don't need Kubernetes to run Docker Compose, and I don't need to expose anything publicly to reach my own machines.

The main lesson

The biggest thing I took from this is that remote development doesn't require making Docker remote — that's the abstraction I got wrong at first. If your environment leans on bind mounts, local databases, hot reload and host-side tooling, the cleaner move is usually:

Move the entire development environment to the remote machine, and move the IDE interface to you.

What you get back is close to local development:

Diagram: you, at PyCharm, connect over SSH and Tailscale down to the X1 Carbon running Docker and your full stack — the whole chain that makes remote development feel local.

You at PyCharm, down through SSH and Tailscale to the X1 running your stack. It feels local because, functionally, it is.

And once the plumbing exists, adding a service is just another Docker Compose project. For the price of a laptop I already owned, I got a small private development environment I can reach from anywhere — not a datacenter, but a lot more useful than a ThinkPad collecting dust.