A private Telegram bot can be a convenient way to reach an agent on a VPS, but a connected gateway alone does not show that the intended person can get a reply. This walkthrough starts with a fresh Hermes Agent data directory, admits one numeric Telegram user ID, checks an actual answer, and leaves the operator with a verified stop control. It follows NousResearch's v0.22.2 Docker instructions and Telegram instructions, checked on 19 October 2026. BIG CHANGE did not run a VPS, pull the image, create a bot, or make a model call for this guide.

The big change

  • What changed: A digest-pinned, version-checked Docker image and dedicated /opt/data mount make this one gateway's software and state identifiable.
  • Why it matters: A numeric Telegram allowlist and an actual private reply show whether the intended account can use the gateway.
  • What to watch: Pairing or global grants can widen access, and a harmless prompt does not limit the agent's tools. Check grants, logs, restart and stop state.

You need authority to administer one Linux VPS over SSH, working Docker Engine and host recovery access, a Telegram account that can create a BotFather bot, and an eligible Hermes model provider account. Check the VPS architecture and resources against the official image's amd64 or arm64 build, and check the provider's current access and billing terms before using its wizard. This guide assumes Docker already works on your host; installation and firewall changes depend on your OS and provider. The host needs outbound access to Telegram and your chosen model provider. Reserve a private directory for Hermes state and restrict who can read it: /opt/data will hold credentials, sessions and logs. The Docker manual calls that mount the persistent source of truth.

1. Pin the image before setup

Connect by SSH and remain in the same shell for these commands. The stable tag can move. Pull it, record its repository digest, and confirm that the image itself reports version 0.22.2. Stop if the version differs, the digest is absent, the directory already contains state, or a container named hermes already exists. Refresh the instructions against the actual version or obtain a matching documented image before going further. A digest fixes the image you will run; it does not establish the release version on its own.

Terminal
set -eu
docker info >/dev/null
docker container inspect hermes >/dev/null 2>&1 && { echo 'Container hermes already exists; stop here.'; exit 1; }
HERMES_DATA="$HOME/.hermes"
if [ -e "$HERMES_DATA" ]; then echo 'Hermes data path already exists; stop here.'; exit 1; fi
mkdir -m 700 "$HERMES_DATA"
docker pull nousresearch/hermes-agent:stable
docker image inspect nousresearch/hermes-agent:stable --format '{{json .RepoDigests}}'
IMAGE_REF="$(docker image inspect nousresearch/hermes-agent:stable --format '{{index .RepoDigests 0}}')"
if ! printf '%s\n' "$IMAGE_REF" | grep -Eq '^nousresearch/hermes-agent@sha256:[0-9a-f]{64}$'; then
  echo 'No valid repository digest; stop here.'; exit 1
fi
printf 'Pinned image: %s\n' "$IMAGE_REF"
VERSION_OUTPUT="$(docker run --rm "$IMAGE_REF" --version)"
printf 'Image version: %s\n' "$VERSION_OUTPUT"
if ! printf '%s\n' "$VERSION_OUTPUT" | grep -Eq '(^|[^0-9])v?0[.]22[.]2([^0-9]|$)'; then
  echo 'Image version is not 0.22.2; stop here.'; exit 1
fi

set -eu stops this shell sequence if Docker is unavailable, the pull or inspect fails, or the version command fails. The explicit digest and version checks stop it on an empty digest or a different version. Do not proceed to setup after any such result. Keep the recorded nousresearch/hermes-agent@sha256:… value. Both setup and gateway below use IMAGE_REF, so a later tag move cannot silently change this container. These are reader checks to perform on the actual VPS; there is no verified image digest in this article. Use a fresh dedicated directory on the host's native filesystem, and never run two gateway containers against the same data directory.

2. Configure the provider and one private Telegram sender

Run the Docker setup wizard with the mounted directory. Select a model route your account permits, complete its authentication, and inspect the resulting provider/model setting. Provider access and charge depend on that account. A saved setting does not prove a model request succeeded.

Terminal
docker run -it --rm -v "$HERMES_DATA:/opt/data" "$IMAGE_REF" setup

In Telegram, ask @BotFather for a bot with /newbot and keep its token private. Obtain the intended sender's numeric Telegram user ID, for example with the ID-check method linked from the Telegram manual; a username is not an ID. The wizard or hermes gateway setup can write the bot token and allowlist. If configuring by hand, put TELEGRAM_BOT_TOKEN=<private token> and TELEGRAM_ALLOWED_USERS=<your numeric ID> in $HERMES_DATA/.env, which appears as /opt/data/.env in the container. Keep the token out of commands, shell history, shared notes and this article.

In $HERMES_DATA/config.yaml (/opt/data/config.yaml in the container), set the global unauthorized DM behavior:

YAML
unauthorized_dm_behavior: ignore

After the setup container exits, the official image may have changed $HERMES_DATA to its runtime UID 10000. Its bootstrap restricts .env to mode 600 and tightens an existing config.yaml to 640; a config first seeded during that boot can need the same tightening. An ordinary SSH login may therefore be unable to open these files. If the wizard did not set the needed keys, an authorized host administrator can inspect and edit the two exact files with sudo, then restore their runtime ownership and private modes. Running a host editor as UID 10000 can fail to traverse a login-owned $HOME, so this command uses host administrator access. On a host with sudo and vi, use it after confirming the nonsecret owner/mode readback. Do not proceed on a different UID or a missing file; resolve the host-specific access first.

Terminal
sudo stat -c '%u:%g %a %n' "$HERMES_DATA" "$HERMES_DATA/.env" "$HERMES_DATA/config.yaml"
# Expected: directory owner 10000 and mode 700; .env owner 10000 and mode 600.
# config.yaml should have owner 10000; its first-boot mode can vary.
sudo vi "$HERMES_DATA/.env"
sudo vi "$HERMES_DATA/config.yaml"
sudo chown 10000:10000 "$HERMES_DATA/.env" "$HERMES_DATA/config.yaml"
sudo chmod 700 "$HERMES_DATA"
sudo chmod 600 "$HERMES_DATA/.env"
sudo chmod 640 "$HERMES_DATA/config.yaml"
sudo stat -c '%u:%g %a %n' "$HERMES_DATA" "$HERMES_DATA/.env" "$HERMES_DATA/config.yaml"

The final stat output should show UID/GID 10000 for both files, directory mode 700, .env mode 600 and config.yaml mode 640; it displays metadata, not credentials. The chown targets only those two files, including an editor-created replacement. If sudo or vi is unavailable, use your host's authorized admin procedure with the same owner and mode readback. Do not make the state directory world-readable or remap the container user for this recipe.

While editing, preserve the wizard's provider settings. Ensure no Telegram-specific unauthorized_dm_behavior override sets pair, and no global GATEWAY_ALLOWED_USERS or GATEWAY_ALLOW_ALL_USERS grants broader access. In config.yaml, also ensure neither gateway.allow_all_users nor top-level allow_all_users is true, and that global or Telegram allowlists contain no additional sender. Do not use * in Telegram allowlists, add group allowlists, or approve pairing codes. A fresh directory avoids inherited grants; if it contains existing data, stop and inspect it instead of overwriting it. Keep BotFather's group privacy enabled, do not add the bot to a group, and do not make it a group admin. The gateway access manual documents allowlists and persistent pairing grants as separate access paths. The security manual describes ignore as silently dropping unauthorized DMs.

This allowlist controls who can start a conversation. It does not confine what an authorized agent can do. Hermes Telegram sessions include terminal tools by default; review the configured tool permissions, Docker mounts, provider access and host policy before admitting a sender. A harmless test prompt is only a test prompt.

3. Start the gateway and check the first reply

Start one named container from the recorded digest. The published Docker command includes an optional -p 8642:8642 for API access; omit it here because a private Telegram DM needs no public API or dashboard port. The image supervises its gateway process, while Docker's unless-stopped policy governs container restart when Docker starts. The mounted directory keeps configuration and sessions across a container restart.

Terminal
docker run -d --name hermes --restart unless-stopped \
  -v "$HERMES_DATA:/opt/data" "$IMAGE_REF" gateway run
if ! {
  docker inspect hermes --format '{{.Image}} {{.State.Status}}' &&
  [ "$(docker inspect hermes --format '{{.State.Status}}')" = running ] &&
  docker logs --tail 50 hermes &&
  docker exec -u hermes hermes hermes pairing list
}; then
  echo 'State, logs or pairing check failed; stopping before any message.'
  docker stop hermes || true
  docker inspect hermes --format '{{.State.Status}}' || true
  exit 1
fi

Confirm the container is running, inspect logs for a connected Telegram gateway and provider errors, and check the pairing list before sending a prompt. If any initial read fails, the command block stops the gateway and prints its state; an unknown CLI or grant result is not clearance. Confirm exited before investigating further, and do not message or restart while the state is uncertain. There should be no other approved sender. If one appears, stop the gateway and inspect the stopped state. docker exec cannot run against a stopped container, and starting the ordinary image entrypoint may restore a previously running gateway. For a fresh default-UID installation, the following offline check runs only the pinned Hermes CLI as UID 10000 with networking disabled, bypassing gateway startup. It is an adaptation of the documented pairing commands and image entrypoint, not a command BIG CHANGE has tested. Use it only after confirming the running gateway UID and mounted data owner are both 10000. If ownership, UID, CLI output or grant state is uncertain, leave the gateway stopped and resolve that condition before any restart.

Terminal
if [ "$(docker exec -u hermes hermes id -u)" != 10000 ] || [ "$(stat -c %u "$HERMES_DATA")" != 10000 ]; then
  echo 'Runtime UID or data owner differs; stop for a host-specific repair.'; docker stop hermes; exit 1
fi
docker stop hermes
if [ "$(docker inspect hermes --format '{{.State.Status}}')" != exited ]; then
  echo 'Container is not exited; do not mount state in another container.'; exit 1
fi
docker run --rm --network none --user 10000:10000 \
  --entrypoint /opt/hermes/.venv/bin/hermes \
  -v "$HERMES_DATA:/opt/data" "$IMAGE_REF" pairing list
# For a listed unwanted Telegram user, read the exact numeric ID first:
printf 'Unwanted numeric Telegram user ID: '; IFS= read -r UNWANTED_ID
if ! printf '%s\n' "$UNWANTED_ID" | grep -Eq '^[0-9]+$'; then echo 'Invalid ID; leave gateway stopped.'; exit 1; fi
docker run --rm --network none --user 10000:10000 \
  --entrypoint /opt/hermes/.venv/bin/hermes \
  -v "$HERMES_DATA:/opt/data" "$IMAGE_REF" pairing revoke telegram "$UNWANTED_ID"
docker run --rm --network none --user 10000:10000 \
  --entrypoint /opt/hermes/.venv/bin/hermes \
  -v "$HERMES_DATA:/opt/data" "$IMAGE_REF" pairing list

Confirm the unwanted ID is absent from the final list. Inspect $HERMES_DATA/.env and config.yaml privately for other platform and global allowlists, * or allow-all settings, and remove unwanted grants through the documented configuration path. The gateway remains stopped until all access paths have been checked. Removing one allowlist entry does not revoke an independent pairing or global grant. After that readback, docker start hermes deliberately resumes the original container; inspect its logs and pairing list again before messaging it.

From the allowed account, open a private chat with the bot and send one harmless request such as “Reply with a short greeting.” Confirm an actual answer in that chat and examine the corresponding gateway log or session record. A sent message, typing indicator, status display, or completed setup screen is not a reply. Telegram's /whoami can show the active messaging scope; it is not proof of host authorization. Save only nonsensitive evidence of the check.

If the bot does not answer, first read the container state and docker logs --tail 50 hermes. Check the bot token, the exact numeric TELEGRAM_ALLOWED_USERS value, model-provider authentication and usage eligibility, and outbound connectivity. If the turn is uncertain, inspect the private chat and logs before sending another message. Telegram's documented cold-boot behavior can drop pending updates, and its duplicate suppression is bounded in memory; this route gives no exactly-once delivery guarantee.

4. Check restart, stop and recovery

After the first turn has settled, restart the same container, inspect fresh logs and the retained /opt/data state, then send a new harmless private prompt and confirm its reply. Stop the container and read Docker's state. exited is the stop signal; a quiet chat alone is not. docker start hermes is the deliberate way to resume later.

Terminal
docker restart hermes
docker logs --tail 50 hermes
docker inspect hermes --format '{{.State.Status}}'
# After the restarted gateway is ready, send a new private test message.
docker stop hermes
docker inspect hermes --format '{{.State.Status}}'
# Expected after stop: exited
# When intentionally resuming later: docker start hermes

For a failed restart, check Docker's state and logs, then $HERMES_DATA/logs/gateways/default/current and $HERMES_DATA/logs/container-boot.log, the directory mount and ownership, and the pinned image/version. The v0.22.2 release notes describe a gateway boot-liveness fix; they cannot establish what happened on your host. Do not delete the state directory as a repair. If the bot token leaks, stop the gateway, revoke it in BotFather, replace it privately in the mounted .env, and recheck access before restarting. Revoke unwanted pairing grants and inspect effective allowlists as well.

VPS hosting and model use can incur separate costs. Verify your actual provider plan, usage limits and charges; the procedure has no promised runtime, free model allowance or availability target. Telegram and the selected model provider process the messages sent through them, while the mounted host directory retains local secrets and conversation state. This is a one-host, one-bot first-reply check. Separate BIG CHANGE guides cover Hermes backup and recovery and a recurring Hermes job.

Sources & further reading

The Docker setup, Telegram setup, gateway access guide, security guide and v0.22.2 release are NousResearch's first-party sources. The quickstart explains model-provider setup. These links document the procedure; they are not a record of BIG CHANGE operating a VPS or Telegram bot.