srmdn.

Back

Using Claude Code on a Self-Managed VPS: My WorkflowBlur image

Most people run AI coding assistants on their laptop, pointed at a local project. That works fine when your app runs locally. But when your project lives on a VPS (a deploy user running services, nginx routing between staging and production, systemd managing processes), the AI has no idea what it’s working with. It suggests Docker. It tries to run go without the full path. It creates files as root and wonders why the service crashes.

I run Claude Code directly on the server. Here’s the setup that makes it actually useful.

Why Not Run It Locally?#

The obvious alternative is to run Claude locally, write code, push to git, pull on the server, and rebuild. That works, and for frontend-heavy projects it’s probably the right call.

But for backend work: API changes, database migrations, systemd service tweaks, nginx config updates. You’re constantly switching context between your laptop and the server. Claude suggests a fix, you paste it, push it, pull it, rebuild, check the logs, paste the error back. It’s friction. When Claude is running on the server itself, it can read the actual log output, check running services, and build the binary right there. The feedback loop is tighter.

There’s also a context problem. Your laptop doesn’t know that Go lives at /usr/local/go/bin/go instead of just go. It doesn’t know that services run as a deploy user, not root. It doesn’t know which ports are in use or how nginx is configured. Without that context, every session starts with Claude making wrong assumptions that you have to correct.

The fix is CLAUDE.md.

CLAUDE.md: The File That Changes Everything#

CLAUDE.md is a file you put in the root of your project. Claude Code reads it automatically at the start of every session, before you type anything. It’s not documentation for humans. It’s instructions for Claude.

Mine looks like this (simplified):

That’s it. Claude now knows the exact binary path, the user model, the port layout, and the deployment pattern. It stops suggesting go build and starts suggesting /usr/local/go/bin/go build. It stops creating files owned by root in directories the service writes to. It knows to test on staging before touching production.

The first session with a good CLAUDE.md feels noticeably different from one without it. You stop spending the first ten minutes correcting wrong assumptions.

Memory Across Sessions#

CLAUDE.md captures stable facts: the stack, the ports, the conventions. But Claude also learns things during a session that aren’t in CLAUDE.md: a bug it fixed and why, a pattern that’s specific to this codebase, a decision you made and the reasoning behind it.

By default, that knowledge is gone when the session ends.

Claude Code has a memory system: a MEMORY.md file it reads at the start of every session and updates as it learns. Out of the box, this file lives in a hidden directory on the server. If the server dies, it’s gone.

My fix: store the memory files in a separate git repository (I use one for VPS infrastructure and shared scripts) and symlink Claude’s memory directory to a folder inside it. The memory is now version-controlled and backed up daily alongside the databases and env files.

# Move memory into your ops repo
mv ~/.claude/projects/-var-www-myproject-staging/memory \
   /var/www/ops-repo/claude-memory

# Symlink back so Claude still finds it
ln -s /var/www/ops-repo/claude-memory \
      ~/.claude/projects/-var-www-myproject-staging/memory
bash

The project name in that path (-var-www-myproject-staging) is just the working directory with slashes replaced by dashes. Claude Code creates it automatically based on where you run it.

When I start a session now, Claude already knows things like: the deploy user issue that causes silent 500 errors when root creates files in directories the service writes to, the SQLite migration pattern we use, which blog posts are published and what they’re about. It picks up where the last session left off.

After any session where something notable was figured out, I commit the memory files:

cd /var/www/ops-repo
git add claude-memory/
git commit -m "chore: update Claude memory"
git push
bash

The Actual Workflow#

A normal development session looks like this:

# SSH into the server
ssh [email protected]

# Start Claude in the project directory
cd /var/www/myproject-staging
claude
bash

Claude reads CLAUDE.md and MEMORY.md. No re-explaining the project.

I describe what I want to build or fix. Claude reads the relevant files, proposes a change, and writes it. Then:

# Rebuild backend (if Go files changed)
systemctl stop myproject-backend-staging
/usr/local/go/bin/go build -o bin/server ./cmd/server/
systemctl start myproject-backend-staging

# Or rebuild frontend (if Astro files changed)
sudo -u deploy npm run build
systemctl restart myproject-astro-staging
bash

I open the staging domain in the browser, check that it works, and either iterate or commit.

The staging to production promotion is explicit and manual, same as without AI:

# Sync staging → production
cd /var/www/myproject-production
git fetch origin
git merge origin/staging
/usr/local/go/bin/go build -o bin/server ./cmd/server/
systemctl restart myproject-backend-production
bash

Claude doesn’t touch production. I do that step myself, deliberately.

The Gotcha That Will Get You#

The most common issue when running Claude on a VPS as root: Claude creates a file, the service crashes, logs say permission denied, and it’s not obvious why.

The cause is the root/deploy user split. Claude runs as root. Your services run as a deploy user. When Claude creates or edits a file, that file is owned by root. The deploy service can’t write to it.

This matters for:

  • Directories the service writes to (database files, uploaded content, cache)
  • Files the service reads at runtime that it might also need to write

The fix is consistent:

chown -R deploy:deploy /path/to/dir
bash

And to prevent it from recurring every time root touches those directories, set a default ACL:

setfacl -d -m u:deploy:rwX /path/to/dir
bash

Now any file created in that directory, by root, by Claude, by anyone, automatically gets deploy write access.

I document this in CLAUDE.md so Claude knows about it. When it creates a file in a service-writable directory, it adds the chown step. Most of the time. When it forgets, the error is quick to diagnose.

What I Tell Claude Before It Writes Any Code#

For a new feature, I don’t say “build me a comment system”. I say:

I want to add a comment system. Before writing any code:
1. Propose the database schema
2. Propose the API endpoints
3. List any questions or assumptions

Do NOT write any code yet.
plaintext

Reviewing a plan before code exists is much faster than reviewing code that made wrong assumptions. Once I’m happy with the plan, I say “looks good, proceed with the database migration first.”

One feature at a time. Review between each one. This produces better code and catches wrong directions early.

Is This Right for You?#

This setup makes sense if:

  • Your project runs on a VPS you control directly
  • Most of your work is backend: API changes, database schema, server configuration
  • You’re working solo or with a small team where one person manages the server
  • You want a tight feedback loop without pushing and pulling for every test

It probably doesn’t make sense if:

  • Your project is frontend-heavy and runs fine locally
  • You have multiple people making server changes simultaneously
  • You’re not comfortable with an AI assistant that has root access to your server

On the root access point: Claude Code asks for confirmation before destructive operations. You see every command before it runs. But it is root access, and the risk is real. The practical risk on a personal project is low. Claude Code is conservative by default. On a production server handling real users, I’d think more carefully before running it there directly.

For my personal site, the workflow is working well. A CLAUDE.md that actually reflects the server setup, memory that persists across sessions, and the discipline to always test on staging first. That combination makes the AI genuinely useful instead of a context-reset every session.

Enjoyed this post?

Get Linux tips, sysadmin war stories, and new posts delivered to your inbox.

No spam. Unsubscribe anytime.

Using Claude Code on a Self-Managed VPS: My Workflow
https://srmdn.com/blog/claude-code-on-a-self-managed-vps
Author srmdn
Published at February 28, 2026