SSD Nodes Learn
How to do am Matt ConnorBy Matt Connor · Updated 2026-07-24

how to run Gemini CLI for headless VPS

Learn how to setup Gemini CLI on VPS without browser. We cover Node install, no-sudo npm, API-key auth, and using tmux so your SSH session no go drop.

Wetin you dey build

You dey build Gemini CLI wey go dey run always for your own server. You go access am via SSH, and e go run long agent tasks even when you don close your laptop. The install na just three commands. The real work na everything wey need desktop: Google CLI wan open browser for you to log in, but your server no get browser. So, most of this guide na for headless path — we go use current Node wey your distro no get, global npm install wey no need root, browserless auth using API key wey you go hide from shell history, and tmux so if SSH session drop, the task no go stop.

Gemini CLI na open-source (Apache-2.0) Node program (@google/gemini-cli) wey dey talk to Google Gemini models. E fit read and write files, run shell commands, and control tools inside the working directory. For VPS, e be like small agent wey dey always ready to work — na why the account wey e dey run as, and the credentials wey dey for the box, dey important pass any single setting for here.

Prerequisites and the honest gotchas

  • Fresh Ubuntu 24.04 KVM VPS wey get root or sudo access. Any KVM plan go work; the CLI itself no heavy, e only dey use few hundred MB of RAM when e dey idle.
  • Node.js 20 or newer. This one na the hard version limit, and the distro package version low pass am — see the next section.
  • Outbound HTTPS (port 443) go Google's APIs. You no need any inbound ports; the tool na client, no be server, so you no need open any firewall hole for am.
  • One way to authenticate wey no need browser for server: either Gemini API key from Google AI Studio, or SSH tunnel back to browser for your own machine. The API-key way na the best if you wan use am for scripts and unattended runs.
  • Docker or Podman, only if you want --sandbox isolation. This one na optional, we go cover am near the end.

The gotcha wey dey catch everybody: the gemini first-run login flow na for desktop dem. E go try open browser, and for headless box, e either go fail or e go give you link wey no go work. Decide your auth path before you start.

Node: the distro package too old

Ubuntu 24.04 dey carry Node 18.19.1 inside im repositories, with npm 9.2.0. Gemini CLI package.json declare engines: { node: ">=20" }, and npm no dey block mismatch by default — e go still install am and print warning wey show 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 }

If you pass that warning, the CLI go run for unsupported runtime. E go start to behave bad or crash as soon e reach Node 20+ API wey e expect to see. Node 18 reach end-of-life for April 2025, so e no go work anyway. Install current LTS before you install the CLI. Two clean ways na NodeSource (system-wide signed apt repo) or nvm (per-user version manager). Pick one.

NodeSource, if you want make every user for the box get Node:

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 — v24.x na the current active LTS. Check NodeSource page for the current setup script; the setup_24.x for the URL na the part wey you must change when new LTS land.

nvm, if you want keep Node inside one user home and you no want use 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 for that URL na the one wey dey current when dem write this; check nvm README for the latest release and change the version before you run am. nvm get one big advantage for this work: e dey install Node and im global packages under ~/.nvm, so the global-install permission problem for next section no go happen at all. If you use nvm, you fit skip the npm-prefix step.

Install the CLI without sudo npm -g

The command wey dey tempt person na sudo npm install -g @google/gemini-cli. No do am. If you use root-owned global prefix, permission errors go dey for every next install wey you go do. E go also leave root-owned files inside your npm cache wey go cause wahala months from now. If you run plain (no-sudo) npm install -g against a system Node, you go see another 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'

Na because npm dey try write inside /usr/lib, and your user no get permission for there. The fix no be to use sudo — the fix na to point npm's global prefix to your home directory. This way, global installs go land for place wey 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

We purposely use ~/.bashrc, no be ~/.profile: tmux — wey you go use run the CLI inside two sections from now — start non-login shell wey dey read ~/.bashrc and e dey skip ~/.profile. So if you put PATH line for wrong file, gemini go no show for the exact place wey you need am. If gemini --version print version number, everything set. If you see gemini: command not found instead, your PATH export no work — check the failure modes. If you dey use nvm, skip the prefix lines altogether: e already dey install globals under your home.

If you don run sudo npm before and you dey see Your cache folder contains root-owned files now, fix am once with sudo chown -R $(id -u):$(id -g) ~/.npm.

The headless auth problem, and how to get past it

Run gemini interactively for the first time and e go offer to log you in with your Google account. For desktop computer, e go open browser tab. For headless VPS, no browser dey, so the flow go either print localhost URL wey e expect you to open, or e go fail completely with something like:

Failed to open browser. Please visit the following URL to authorize:
https://accounts.google.com/o/oauth2/v2/auth?...&redirect_uri=http://localhost:PORT

The trap na the redirect_uri=http://localhost:PORT. Even if you open that URL for your laptop and approve am, Google go redirect to http://localhost:PORT — localhost for the server, and no port wey your laptop fit reach. The login no go ever finish.

Two real ways dey to pass through.

The first one na API key, and na the correct default for server. Create key for Google AI Studio (aistudio.google.com) and give the CLI as environment variable; e go read GEMINI_API_KEY and skip the browser flow entirely. Now, for the "keep it out of history and world-readable files" part: Do not type export GEMINI_API_KEY=AIza... for the prompt — e go land for ~/.bash_history for cleartext, and do not put am for file wey others fit read. Write am to mode-600 file wey the shell go source 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 mean say only your user fit read the file. Confirm say the key reach the environment with printenv GEMINI_API_KEY; if e print nothing, the CLI go go back to browser flow and fail. E go also read .env file for ~/.gemini/ if you prefer that layout — same rule, so chmod 600 ~/.gemini/.env.

The second way na to use personal-Google-account login (and its free tier) by tunnelling the OAuth callback back to your laptop. The wahala be say the CLI loopback server bind random port every time e run, so nothing stable to forward unless you pin am first with the OAUTH_CALLBACK_PORT environment variable, then forward that exact port:

# from your laptop, forward the callback port into the SSH session:
ssh -L 8085:localhost:8085 user@your-server
# then, on the server, pin the callback to the same port and start the CLI:
export OAUTH_CALLBACK_PORT=8085
gemini

The CLI no fit open browser, so e go print the auth URL; open am for your laptop browser, approve am, and when Google redirect to http://localhost:8085/..., the SSH forward go carry am reach the loopback server for the VPS and the login go finish. If you leave the port unpinned, e go land for new random port every time e run, and no ssh -L wey you set up before go fit catch am. E dey work, but e need you to dey sit for browser, so e no good for scripts. For anything wey you wan leave running, use the API key.

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

Run am inside tmux so SSH session wey drop no go kill am

Any gemini process wey you launch straight from your SSH shell na child of that shell. If connection drop — laptop close, Wi-Fi loss, or idle timeout — sshd go tear down the pseudo-terminal. The shell go get SIGHUP, and e go hang up for CLI too. If you dey edit files for 10 minutes and the task die, you no go see the process again when you reconnect.

tmux fix this problem because e own the shell instead of sshd owning am. This na the same pattern as running an AI coding agent on a remote VPS inside tmux, and e work the same way 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 attach to session wey dem name gemini if e dey, or e go create am if e no dey. Na the only command you need run right after every login. The shell inside belong to the detached tmux server, no be your SSH session, so if connection drop, the CLI go still dey work. Reconnect, attach, and you go see the same scrollback.

For non-interactive, scripted runs, Gemini CLI get headless mode: gemini -p "summarise the failing tests in this repo" print answer and exit, and --output-format json give machine-readable output wey you fit pipe elsewhere. Headless mode with API key na wetin you need inside tmux session wey dey run long batch job, or if you run am from cron entry — but one thing: cron job no dey source any of your login files. So, make sure you give the crontab line its own GEMINI_API_KEY (or make the command source ~/.gemini_env), otherwise the CLI go try use browser flow and e go fail.

Sandboxing and permissions for box wey dey run production

Agent wey get shell access na shell. Gemini CLI fit run commands, and by default e dey ask before e run any risky command — but people dey use --yolo (auto-approve every tool call), and then e fit delete files, push to git, or hit internal services with full authority of the user wey e dey run as. For box wey dey run production, dat one na real blast radius, no be just theory.

Three controls, based on how dem go help you:

  • Run am as dedicated, unprivileged user. No be root, no be member of sudo. Create an agent user with im own home, install Node and the CLI for there, and if instruction fail, e go stay inside dat account only. Dis one na di most important decision.
  • Keep production credentials far from di box. No prod ~/.aws/credentials, no .env wey dem copy from production, no database password wey get write access to anything wey important. Give am staging or read-only credential.
  • Use di built-in sandbox. If Docker or Podman dey ground, gemini --sandbox (or GEMINI_SANDBOX=docker) dey run di agent's tool calls inside container wey isolated from di host filesystem and network. E no be substitute for di unprivileged user, but e na strong second layer when di same VPS dey do real work.

If you dey run Gemini CLI beside other self-hosted tooling — like MCP server wey dey expose tools to di agent for di same VPS — treat every new capability as more surface wey di agent fit reach, and limit di tokens wey e dey get to exactly one job.

Quota, cost, and which auth path you chose

The auth path na him dey decide how dem go bill you. Personal Google account (the OAuth path) dey use free Gemini Code Assist tier, wey get real per-minute and per-day limits; if you pass dem, requests go return rate-limit error until the window reset. API key from AI Studio fit be free-tier or dem go bill you depending on the project — billed key dey increase the limits and dem dey charge per token. Vertex and Cloud-project auth dey bill through Google Cloud.

Two practical notes. An unattended agent wey dey run for loop fit burn quota fast, so watch am well for the first few times before you trust am for cron job. And if your reason for server-side model na privacy or unmetered inference instead of Google hosted models, that na different tool — self-hosting an open LLM with Ollama on a VPS dey keep the weights and the prompts for your own box, but you go run much smaller model than Gemini.

How to keep am updated

Gemini CLI dey get many updates. Because you install am inside user-owned prefix, you no need to use sudo for any update:

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

Dem get different release channels: @latest na stable, @preview na weekly preview, @nightly na bleeding edge — if you want stability, pin am to @latest. For nvm, global packages dey inside the active Node version, so if you use nvm use to change Node, you fit need to reinstall the CLI. Better read the release notes instead of to dey chase every single patch.

Failure modes, with the exact strings

npm WARN EBADENGINE Unsupported engine ... required: { node: '>=20' }, then the CLI crash for runtime. Node too old — distro dey use 18.19.1, and dat one don pass end-of-life. Install Node 20+ from NodeSource or nvm, confirm am with node --version, and if you get many Nodes for machine, make sure which node dey point to new one and no be /usr/bin/node.

npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Global install enter prefix wey root own. No use sudo for am — set npm config set prefix ~/.npm-global, put ~/.npm-global/bin for PATH, and reinstall am as your normal user. If old sudo npm leave cache files wey root own (Your cache folder contains root-owned files), run sudo chown -R $(id -u):$(id -g) ~/.npm.

Failed to open browser, login wey hang, or redirect_uri=http://localhost:PORT wey you no fit reach. OAuth flow dey find browser wey server no get, and localhost callback dey point to server instead of your laptop. Use API-key path (GEMINI_API_KEY), or pin OAUTH_CALLBACK_PORT, forward am over SSH with ssh -L, and open the URL locally.

Process vanish when SSH drop. You run gemini direct from SSH shell, so e be child of dat shell and e die when pty disconnect. Nothing to recover. Start every session with tmux new -A -s gemini and run the CLI inside am.

Auth still fail even when key set — CLI go back to auth picker, or request go return API key not valid with HTTP 400. Key no dey inside environment wey CLI dey see. Confirm am with printenv GEMINI_API_KEY; if e empty, your ~/.gemini_env no get source — check if line dey inside ~/.bashrc, wey interactive shells (including tmux) dey read but cron and other non-interactive shells no dey read. One extra space or quote inside key value go also cause API key not valid.

429 / RESOURCE_EXHAUSTED / rate-limit message. You don hit quota for whichever tier your auth dey use. Wait for window to reset, slow down the agent, or move to billed API key. Agent wey dey retry loop go keep hitting dis — stop am and check wetin e dey do.

FAQ

How I go take authenticate Gemini CLI for headless server?

Use API key, no be browser login. Create key for Google AI Studio, put am for one mode-600 file wey your shell dey source (export GEMINI_API_KEY=...), and the CLI go skip that OAuth browser flow entirely. If you really want the personal-account free tier, pin the loopback port with OAUTH_CALLBACK_PORT=8085, forward am back to your laptop with ssh -L 8085:localhost:8085 user@server, and open the URL wey dem print for local machine — but that one need person for browser, so e no good for scripts.

Why npm global install dey ask for sudo, and how I go take avoid am?

Because npm default global prefix na /usr/lib/node_modules, and your user no get permission to write for there, so plain npm install -g go fail with EACCES. Wrong fix na sudo npm -g, because e dey leave root-owned files wey go break later installs. Correct fix na to point the prefix to your home (npm config set prefix ~/.npm-global) and add im bin to PATH, or just use nvm, wey dey install global packages for your home automatically.

How I go take make Gemini CLI continue to run after I disconnect?

Run am inside tmux. Any process wey you start from SSH shell go die when connection drop because e be child of that shell; tmux dey run the shell under one detached server wey dey survive disconnect. Use tmux new -A -s gemini, run gemini inside, detach with Ctrl-b d, and reattach later with tmux attach -t gemini.

E safe to run Gemini CLI for production box?

Only if you dey careful, because agent wey get shell access fit do anything wey the user wey run am fit do. Run am as one dedicated unprivileged user wey no get sudo, keep production credentials far from the machine, avoid --yolo auto-approval, and use --sandbox (Docker or Podman) to isolate tool calls from the host. The account wey e dey run under important pass any single flag wey you set.

I need to open any firewall ports for Gemini CLI?

No. E be client wey dey make outbound HTTPS calls to Google APIs, so e need outbound port 443 but e no need any inbound ports. If you use OAuth tunnel, the pinned callback port (like 8085) dey live for localhost and SSH forward dey carry am reach there, no be open inbound port. Keep inbound ports locked down.

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