How to Manage Nginx Boxes With Claude Code

This guide shows you how to solve that with a single conversational interface powered by Claude Code. You describe what you want in plain English: “reload nginx on all EU production servers”, “add this vhost to web-01 through web-05 and test before applying”, “purge cache globally except staging”. And the system figures out the targets, executes the operations in parallel over SSH, and reports exactly what succeeded and what didn’t.

The result is a control plane that sits on one machine, reaches your entire fleet through your existing VPN and bastion infrastructure, and lets any member of your team manage nginx across every server without knowing which box is where or writing a single ad-hoc script.

Prerequisites

  • A control server or dedicated machine running Ubuntu 22.04 or 24.04 LTS that stays connected to your network (an existing VPS works fine)
  • NordLayer VPN is active on the control server with access to your internal server network
  • A bastion host already configured and reachable
  • SSH key-based access to all target nginx servers (or the ability to distribute a new key to them)
  • All target servers running Ubuntu 20.04 or 22.04 with nginx installed and managed by systemd
  • An Anthropic account with API access for Claude Code
  • Node.js 22 LTS on the control server (for Claude Code)
  • Python 3.10+ on the control server (for the Fabric orchestration layer)
  • A server inventory — at minimum a list of IPs or hostnames, their SSH users, and any logical groupings (region, role, environment) you want to be able to target as a group

The Architecture

One central control server (your existing VPS or a new one) runs a Claude Code environment with a server inventory file and an SSH agent that can reach all your nginx boxes through NordLayer VPN + bastion. You talk to Claude in natural language. Claude translates your intent into shell commands, executes them across the target servers via SSH, collects results, and reports back. For global or semi-global operations, it runs parallel SSH sessions. For individual servers, it targets by hostname or IP. Everything is logged.

The stack is: Claude Code + a Python SSH orchestration layer (Fabric) + a JSON server inventory + a CLAUDE.md that defines your fleet.

Part 1 – Control Server Setup

This is the machine you sit at. It can be your existing VPS, a dedicated box, or even your laptop if it stays on NordLayer.

Install dependencies:

sudo apt update && sudo apt install -y python3 python3-pip python3-venv git
pip3 install fabric invoke paramiko --break-system-packages
npm install -g @anthropic-ai/claude-code
claude auth login

Create the project:

mkdir -p /home/agent/nginx-fleet
cd /home/agent/nginx-fleet
python3 -m venv venv
source venv/bin/activate
pip install fabric paramiko

Part 2 – SSH Key Distribution

All 100s of servers need to trust one SSH key from the control server. If you don’t already have a dedicated key for this:

ssh-keygen -t ed25519 -C "nginx-fleet-control" -f ~/.ssh/nginx_fleet_key -N ""

Distribute it to every server. If you can still SSH into them manually:

ssh-copy-id -i ~/.ssh/nginx_fleet_key.pub user@<server-ip>

For bulk distribution across all servers at once, once you have your inventory set up (Part 3), you can run:

# one-time bootstrap — assumes you can still reach them with current credentials
cat ~/.ssh/nginx_fleet_key.pub | ssh -J bastion-user@<bastion-ip> user@<target-ip> \
  "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Create an SSH config so all connections route through the bastion automatically: nano ~/.ssh/config

Host bastion
    HostName <your-bastion-ip>
    User <bastion-user>
    IdentityFile ~/.ssh/nginx_fleet_key

Host 10.* 192.168.*
    ProxyJump bastion
    User <server-user>
    IdentityFile ~/.ssh/nginx_fleet_key
    StrictHostKeyChecking no
    ConnectTimeout 10

Adjust the IP ranges to match your internal NordLayer network ranges.

Part 3 – Server Inventory

This is the single source of truth Claude reads to know your fleet. It is a JSON file you maintain.

Run: nano /home/agent/nginx-fleet/inventory.json

{
  "servers": [
    {
      "id": "web-01",
      "host": "10.10.1.1",
      "user": "ubuntu",
      "tags": ["production", "eu-west"],
      "nginx_config": "/etc/nginx/nginx.conf",
      "vhosts_dir": "/etc/nginx/sites-available",
      "cache_dir": "/var/cache/nginx"
    },
    {
      "id": "web-02",
      "host": "10.10.1.2",
      "user": "ubuntu",
      "tags": ["production", "us-east"],
      "nginx_config": "/etc/nginx/nginx.conf",
      "vhosts_dir": "/etc/nginx/sites-available",
      "cache_dir": "/var/cache/nginx"
    }
  ]
}

Add all your servers. The tags field is how you target subsets – “all production”, “all eu-west”, “all us-east”, etc. The path fields handle the variation across servers.

Part 4 – The Fabric Orchestration Layer

This is the Python layer Claude calls to actually execute operations on remote servers. Create it: nano /home/agent/nginx-fleet/fleet.py

#!/usr/bin/env python3
import json, sys, argparse
from fabric import Connection, ThreadingGroup
from pathlib import Path

INVENTORY = Path(__file__).parent / "inventory.json"
SSH_KEY   = Path.home() / ".ssh/nginx_fleet_key"

def load_servers(tags=None, ids=None):
    data = json.loads(INVENTORY.read_text())["servers"]
    if ids:
        id_list = [i.strip() for i in ids.split(",")]
        return [s for s in data if s["id"] in id_list]
    if tags:
        tag_list = [t.strip() for t in tags.split(",")]
        return [s for s in data if any(t in s.get("tags", []) for t in tag_list)]
    return data

def get_connection(server):
    return Connection(
        host=server["host"],
        user=server["user"],
        connect_kwargs={"key_filename": str(SSH_KEY)},
    )

def run_on_servers(servers, command, sudo=False):
    results = {}
    for s in servers:
        try:
            conn = get_connection(s)
            result = conn.sudo(command) if sudo else conn.run(command, hide=True)
            results[s["id"]] = {"status": "ok", "output": result.stdout.strip()}
        except Exception as e:
            results[s["id"]] = {"status": "error", "output": str(e)}
    return results

def print_results(results):
    for server_id, result in results.items():
        status = "OK" if result["status"] == "ok" else "ERROR"
        print(f"[{status}] {server_id}")
        if result["output"]:
            print(f"       {result['output'][:300]}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="nginx fleet control")
    parser.add_argument("--tags",    help="Filter by tags (comma-separated)")
    parser.add_argument("--ids",     help="Target specific server IDs (comma-separated)")
    parser.add_argument("--cmd",     help="Raw shell command to run")
    parser.add_argument("--sudo",    action="store_true")
    parser.add_argument("--nginx-reload",  action="store_true")
    parser.add_argument("--nginx-restart", action="store_true")
    parser.add_argument("--nginx-test",    action="store_true")
    parser.add_argument("--purge-cache",   action="store_true")
    parser.add_argument("--get-config",    help="Path to remote file to print")
    args = parser.parse_args()

    servers = load_servers(tags=args.tags, ids=args.ids)
    print(f"Targeting {len(servers)} server(s): {[s['id'] for s in servers]}\n")

    if args.nginx_reload:
        print_results(run_on_servers(servers, "nginx -t && systemctl reload nginx", sudo=True))
    elif args.nginx_restart:
        print_results(run_on_servers(servers, "systemctl restart nginx", sudo=True))
    elif args.nginx_test:
        print_results(run_on_servers(servers, "nginx -t", sudo=True))
    elif args.purge_cache:
        results = {}
        for s in servers:
            cache = s.get("cache_dir", "/var/cache/nginx")
            conn = get_connection(s)
            try:
                conn.sudo(f"find {cache} -type f -delete")
                results[s["id"]] = {"status": "ok", "output": "cache purged"}
            except Exception as e:
                results[s["id"]] = {"status": "error", "output": str(e)}
        print_results(results)
    elif args.get_config:
        for s in servers:
            print(f"\n=== {s['id']} ({s['host']}) ===")
            conn = get_connection(s)
            try:
                result = conn.sudo(f"cat {args.get_config}", hide=True)
                print(result.stdout)
            except Exception as e:
                print(f"ERROR: {e}")
    elif args.cmd:
        print_results(run_on_servers(servers, args.cmd, sudo=args.sudo))
    else:
        parser.print_help()

Make it executable:

chmod +x /home/agent/nginx-fleet/fleet.py

Part 5 – The CLAUDE.md File

This is what turns Claude Code into your nginx fleet manager. It tells Claude exactly what tools it has, what the fleet looks like, and what it is and is not allowed to do autonomously.

Run:

nano /home/agent/nginx-fleet/CLAUDE.md

Add this:

# nginx Fleet Control Agent

You are the nginx fleet manager for [Your Company]. You have SSH access
to all servers in the fleet via the inventory at ./inventory.json and
the orchestration tool at ./fleet.py.

## What You Can Do

Run fleet.py to execute operations across any subset of servers.
Read and modify nginx config files on remote servers via SSH.
Test configs before applying. Reload or restart nginx services.
Purge nginx cache. Fetch current configs for review.

## How to Target Servers

All servers:        python fleet.py --cmd "..."
By tag:             python fleet.py --tags production --cmd "..."
By tag (multi):     python fleet.py --tags "eu-west,us-east" --cmd "..."
Specific servers:   python fleet.py --ids "web-01,web-03" --cmd "..."

## Common Operations

Test nginx config:  python fleet.py [--tags X] --nginx-test
Reload nginx:       python fleet.py [--tags X] --nginx-reload
Restart nginx:      python fleet.py [--tags X] --nginx-restart
Purge cache:        python fleet.py [--tags X] --purge-cache
Get a config file:  python fleet.py [--ids X] --get-config /etc/nginx/nginx.conf
Run any command:    python fleet.py [--tags X] --cmd "your command" [--sudo]

## Editing Config Files Remotely

To edit a vhost or nginx.conf on a specific server:
1. Fetch the current file with --get-config
2. Show the proposed change to the operator
3. Use SSH + tee or sed to write the change
4. Run --nginx-test to validate
5. Run --nginx-reload to apply

Always test before reload. Never reload without a passing nginx -t.

## Guardrails

- Always run nginx -t before any reload or restart
- Always confirm the target server list with the operator before
  making config changes to more than 5 servers at once
- Never delete vhost files without explicit operator confirmation
- Never modify nginx.conf on all servers simultaneously without
  operator confirmation — do it in batches of 10
- Log every action taken to ./logs/actions.log with timestamp,
  operator, target servers, and command run

## Fleet Facts

- Servers run Ubuntu, mostly 22.04 with some 20.04 variation
- nginx configs live at /etc/nginx/nginx.conf
- vhosts live at /etc/nginx/sites-available/ (symlinked to sites-enabled)
- Cache dir is /var/cache/nginx on most servers (check inventory for exceptions)
- All servers are behind NordLayer VPN and accessible via bastion

Part 6 – Logs Directory

mkdir -p /home/agent/nginx-fleet/logs

Part 7 – Using It

Start a Claude Code session in the project:

cd /home/agent/nginx-fleet
source venv/bin/activate
claude

Then talk to it naturally. Examples of what you can say:

Reload nginx on all production servers
Test the nginx config on web-01 and web-02 before we push the new vhost
Show me the current nginx.conf on web-07
Purge the cache on all eu-west servers
Add a new vhost for bluegrid.io on web-01 — redirect HTTP to HTTPS,
proxy to 127.0.0.1:8080, and set client_max_body_size to 50m
Restart nginx on all us-east servers and show me which ones failed
Check what version of nginx is running across the entire fleet

Claude will read the inventory, figure out which servers to target, propose the commands, execute them, and report results back to you in the terminal.

Part 8 – Multi-User Access

For your team of 2-5, each person installs Claude Code locally, connects to NordLayer, and SSH’s into the control server. Or you run a shared tmux session on the control server that anyone on the team can attach to:

# on the control server
tmux new-session -s fleet
# team members attach from their own machines
ssh -i ~/.ssh/nginx_fleet_key agent@<control-server-ip>
tmux attach -t fleet

For proper multi-user with individual audit trails, each team member gets their own Unix user on the control server, each with access to the fleet SSH key and the project directory.

Ivan Dabić

A man with a beard and glasses, wearing an orange hoodie and a black cap with a Hard Rock Cafe logo, stands with his arms crossed against a plain white background.

Ivan Dabić

Co-founder and CEO of BlueGrid.io, with a background in cloud infrastructure, distributed systems, monitoring, and security operations. He works closely with engineering teams to build and operate reliable systems while documenting both technical and organizational aspects of modern engineering work.

Ivan is a metalhead, and big fan of cyberpunk move genre. If you are his secret Santa go with Star Wars Lego box!

Share this post

Share this link via

Or copy link