SSD Nodes Learn 🎉 VPS from $4.99/mo
Guides Matt ConnorBy Matt Connor

Self-host Iva: a Telegram AI assistant

Run Iva, a self-hosted Telegram AI assistant, on a small VPS: no open ports, no domain, systemd user services and an Obsidian vault you back up.

What you are building

Iva is a self-hosted Telegram AI assistant, and it is one of the few you can deploy with no inbound port to open and no domain to point at it. There is no certificate either, because nothing on the box listens for the public. The service dials out to Telegram, holds the connection open, and reads your messages from the reply. Everything else follows from that one outbound connection.

Iva is MIT licensed and written in Node. Its memory is a folder of plain markdown files that Obsidian opens directly, so the notes it keeps about you stay readable without the app. This guide pins release v0.3.13, published on 6 August 2026.

Most self-hosted software starts with a DNS (domain name system) record and a Let's Encrypt certificate issued with Certbot. Iva skips that layer completely, which is why a small VPS behind a firewall that allows only SSH is a complete deployment.

Why Iva needs no open ports

iva-telegram-poll.service is a long polling bridge. It calls Telegram's getUpdates API and waits for the answer, so every connection starts from your server. Telegram never connects back, which means there is no reverse proxy to configure and no certificate renewal to forget about.

The agent itself does listen, on 127.0.0.1 port 8723 only. The project documentation is direct about it: do not expose port 8723, and any reverse proxy you put in front of it must keep the bearer token requirement. Check the bind address after the install.

sudo ss -tlnp | grep 8723

The address must read 127.0.0.1:8723. A wildcard address such as 0.0.0.0:8723 means the agent's HTTP routes are reachable from the internet, and you should fix that before you send the bot anything private.

So the firewall stays shut. Allow SSH before you enable it, because ufw enable with no SSH rule closes the session you are typing in.

sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status

What the one line installer actually does

The project's front page gives you one command that pipes a script from GitHub into bash. Read the script first, because it does much more than install one program.

curl -fsSL https://raw.githubusercontent.com/smixs/iva/main/install.sh -o iva-install.sh
less iva-install.sh
  • It installs system packages with the detected package manager: git, gh, python3, ffmpeg, pandoc and poppler-utils.
  • It creates a 2 GB swapfile at /swapfile when the box has under 1.5 GB of RAM and no swap, because the build is killed without it.
  • It installs nvm, then Node 24, which is the minimum version Iva runs on.
  • It installs uv, the Python package manager, into ~/.local/bin.
  • It clones the repository to ~/iva and runs npm ci.
  • It installs two global npm packages, agent-browser (which then downloads Chromium) and @googleworkspace/cli.
  • It runs the setup wizard, builds the project, and creates the vault.
  • It writes an iva command into ~/.local/bin and installs the systemd user units.

That is a lot of software for one line. It also explains the README's instruction to install as an ordinary user and not as root: the agent's shell tool later runs with the privileges the installer had. The script calls sudo only for the package installs and the swapfile, through one helper that runs commands directly when you are already root.

Create a dedicated user before you install

Give Iva its own account. The agent runs shell commands on the host through Node's child_process, with no container and no sandbox, so the account it runs under is the security boundary.

sudo adduser --disabled-password --gecos "" iva
sudo usermod -aG sudo iva
sudo install -d -m 700 -o iva -g iva /home/iva/.ssh
sudo cp ~/.ssh/authorized_keys /home/iva/.ssh/authorized_keys
sudo chown iva:iva /home/iva/.ssh/authorized_keys
sudo chmod 600 /home/iva/.ssh/authorized_keys
sudo loginctl enable-linger iva

enable-linger matters because Iva runs as systemd user units. Without linger, systemd stops that user's services the moment their last session ends, so the assistant would die when you close SSH. The same rule governs any service and timer you write yourself under systemd.

The account needs sudo only while the installer adds packages. Take it away when the install is finished.

sudo deluser iva sudo

Log in as that user directly over SSH. A shell you enter with sudo -iu iva has neither DBUS_SESSION_BUS_ADDRESS nor XDG_RUNTIME_DIR set, so every systemctl --user command fails with Failed to connect to bus. Drawing this boundary is the same exercise as running services under least privilege users.

Install a pinned release instead of piping to bash

The installer has a useful property. Before it clones anything, it checks whether the directory the script sits in already holds a package.json containing "eve". If it does, it builds that checkout and skips the clone. So you can choose the version yourself.

git clone --branch v0.3.13 https://github.com/smixs/iva.git ~/iva
cd ~/iva
git log -1 --oneline
bash install.sh

Now you know which code is running. You do not know that when the script clones main at whatever state it happens to be in that hour. Iva shipped five releases between 4 and 6 August 2026, so main this morning and main this afternoon are not the same program.

A tag checkout leaves git in a detached HEAD state. That runs fine, but understand the limit: iva update moves the checkout forward onto the update branch, so the pin is a known starting point and not a permanent freeze. iva version prints the package version and the git commit, so you can always see where you are.

To move to a later release on purpose, list the tags, set IVA_TAG to the one you picked, then run the installer from inside the checkout again.

cd ~/iva
git fetch --tags
git tag --list 'v*' | sort -V | tail -5
IVA_TAG=v0.3.13
git checkout "$IVA_TAG"
bash install.sh --skip-setup
iva restart

--skip-setup stops the wizard from running again over a .env that already works.

The five wizard steps, and the keys they ask for

  1. Model provider and model. MODEL_PROVIDER accepts opencode, ollama, openrouter or codex. The wizard validates the key live and lists the models your plan offers.
  2. Voice and search. A Deepgram key transcribes voice notes. A web search key (Tavily, Exa, Parallel or Brave) is optional.
  3. Telegram bot token from @BotFather, checked against the getMe endpoint.
  4. Access control. You send your bot a message, and the wizard reads your numeric user ID out of getUpdates.
  5. System settings. IANA timezone, vault directory, and the local port, 8723 by default.

Two of those are dependencies the "one command" framing hides. Iva ships no model, so it needs a paid model plan or an API key before it answers anything. It also does not transcribe audio itself, so voice notes need a separate service. Deepgram's nova-3 model with DEEPGRAM_LANGUAGE=multi detects the language, and new Deepgram accounts get starter credits that cover months of personal use. Text works with the model key alone. Only voice depends on Deepgram.

Check what the wizard wrote.

grep -E '^(MODEL_PROVIDER|TELEGRAM_ALLOWED_USER_IDS|ASSISTANT_VAULT_DIR|IVA_PORT)=' ~/iva/.env
ls -l ~/iva/.env

ls should print -rw-------, mode 0600, because that file holds every key you just pasted. TELEGRAM_ALLOWED_USER_IDS must hold your numeric ID. The allowlist is fail closed, so an empty value means Iva answers nobody.

The model is read once, at process start. Editing MODEL_PROVIDER or a model name in .env changes nothing until you run iva restart. On openrouter the model name is a vendor slug such as anthropic/claude-sonnet-4.5, not a bare name. On codex there is no API key at all: iva login signs in to an existing ChatGPT subscription.

What does Iva cost to run each month

ChartMonthly cost of a self-hosted Iva, published list prices, August 2026
The data behind this chart
[
  {
    "plan": "Small VPS, always on",
    "usd_per_month": 5
  },
  {
    "plan": "OpenCode Go model plan",
    "usd_per_month": 5
  },
  {
    "plan": "Ollama Cloud model plan",
    "usd_per_month": 20
  },
  {
    "plan": "Deepgram voice, starter credits",
    "usd_per_month": 0
  },
  {
    "plan": "Tavily web search, free tier",
    "usd_per_month": 0
  },
  {
    "plan": "Cheapest complete setup",
    "usd_per_month": 10
  }
]

These are published list prices as of August 2026, not measurements. A small VPS at 5 US dollars plus the OpenCode Go plan at 5 dollars is the cheapest complete setup, about 10 dollars a month. Ollama Cloud is the other flat rate option at 20 dollars, and its frontier models bill as extra usage on top of the plan. Voice costs 0 while the Deepgram starter credits last.

OpenRouter has no row here because it is pay as you go, so your bill tracks your usage. That is the option to watch: an assistant that carries a 131072 token context window into every turn can outspend a flat plan quickly. Set the context window variable to the model's real size, since an inflated value only wastes tokens.

The two services and the two timers

  • iva.service runs the agent itself.
  • iva-telegram-poll.service runs the long polling bridge that talks to Telegram.
  • iva-memory-doctor.timer fires at 05:00 and runs the nightly maintenance pass over the vault.
  • iva-update-check.timer fires at 10:00 and checks whether a newer release exists.
  • iva-telegram-userbot.service exists only if you set up the optional Telethon proxy.
iva status
systemctl --user status iva.service iva-telegram-poll.service
systemctl --user list-timers
iva logs poll

iva status reports both services and both watchdog timers. systemctl --user list-timers prints the next run time for each timer, which is how you confirm the memory doctor will actually run tonight. Both services should sit in active (running). If one restarts in a loop, journalctl --user -u iva.service -n 100 gives the reason.

The split matters when you debug. The bridge can be alive and polling while the agent is down, and then Telegram accepts your message and nothing ever answers it. iva logs poll follows the bridge, iva logs follows the agent, so the two logs tell you which half is broken.

Where the Obsidian vault lives, and how to back it up

ASSISTANT_VAULT_DIR defaults to vault inside the install directory, so the memory sits at ~/iva/vault. It is its own git repository, separate from the code, which is what lets you update Iva without touching your notes.

  • vault/CORE.md holds durable facts and standing preferences, capped at 1200 characters, and travels in every system prompt.
  • vault/daily/YYYY-MM-DD.md is the day's transcript, treated as append only.
  • vault/cards/ holds typed cards for contacts, projects, decisions, ideas and notes.
  • vault/summaries/daily/, weekly/, monthly/ and yearly/ hold the rollups.
  • vault/attachments/ holds files by date, and vault/.graph/ holds the link graph.
  • vault/schema.json defines the card types and the decay rules.

Rollups run on a schedule inside the process. The daily pass at 04:00 turns yesterday's transcript into cards and a summary and rewrites CORE.md, then the weekly, monthly and yearly passes compress those in turn. At 05:00 the memory doctor runs a deterministic pass with no model involved: it enforces the schema, rebuilds the link graph, regenerates the index, then commits and pushes.

That push is your backup, and it is the step people miss. If the vault has no git remote, the doctor tries to create a private GitHub repository through gh, which needs an authenticated GitHub CLI.

gh auth login
systemctl --user start iva-memory-doctor.service
cd ~/iva/vault && git log --oneline -3

A commit dated today means the pass ran and the vault is committed. gh not available warnings in the log mean the opposite: the vault is maintained but never leaves the server, so a dead VPS takes your memory with it.

Keep a copy you control as well.

tar czf ~/iva-vault-backup.tgz -C ~/iva vault

Copy that file off the box with scp, then delete it from the server. To read the memory in Obsidian, point Obsidian at a clone of the vault repository. Wikilinks and backlinks work as they are, and so does the graph view. Editing cards and CORE.md by hand is safe. Leave MOC.md and .graph/ alone, because the nightly pass regenerates both.

Treat the vault as a log of your life

That directory is a dated record of what you said, who you met and what you decided, on a machine you rent. Two things follow from it.

Self-hosting moves the storage, not the processing. Every turn goes to your model provider, and every voice note goes to Deepgram. The vault is yours. The requests are not private from the companies serving them. Running the memory layer yourself, as with a Mem0 memory server on your own VPS, has the same shape: the store is local while the model call still leaves the building. If a subject is too sensitive for a third party, keep it out of the chat.

The reach of that account is the whole vault. Iva's tools run host native through Node's fs and child_process, with no Docker and no sandbox, so a hijacked turn has whatever access the service user has. That is why the account holds no sudo after the install, and why the allowlist deserves more attention than it looks: it is the gate that decides whose messages become commands on your server. If you want the assistant in your pocket without opening anything, this is the same pattern used to reach a self-hosted Hermes agent from your phone, with the chat client doing the job a public endpoint would otherwise do.

What breaks, and the message you will see

Build killed, exit 137. The kernel's out of memory killer took the build. The installer adds swap only when RAM is under 1.5 GB and no swap exists, so add it yourself and run the installer again.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
free -h

Failed to connect to bus. Every systemctl --user command prints this when the shell has no user session bus, which is what sudo -iu iva gives you. Open a normal SSH session as that user, or export XDG_RUNTIME_DIR=/run/user/$(id -u iva) before the command.

The bot ignores you. TELEGRAM_ALLOWED_USER_IDS is empty, and the allowlist is fail closed. Send the bot a message: it replies with your numeric ID and nothing else. Put that ID in .env and run iva restart.

The bot goes silent right after iva config. The wizard can move IVA_PORT without updating ASSISTANT_HOST, so the bridge connects to a port where nothing answers. Compare the two lines, then restart.

grep -E '^(IVA_PORT|ASSISTANT_HOST)=' ~/iva/.env
iva restart

A voice note gets no reply. Telegram's Bot API refuses file downloads over 20 MB, so the bridge never receives long audio and has nothing to send Deepgram. Split the file before you send it.

ffmpeg -i long.ogg -f segment -segment_time 600 -c copy part-%02d.ogg

A turn hangs and never answers. Wedged workflow state survives a restart and re-queues itself, so restarting alone does not clear it. iva reset quarantines that state and restarts both services. /new in the chat starts a fresh conversation.

Chromium fails on Ubuntu 24.04. Ubuntu 24.04 blocks unprivileged user namespaces through AppArmor, so Chromium's own sandbox cannot start and agent-browser fails. The installer works around it by writing "--no-sandbox" into ~/.agent-browser/config.json. Know that this is there. It lowers the browser's isolation, which is one more reason the account should own nothing except Iva.

FAQ

Do I need a domain or an open port to self-host Iva?

No. Iva talks to Telegram by long polling: iva-telegram-poll.service calls getUpdates and waits, so every connection is outbound from your server. Nothing has to reach the box from outside, so you need no DNS record and no certificate. The agent's own HTTP port, 8723, binds to 127.0.0.1, and the project documentation tells you not to expose it. A firewall that allows SSH and nothing else is the correct configuration.

What does it cost to run Iva each month?

At published August 2026 list prices, a small VPS at 5 dollars plus the cheapest flat rate model plan at 5 dollars comes to about 10 US dollars a month. Ollama Cloud costs 20 dollars instead, and bills frontier models on top. Deepgram starter credits cover voice at first, and the web search tiers have free allowances. OpenRouter is pay as you go, so it has no fixed monthly figure.

Where does Iva keep my data, and how do I back it up?

In ~/iva/vault by default, set by ASSISTANT_VAULT_DIR. It is a separate private git repository of plain markdown: CORE.md, daily/YYYY-MM-DD.md, cards/ and summaries/. The 05:00 memory doctor commits and pushes it, which only helps if the repository has a remote, so run gh auth login or add a remote of your own during setup. Keep an offline copy as well with tar czf ~/iva-vault-backup.tgz -C ~/iva vault and move that file off the server.

Is my data private if I self-host Iva?

The storage is yours. The processing is not. The vault stays on your disk until you push it, and .env sits at mode 0600 owned by the service user. Model calls and voice transcription are cloud APIs, so those messages transit your model provider and Deepgram. Iva is MIT licensed, so you can read exactly what it sends and change it. The Telegram allowlist is fail closed, which means an empty TELEGRAM_ALLOWED_USER_IDS blocks everyone, including you.

Which model providers does Iva support?

MODEL_PROVIDER accepts opencode (OpenCode Go), ollama (Ollama Cloud), openrouter and codex. OpenRouter takes a vendor slug such as anthropic/claude-sonnet-4.5 and offers the widest choice of models. codex signs in to an existing ChatGPT subscription with iva login and uses no API key. Set the provider, set the matching context window to the model's real size, then run iva restart, because the model is read once at process start.