Setting Up Claude Code on a Home Server

Yes, I know. You were expecting me to write about OpenClaw. It's had a big few months, and if you follow this space you've seen it pop up constantly - open-source, self-hostable, no vendor lock-in. I looked at it seriously. I went with Claude Code instead, and the reasoning is worth unpacking before we get into the setup.

Why Not OpenClaw?

OpenClaw's pitch is compelling, especially for a homelab crowd. It's open-source, you can run the whole stack locally, and you're not tied to any one model provider. Point it at Ollama or whatever backend you fancy and off you go. For people who want full control, that's genuinely attractive.

A few things gave me pause though. Agentic coding tasks hit the limits of weaker models fast - when a tool is reading files, writing code, running commands, and course-correcting in a loop, you feel the ceiling on local models quickly. Beyond that, self-hosting an inference layer means managing updates, patching dependencies, and thinking about what's exposed on the network. My Mac Mini is accessed over SSH from outside the house, so I'm already conscious of attack surface; adding more services didn't appeal.

The thing that settled it was simpler: I already have a Claude Pro subscription I'm not using anywhere near fully, and Claude Code is included with it. Avoiding vendor lock-in by paying for a separate setup when I'm already a paying customer doesn't really stack up.

OpenClaw is the right choice if you want zero external dependencies and are prepared to trade some raw capability for full autonomy. It just wasn't the right choice for this.

The Setup

Rather than installing Claude Code directly on the Mac Mini, I'm running it in Docker. This keeps the Node.js dependency contained, makes updates simple, and the tool only has access to whatever I explicitly mount. It also means I can tear down and rebuild without losing any of my actual work.

Here's how the setup evolved, because it didn't start at Docker Compose - it got there for good reasons.

Step 1 - Handling the GitHub Token Safely

The first thing to sort out is credential management. You need a GitHub personal access token to let Claude Code clone repos and push branches. Generate one scoped tightly - for my use case, that's a fine-grained token with read/write access to contents on a single repository, nothing else.

Don't pass the token inline when starting the container. If you type it directly into the shell command it ends up in your bash history, which is not where you want credentials sitting. Instead, put it in a .env file:

mkdir -p ~/claudecode
nano ~/claudecode/.env

Put your token in there:

GITHUB_TOKEN=your_token_here

Then lock it down so only your user can read it:

chmod 600 ~/claudecode/.env

The .env file approach is better for a couple of reasons. You're not typing the token into your shell history every time, and it lives in one place you can update when tokens rotate - without having to touch anything else.

One thing to note: don't bake the token into the Docker image itself. If you COPY a secrets file into an image during the build, it ends up baked into the image layers permanently. If you ever push that image to a registry or share it with someone, the token goes with it. Pass it in at runtime via --env-file and the secret never touches the image.

Step 2 - Persistent Volumes

Running a container with --rm means it tears itself down cleanly when you exit. That's tidy, but it also means anything inside the container - your cloned repo, Claude Code's auth config, anything you've set up - is gone when the container stops.

The fix is Docker volumes. You mount directories from the Mac Mini host into the container, so they survive the container being torn down and rebuilt:

Create those directories on the host before you start:

mkdir -p ~/claudecode/repo ~/claudecode/config

Step 3 - Docker Compose

At this point the docker run command was getting unwieldy - env file flag, two volume mounts, -it for the interactive terminal. Docker Compose is well-suited to exactly this situation, not because it adds features, but because it captures all those flags in a file so you're not reconstructing a long command every time.

Create ~/claudecode/docker-compose.yml:

services:
  claudecode:
    image: claudecode
    volumes:
      - ./repo:/app
      - ./config:/root/.claude
    env_file:
      - .env
    stdin_open: true
    tty: true

One thing worth knowing: on some installations, Docker Compose runs as docker-compose (with a hyphen) rather than docker compose (as a plugin). If you get "compose is not a docker command", use the hyphenated version:

cd ~/claudecode
docker-compose run claudecode

That drops you straight into a Claude Code session. Your repo directory and config are mounted in from the host, your GitHub token is available in the environment, and everything persists when the container stops.

First Run

On the first run you'll need to clone your repo into /app and authenticate Claude Code. Both of those will write through to the host via the volumes, so you only do it once. After that, every subsequent run picks up exactly where you left off.

To clone using the GitHub token from the environment:

git clone https://${GITHUB_TOKEN}@github.com/your-username/your-repo.git /app/your-repo

Wrapping Up

The end result is clean: one directory on the Mac Mini, a Compose file that captures all the configuration, credentials in a locked-down .env file that never touches the image, and persistent volumes so nothing is lost between sessions.

If your priorities sit differently - full local inference, no external API calls - OpenClaw is worth a proper look. But if you're already paying for Claude Pro and want something that works reliably over SSH without a lot of ongoing maintenance, this setup has been solid.

← Back to blog