SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Run Gemini CLI on a headless VPS

Run Google's Gemini CLI on a headless VPS: a current Node, a no-sudo global install, browserless API-key auth, and tmux so long tasks survive SSH drops.

What you are building

An always-on Gemini CLI on a server you own, reachable over SSH, running long agent tasks that keep working after you close the laptop. The install is three commands. What takes work is everything that assumes a desktop: Google's CLI wants to open a browser to log you in, and your server does not have one. So most of this guide is the headless path, a current Node the distro will not give you, a global npm install that does not need root, browserless auth with an API key you keep out of your shell history, and tmux so a dropped SSH session does not take a running task with it.

Gemini CLI is an open-source (Apache-2.0) Node program (@google/gemini-cli) that talks to Google's Gemini models and can read and write files, run shell commands, and drive tools in the working directory. On a VPS it is a small, always-available agent you can leave working, which is why the account it runs as, and the credentials sitting on the box, matter more than any single setting here.

Prerequisites and the honest gotchas

  • A fresh Ubuntu 24.04 KVM VPS with root or sudo. Any KVM plan works; the CLI itself is light, a few hundred MB of RAM at rest.
  • Node.js 20 or newer. This is the one hard version floor, and the distro package is below it, see the next section. Node 20 itself reached end-of-life on April 30, 2026, so install 24.
  • Outbound HTTPS (port 443) to Google's APIs. No inbound ports are needed; this is a client, not a server, so you open no firewall hole for it.
  • A way to authenticate that does not need a browser on the server: either a paid Gemini API key from Google AI Studio, or the Google sign-in, which on a headless box has you paste a code back into the terminal. The API-key path is the one that scales to scripts and unattended runs.
  • Docker or Podman, only if you want the --sandbox isolation. Optional, covered near the end.

The gotcha that catches everyone now is the account, not the browser. Google announced on May 19, 2026 that from June 18, 2026 Gemini CLI would stop serving requests for Google AI Pro and Ultra subscribers and for anyone using it free of charge, and it points those users to its replacement, Antigravity CLI (agy). Gemini CLI still works with paid Gemini API keys, Gemini Enterprise Agent Platform API keys, and an organisation's Gemini Code Assist Standard or Enterprise licence. Decide the auth path before you start.

Node: the distro package is too old

Ubuntu 24.04 ships Node 18.19.1 in its own repositories, paired with npm 9.2.0. Gemini CLI's package.json declares engines: { node: ">=20" }, and npm does not hard-stop a mismatch by default, it installs anyway and prints a warning that names the gap:

npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@google/gemini-cli@0.50.0',
npm WARN EBADENGINE   required: { node: '>=20' },
npm WARN EBADENGINE   current: { node: 'v18.19.1', npm: '9.2.0' }
npm WARN EBADENGINE }

Push past that warning and the CLI runs on an unsupported runtime, where it misbehaves or crashes the moment it reaches a Node 20+ API it expects to exist. Node 18 also reached end-of-life in April 2025, so it is a dead end either way. Install a current LTS before you install the CLI. The two clean routes are NodeSource (a system-wide signed apt repo) or nvm (a per-user version manager). Pick one.

NodeSource, if you want Node available to every user on the box:

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version

node --version must print v20.x or higher to satisfy the CLI, but Node 20 has been end-of-life since April 30, 2026, so aim for v24.x, the current active LTS. Node 26 is scheduled to become the active LTS on October 28, 2026. Check the NodeSource page for the current setup script; the setup_24.x in the URL is the line to bump when a newer LTS lands.

nvm, if you would rather keep Node inside one user's home and never touch it with sudo:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
node --version

The v0.40.1 in that URL was current when this was written; check nvm's README for the latest release and swap the version in before you run it. nvm has a real advantage for this job: it installs Node and its global packages under ~/.nvm, so the global-install permission problem in the next section simply never happens. If you go the nvm route, you can skip the npm-prefix step.

Install the CLI without sudo npm -g

The tempting command is sudo npm install -g @google/gemini-cli. Do not. A root-owned global prefix produces permission errors on every later install and leaves root-owned files in your npm cache that bite months from now. Run a plain (no-sudo) npm install -g against a system Node and you get the other failure:

npm error code EACCES
npm error syscall mkdir
npm error path /usr/lib/node_modules/@google
npm error errno -13
npm error Error: EACCES: permission denied, mkdir '/usr/lib/node_modules/@google'

That is npm trying to write into /usr/lib, which your user cannot. The fix is not sudo, it is to point npm's global prefix at your home directory so global installs land somewhere you own:

mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
npm install -g @google/gemini-cli
gemini --version

~/.bashrc, not ~/.profile, is deliberate: tmux, which you will run the CLI inside two sections from now, starts a non-login shell that reads ~/.bashrc and skips ~/.profile, so a PATH line in the wrong file leaves gemini invisible in exactly the place you need it. gemini --version printing a version number is the whole test. If you instead get gemini: command not found, your PATH export did not take, see the failure modes. On nvm, skip the prefix lines entirely: it already installs globals under your home.

If you ran sudo npm at some earlier point and now see Your cache folder contains root-owned files, repair it once with sudo chown -R $(id -u):$(id -g) ~/.npm.

The headless auth problem, and how to get past it

Run gemini interactively the first time and it offers to log you in with your Google account. On a desktop that opens a browser tab. On a headless Linux VPS the CLI checks for a display first, and when none of DISPLAY, WAYLAND_DISPLAY or MIR_SOCKET is set it does not try to launch a browser at all. It prints a sign-in link and waits for you to paste a code back:

Please visit the following URL to authorize the application:

https://accounts.google.com/o/oauth2/v2/auth?...

Enter the authorization code:

That flow works over plain SSH, but it needs you at a browser every time, and since June 18, 2026 it only gets you in if your Google account is one Gemini CLI still serves.

There are two honest ways through.

The first is an API key, and it is the right default for a server. Create a key in Google AI Studio (aistudio.google.com) on a project with billing turned on, since Gemini CLI no longer serves free-of-charge use, and hand it to the CLI as an environment variable; it reads GEMINI_API_KEY and skips the browser flow entirely. Now the "keep it out of history and world-readable files" part. Do not type export GEMINI_API_KEY=AIza... at the prompt, it lands in ~/.bash_history in cleartext, and do not put it in a file others can read. Write it to a mode-600 file the shell sources at start:

umask 077
printf 'export GEMINI_API_KEY=%s\n' 'AIzaSyYOUR_KEY_HERE' > ~/.gemini_env
chmod 600 ~/.gemini_env
echo '[ -f ~/.gemini_env ] && . ~/.gemini_env' >> ~/.bashrc
source ~/.bashrc

chmod 600 means only your user can read the file. Confirm the key reached the environment with printenv GEMINI_API_KEY; if that prints nothing, the CLI drops back to its auth picker instead of using the key. It also reads a .env file in ~/.gemini/ if you prefer that layout, same rule, so chmod 600 ~/.gemini/.env.

The second way is the Google sign-in itself, which on a headless box needs no tunnel and no open port. Since June 18, 2026 it is mainly useful for an organisation's Gemini Code Assist Standard or Enterprise licence (set GOOGLE_CLOUD_PROJECT as well); free personal accounts and Google AI Pro or Ultra subscriptions are no longer served. Start the CLI over a normal SSH session and choose Sign in with Google:

gemini

Copy the printed URL into your laptop browser and approve. Google redirects to https://codeassist.google.com/authcode, which shows you an authorization code; paste it at the Enter the authorization code: prompt on the VPS and the login completes. Nothing on the server waits for a callback, so there is no port to pin and no ssh -L to set up. OAUTH_CALLBACK_PORT only matters to the loopback flow the CLI uses when it finds a display. If you connected with X11 forwarding (ssh -X), DISPLAY is set and the CLI takes that loopback flow instead, with a redirect to http://127.0.0.1:PORT/oauth2callback on the server; run NO_BROWSER=true gemini to force the paste-a-code flow. It works, but it needs you sitting at a browser, so it is no good for scripts. For anything you leave running, use the API key.

For Vertex AI or a Google Cloud project instead of AI Studio, set GOOGLE_API_KEY together with GOOGLE_GENAI_USE_VERTEXAI=true, or GOOGLE_CLOUD_PROJECT for a Code Assist licence, same environment-variable discipline, same mode-600 file.

Run it inside tmux so a dropped SSH session does not kill it

A gemini process you launch straight from your SSH shell is a child of that shell. Lose the connection, closed laptop, dropped Wi-Fi, an idle timeout, and sshd tears down the pseudo-terminal, the shell gets SIGHUP, and it hangs up on the CLI in turn. A task ten minutes into editing files dies with it, and on reconnect there is no process to recover.

tmux fixes this by owning the shell instead of sshd owning it. This is the same pattern as running an AI coding agent on a remote VPS inside tmux, and it works identically here:

sudo apt install -y tmux
tmux new -A -s gemini
# inside the session:
gemini
# detach with Ctrl-b then d, the task keeps running
# reconnect later from any machine:
tmux attach -t gemini

tmux new -A -s gemini attaches to a session named gemini if it exists and creates it if it does not, so it is the one command to run right after every login. The shell inside belongs to the detached tmux server, not to your SSH session, so dropping the connection leaves the CLI working. Reconnect, attach, and you are back in the same scrollback. If you end up running several agent sessions on one box, one per tmux session, they have no way to talk to each other here, unlike Claude Code, where one session can hand text to another on the same VPS, so keep each Gemini job independent or coordinate them through files on disk.

For non-interactive, scripted runs, Gemini CLI has a headless mode: gemini -p "summarise the failing tests in this repo" prints an answer and exits, and --output-format json gives machine-readable output to pipe elsewhere. Headless mode with an API key is exactly what you want inside a tmux session running a long batch job, or fired from a cron entry, with one caveat: a cron job sources none of your login files, so give the crontab line its own GEMINI_API_KEY (or have the command source ~/.gemini_env), or the run fails for lack of credentials.

Sandboxing and permissions on a box that also runs production

An agent with shell access is a shell. Gemini CLI can run commands, and by default it asks before each risky one, but people reach for --yolo (auto-approve every tool call), and then it can delete files, push to git, or hit internal services with the full authority of the user it runs as. On a box that also runs production, that is a real blast radius, not a hypothetical.

Three controls, in order of how much they buy you:

  • Run it as a dedicated, unprivileged user. Not root, not a member of sudo. Create an agent user with its own home, install Node and the CLI there, and a misread instruction stays confined to that account. This is the single highest-value decision.
  • Keep production credentials off the box. No prod ~/.aws/credentials, no .env copied down from production, no database password with write access to anything that matters. Give it a staging or read-only credential.
  • Use the built-in sandbox. With Docker or Podman installed, gemini --sandbox (or GEMINI_SANDBOX=docker) runs the agent's tool calls inside a container that mounts only the current project directory, not the rest of the host filesystem. It is not a substitute for the unprivileged user, but it is a strong second layer when the same VPS is doing real work.

If you are running Gemini CLI next to other self-hosted tooling, an MCP server exposing tools to the agent on the same VPS, say, treat each added capability as more surface the agent can reach, and scope the tokens it is handed to exactly one job.

Quota, cost, and which auth path you chose

The auth path decides how you are billed, and since June 18, 2026 whether you are served at all. From that date, per Google's announcement, Gemini CLI stopped serving requests for Google AI Pro and Ultra subscribers and for anyone using it free of charge, which takes in the personal-account free tier and unbilled API keys. It remains available through paid Gemini API keys and Gemini Enterprise Agent Platform API keys, which charge per token, and for organisations with a Gemini Code Assist Standard or Enterprise licence or other Google Cloud access, which bill through Google Cloud. Each tier still has per-minute and per-day limits; exceed them and requests return a rate-limit error until the window resets.

Two practical notes. An unattended agent in a loop can burn quota fast, so watch it the first few times before you trust it to a cron job. And if your reason for a server-side model is privacy or unmetered inference rather than Google's hosted models, that is a different tool, self-hosting an open LLM with Ollama on a VPS keeps the weights and the prompts on your own box, at the cost of running a much smaller model than Gemini.

Keeping it updated

Gemini CLI ships often. Because you installed it into a user-owned prefix, updates never need sudo:

npm install -g @google/gemini-cli@latest
gemini --version

There are release channels: @latest is stable, @preview is the weekly preview, @nightly is the bleeding edge, pin to @latest on anything you rely on. On nvm, global packages live under the active Node version, so after nvm use to switch Node you may need to reinstall the CLI. Read the release notes rather than chasing every patch.

Failure modes, with the exact strings

npm WARN EBADENGINE Unsupported engine ... required: { node: '>=20' }, then the CLI crashing at runtime. Node is too old, the distro's 18.19.1, which is also past end-of-life. Install a current LTS from NodeSource or nvm (Node 20 meets the CLI's floor but has itself been end-of-life since April 2026), confirm with node --version, and if you have several Nodes installed, check which node points at the new one and not /usr/bin/node.

npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. A global install into a root-owned prefix. Do not sudo it, set npm config set prefix ~/.npm-global, put ~/.npm-global/bin on PATH, and reinstall as your normal user. If an earlier sudo npm left root-owned cache files (Your cache folder contains root-owned files), run sudo chown -R $(id -u):$(id -g) ~/.npm.

Failed to open browser, or a redirect_uri pointing at http://127.0.0.1:PORT you cannot reach. The CLI found a display variable (often from ssh -X) and took the loopback flow, whose callback lands on the server. Run NO_BROWSER=true gemini to get the paste-a-code flow, or use the API-key path (GEMINI_API_KEY). On a plain SSH session you should instead see Enter the authorization code:; approve the URL in your laptop browser and paste the code Google shows you.

The process vanished when SSH dropped. You ran gemini straight from the SSH shell, so it was a child of that shell and died with the pty on disconnect. Nothing to recover. Start every session with tmux new -A -s gemini and run the CLI inside it.

Auth still fails with the key set, the CLI drops back to its auth picker, or a request returns API key not valid with HTTP 400. The key is not in the environment the CLI sees. Confirm with printenv GEMINI_API_KEY; if it is empty, your ~/.gemini_env was never sourced, check the line is in ~/.bashrc, which interactive shells (tmux included) read but cron and other non-interactive shells do not. A stray space or quote inside the key value also produces API key not valid.

429 / RESOURCE_EXHAUSTED / a rate-limit message. You hit the quota for whichever tier your auth uses. Wait for the window to reset, slow the agent down, or move to a billed API key. An agent stuck in a retry loop keeps hitting this, stop it and check what it is doing.

FAQ

How do I authenticate Gemini CLI on a headless server?

Use a paid API key, not the browser login. Create a key in Google AI Studio on a billed project, put it in a mode-600 file your shell sources (export GEMINI_API_KEY=...), and the CLI skips the OAuth browser flow entirely. If you sign in with Google instead, for example on a Code Assist Standard or Enterprise licence, the CLI on a headless box prints a URL; open it on your laptop, approve, and paste the code Google shows back at the Enter the authorization code: prompt. There is no port to pin and no tunnel, but it needs you present at a browser, so it is no good for scripts. Free personal accounts have not been served by Gemini CLI since June 18, 2026.

Why does the npm global install want sudo, and how do I avoid it?

Because npm's default global prefix is /usr/lib/node_modules, which your user cannot write to, so a plain npm install -g fails with EACCES. The wrong fix is sudo npm -g, which leaves root-owned files that break later installs. The right fix is to point the prefix at your home (npm config set prefix ~/.npm-global) and add its bin to PATH, or use nvm, which installs global packages under your home automatically.

How do I keep Gemini CLI running after I disconnect?

Run it inside tmux. A process started from your SSH shell dies when the connection drops because it is a child of that shell; tmux runs the shell under a detached server that survives the disconnect. Use tmux new -A -s gemini, run gemini inside, detach with Ctrl-b d, and reattach later with tmux attach -t gemini.

Is it safe to run Gemini CLI on a production box?

Only with care, because an agent with shell access can do anything the user it runs as can do. Run it as a dedicated unprivileged user with no sudo, keep production credentials off the machine, avoid --yolo auto-approval, and use --sandbox (Docker or Podman) to isolate tool calls from the host. The account it runs under matters more than any single flag you set.

Do I need to open any firewall ports for Gemini CLI?

No. It is a client that makes outbound HTTPS calls to Google's APIs, so it needs outbound port 443 but no inbound ports. The Google sign-in on a headless box needs no port either: you paste a code back into the terminal instead of receiving a callback on the server. Keep inbound locked down.

#gemini-cli#node#tmux#headless#ai#vps