Claude Code on a VPS: Deploy and Build from Your Terminal
Deploy your site from Claude Code with one SSH command: a read-only GitHub deploy key, a safe deploy script, and no CI/CD platform.
TL;DR
Set up a read-only GitHub deploy key on your VPS, write a one-file deploy script, and trigger it from Claude Code on your laptop with one SSH command. No CI/CD platform needed.
In This Guide click to collapse
The Architecture: Local CLI to Live Site
The idea is simple: skip the CI/CD platform entirely. Instead of configuring GitHub Actions, Vercel hooks, or Netlify build pipelines, I use Claude Code on my local machine to SSH into the VPS and run a deploy script. One command, and the site goes from repository to production.
| Stage | Where | What Happens |
|---|---|---|
| 1. Develop | Local machine | Edit code, test locally, push to GitHub |
| 2. Trigger | Local terminal | Claude Code runs ssh yourserver "~/deploy.sh" |
| 3. Build | VPS | git pull, npm ci, npm run build |
| 4. Ship | VPS | rsync build output to web root, site is live |
This works with any VPS provider. I run mine on ScalaHosting with a RHEL-family distribution (AlmaLinux or Rocky Linux 9) and SPanel, but the same approach applies to DigitalOcean, Hetzner, Linode, or any server you can SSH into.
One Pipeline, Zero CI/CD Platforms
No YAML configs, no build minutes quota, no webhook debugging. Your VPS does the building and serving. Claude Code just triggers it over SSH.
Prerequisites and VPS Setup
What You Need
| Requirement | Version / Details | Purpose |
|---|---|---|
| VPS with SSH access | Any Linux distribution | Remote server to build and host |
| Node.js | 22 LTS or newer (check your framework’s minimum) | Build toolchain for Astro/Next/etc. |
| Git | 2.x+ | Pull from GitHub on the VPS |
| Claude Code CLI | Latest (native installer) | Local terminal interface |
| GitHub repo | Public or private | Source of truth for your code |
I use ScalaHosting with a RHEL-family distribution (AlmaLinux or Rocky Linux 9) and SPanel. The control panel gives you SSH access, file management, and SSL certificates out of the box. Any provider that gives you root or user-level SSH works.
SSH Configuration with Custom Port and Alias
Most VPS providers let you change the SSH port from the default 22. This cuts down on automated brute-force noise dramatically. Set up an alias in your local SSH config so you never have to remember the port or full hostname.
# ~/.ssh/config
Host yourserver
HostName your-server-ip
User youruser
Port 2222
IdentityFile ~/.ssh/id_ed25519
Now ssh yourserver connects you directly. No flags, no port numbers.
Quick SSH Security Wins
- Custom port: Change from 22 to something like 2222 or 6543. Stops most automated scanners.
- Key-only auth: Set
PasswordAuthentication noin/etc/ssh/sshd_config. Eliminates brute-force attacks entirely. - Disable root login: Set
PermitRootLogin no. Always SSH as a regular user and usesudowhen needed. - SSH alias: Saves keystrokes and prevents port/hostname typos.
Installing Claude Code (Locally, and Optionally on the VPS)
Installation and Authentication
Install Claude Code on your own machine first; that is where it runs in this workflow. Anthropic’s recommended method is the native installer, which needs no Node.js:
# macOS, Linux, WSL
curl -fsSL https://claude.ai/install.sh | bash
# Verify it installed
claude --version
# Start Claude Code and sign in when prompted (browser flow)
claude
On Windows PowerShell the equivalent is irm https://claude.ai/install.ps1 | iex. Check the current Claude Code setup docs if either command has changed. The older npm install -g @anthropic-ai/claude-code route still works as a fallback, but it is no longer the recommended path.
Installing it on the VPS as well is optional. Only do it if you want AI-assisted debugging on the server itself; the same installer works there, and it installs into ~/.local/bin without sudo.
The “command not found” Trap
If claude returns “command not found” after installation, the install directory (~/.local/bin for the native installer) is not in PATH. This is especially common on AlmaLinux, Rocky Linux, CentOS, and other RHEL-based systems. See the fix below.
Environment Essentials
The most common post-install issue is PATH not including the install directory. Fix it by adding the path to your shell profile:
# Native installer location
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# Verify
claude --version
# Only if you used the npm fallback: find its global bin directory
npm config get prefix
Environment Checklist
| Check | Command | Expected Output |
|---|---|---|
| Node.js | node --version |
v22 LTS or newer |
| npm | npm --version |
10+ |
| Git | git --version |
2.x+ |
| Claude CLI | claude --version |
Any version number |
| SSH key | ls ~/.ssh/github_deploy* |
Key files listed |
GitHub SSH Deploy Keys
Why a Deploy Key
Your VPS needs to pull from GitHub, but you should not put your personal SSH key or a full-access token on a remote server. Deploy keys solve this: they grant read-only access to a single repository.
| Method | Scope | Access Level | Revocation |
|---|---|---|---|
| Deploy Key | Single repository | Read-only (default) | Remove from repo settings |
| Personal Access Token | All repos (or selected) | Read/write (configurable) | Revoke in GitHub settings |
| Personal SSH Key | All repos you own | Full read/write | Remove from GitHub account |
The deploy key wins for VPS use. If the server is ever compromised, the attacker gets read access to one repository, not your entire GitHub account.
Setting It Up
Generate a dedicated key on the VPS, then add the public key to your GitHub repository.
# On the VPS: generate a key pair
ssh-keygen -t ed25519 -f ~/.ssh/github_deploy -C "deploy-key-yourserver"
# Display the public key (copy this)
cat ~/.ssh/github_deploy.pub
Add the public key in GitHub under Settings > Deploy Keys > Add deploy key. Leave “Allow write access” unchecked.
Now configure SSH on the VPS to use this key for GitHub:
# ~/.ssh/config (on the VPS)
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/github_deploy
IdentitiesOnly yes
Test the connection:
ssh -T git@github.com
# Should output: Hi your-github-username/your-repo-name! You've successfully authenticated...
Deploy Keys Are Single-Repo Scoped
Unlike personal SSH keys, a deploy key only works with the one repository you add it to. If you need to pull from multiple repos on the same VPS, generate a separate deploy key for each and use different Host aliases in your SSH config.
The Deploy Script
Writing deploy.sh
Create a script on the VPS that handles the full pipeline: pull, install, build, and sync. This is the single file that turns ssh yourserver "~/deploy.sh" into a complete deployment.
#!/bin/bash
set -e
PROJECT_DIR="$HOME/projects/your-repo-name"
PUBLIC_DIR="$HOME/public_html"
echo "=== Deploying site ==="
# Pull latest code
cd "$PROJECT_DIR"
git pull origin main
# Install dependencies (reproducible, uses package-lock.json)
npm ci
# Build the site
npm run build
# Sync to web root
rsync -av --delete \
--exclude='wp-*' \
--exclude='.htaccess' \
--exclude='.well-known' \
--exclude='index.php' \
dist/ "$PUBLIC_DIR/"
echo "=== Deploy complete ==="
Make it executable:
chmod +x ~/deploy.sh
What Each Step Does
| Step | Command | Purpose |
|---|---|---|
| 1 | set -e |
Exit immediately if any command fails (no silent errors) |
| 2 | git pull origin main |
Fetch latest code from GitHub via deploy key |
| 3 | npm ci |
Install exact dependency versions from the lockfile |
| 4 | npm run build |
Generate static output in dist/ |
| 5 | rsync -av --delete |
Sync build output to web root, removing stale files |
The Rsync Strategy
The rsync command does the heavy lifting. The --delete flag removes files from the web root that no longer exist in the build output. But some files must survive every deploy: your .htaccess, WordPress files (if running a hybrid setup), and SSL validation directories.
| Exclusion | Why |
|---|---|
wp-* |
Preserves WordPress core files (hybrid setups) |
.htaccess |
Keeps custom Apache rules and security headers |
.well-known |
Preserves SSL certificate validation files |
index.php |
Keeps WordPress entry point (hybrid setups) |
The –delete Flag Is Destructive
Without the --exclude flags, rsync --delete will wipe everything in the target directory that isn’t in your build output. Double-check your exclusions before running the first deploy. One missing exclude can delete your .htaccess or SSL files.
Always Test with –dry-run First
Before your first real deploy, run rsync -avn --delete --exclude='...' dist/ ~/public_html/ with the -n (dry-run) flag. It shows exactly what would be added, deleted, or changed without touching any files. Verify the output before removing the flag.
Running Deployments from Claude Code
One-Command Deploy
With your SSH alias and deploy script in place, deploying from Claude Code is a single command:
ssh yourserver "~/deploy.sh"
Claude Code can run this directly from your local terminal. You can also ask it conversationally: “deploy the site” or “run the deploy script on the server.” It executes the SSH command and streams the output back to you.
What a Deployment Looks Like
You: deploy the site to production
Claude Code: I'll run the deploy script on your VPS.
$ ssh yourserver "~/deploy.sh"
=== Deploying site ===
Already on 'main'
From github.com:your-github-username/your-repo-name
a1b2c3d..d4e5f6g main -> origin/main
Updating a1b2c3d..d4e5f6g
Fast-forward
src/pages/index.astro | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
npm install: up to date
Building site...
generating static routes
▶ src/pages/index.astro
▶ src/pages/about.astro
▶ src/pages/blog/index.astro
build complete in 4.2s
sending incremental file list
=== Deploy complete ===
Site is live. The deploy pulled 1 changed file, rebuilt, and synced to production.
Claude Code as Your DevOps Partner
Deployment is just one thing you can do remotely. Because Claude Code can run any SSH command, your VPS becomes an extension of your local terminal. Check disk space, tail logs, restart services, inspect processes, all without opening a separate SSH session.
| Task | Command | What It Shows |
|---|---|---|
| Deploy | ssh yourserver "~/deploy.sh" |
Full deploy pipeline output |
| Check disk space | ssh yourserver "df -h" |
Filesystem usage on all mounts |
| View recent logs | ssh yourserver "tail -20 ~/logs/error.log" |
Last 20 lines of error log |
| Check running processes | ssh yourserver "pm2 status" |
PM2-managed process list and status |
| Restart a service | ssh yourserver "pm2 restart app-name" |
Service restart confirmation |
| Memory usage | ssh yourserver "free -h" |
RAM usage breakdown |
Guardrails when an agent can run your deploy
Letting an AI agent run ssh commands is useful only if the blast radius is small. Here that means a read-only deploy key, a deploy script that stops on the first error (set -e), a dry-run step before rsync --delete, and a human who approves the command. The same pattern, an agent that proposes and a person who approves, is how I design larger marketing automations. See AI agents with human-in-the-loop guardrails and the approval-gate pattern in detail.
Your VPS as an Extension of Your Terminal
The mental model shift: stop thinking of your VPS as a remote machine you “log into.” With Claude Code and SSH aliases, it becomes another tool in your local workflow. Ask Claude to check something on the server the same way you’d ask it to read a local file.
Security Hardening
Running deployments over SSH means your security baseline matters. A compromised VPS with a deploy key gives an attacker access to your repository (read-only, but still). Lock things down.
| Path | Permission | Purpose |
|---|---|---|
~/.ssh/github_deploy |
600 | Deploy key private key |
~/.ssh/config |
600 | SSH client configuration |
~/deploy.sh |
700 | Deploy script (owner-execute only) |
~/.claude/ |
700 | Claude CLI config and credentials |
Harden SSH on the VPS itself by editing /etc/ssh/sshd_config:
# /etc/ssh/sshd_config
Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
If you run Apache, add security headers and block sensitive files in .htaccess:
# Block sensitive files
<FilesMatch "(\.env|\.git|deploy\.sh)$">
Require all denied
</FilesMatch>
# Security headers
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Never Commit Credentials
Your .env file, SSH keys, and API tokens must never appear in your Git repository. Add them to .gitignore and verify with git status before every commit.
The Security Minimum
These four items are non-negotiable for any VPS running deployments:
- SSH key-only authentication (no passwords)
- Non-default SSH port
- Read-only deploy key (not a personal SSH key)
- 600/700 permissions on all sensitive files
FAQ
Can I use this setup with any static site generator?
Yes. The deploy script runs npm run build and rsyncs the output directory. This works with Astro, Next.js (static export), Hugo, Eleventy, or any tool that produces a folder of static files. Just change the dist/ path in the rsync command to match your generator’s output directory (out/ for Next.js, public/ for Hugo, etc.).
What if the build fails on the VPS?
The set -e flag at the top of the deploy script stops execution on any error. If npm run build fails, the rsync step never runs, so your live site stays untouched. You’ll see the error output in your terminal. Fix the issue locally, push to GitHub, and re-run the deploy.
Is it safe to have Claude Code installed on a production VPS?
Claude Code on the VPS is optional for this workflow. The core pipeline only needs Git, Node.js, and rsync on the server. Claude Code is installed locally on your development machine and triggers the deploy over SSH. If you do install it on the VPS (for remote AI-assisted debugging), restrict access with chmod 700 ~/.claude, since that directory holds your login credentials.
How do I roll back a bad deployment?
The fastest rollback is reverting the commit on GitHub, then re-running the deploy script. Alternatively, keep the previous build output: modify the deploy script to copy dist/ to a timestamped backup folder before each rsync. If a deploy breaks something, rsync the backup back to the web root.
How does this differ from using the VPS as a full workstation?
This workflow treats the VPS as a deploy target: Claude Code runs on your laptop, the VPS just builds and ships. The opposite pattern is to make the VPS your primary dev machine, with Claude Code running in a persistent tmux session on the server itself. You SSH in from any device (laptop, phone) and pick up exactly where you left off. I cover the full setup, including tmux from source without sudo and dual GitHub identities, in Advanced Claude Code VPS Setup: The Full Workstation Pattern. The two patterns stack: the same VPS can be both your deploy target and your primary workstation.
Can I deploy from my phone?
Not directly with Claude Code, but you can pair this setup with a Telegram bot that runs on the VPS. Send a /deploy command from your phone, and the bot executes ~/deploy.sh locally on the server. I may cover that approach in a future post. For a team version, the same deploy script can sit behind a chat command with an approval step, as described in human-in-the-loop approval gates for AI agents.
Why not just use GitHub Actions or Vercel?
Those are great tools. But for a personal site on a VPS you already pay for, adding a CI/CD platform introduces another moving part with its own configuration, build minutes, and potential failure points. The SSH approach has zero external dependencies: your code is on GitHub, your server builds it, done. No third-party service sitting between your repository and your live site.
Can an AI agent deploy my site safely?
Yes, if you limit what it can break. Give the server a read-only deploy key, keep the deploy script strict (set -e, a dry-run before rsync --delete), and make the agent show you the exact command before it runs. The agent proposes, you approve. For bigger workflows, add an explicit approval step; see human-in-the-loop approval gates for AI agents.
