Automate chain upgrades with Cosmovisor

Written by Furi11 min read

On this page

Put a Cosmos Hub node under Cosmovisor v1.7.3 so it switches to the Gaia v29.0.0 binary at the governance upgrade height on its own. It is for node operators and validators who still swap gaiad binaries by hand at the halt height.

Prerequisites#

  • A Linux server with systemd. The Cosmos Hub installation docs (opens in a new tab) use Ubuntu 22.04 LTS.
  • Hardware sized for the Hub. The join mainnet guide (opens in a new tab) lists 16 GB RAM and 2 TB of storage for a full node, and 32 GB RAM with 500 GB to 2 TB for a validator. Leave extra free disk for the backup Cosmovisor takes before an upgrade, which step 2 explains.
  • Go 1.26. Gaia v29.0.0's go.mod requires Go 1.26.7 or later, and Cosmovisor v1.7.3 requires Go 1.23 or later. The commands below install 1.26.10, the current 1.26 release.
  • git, make, gcc, curl and gnupg.
  • A synced gaiad node on chain ID cosmoshub-4, with its home at ~/.gaia. Run a Cosmos Hub full node with state sync shows how to set one up.

Install the build tools and Go on an amd64 server. On arm64, use the linux-arm64 archive and its SHA256 from go.dev/dl (opens in a new tab).

bash
1sudo apt update
2sudo apt install -y git make gcc build-essential curl gnupg
3curl -LO https://go.dev/dl/go1.26.10.linux-amd64.tar.gz
4echo "ccfe7ae822d2c2f41f4fe7c956199967a4dac22d0ee0d2b2bfdb440aab3b20c5  go1.26.10.linux-amd64.tar.gz" | sha256sum -c -
5sudo rm -rf /usr/local/go
6sudo tar -C /usr/local -xzf go1.26.10.linux-amd64.tar.gz
7cat >> ~/.profile << 'EOF'
8export PATH=$PATH:/usr/local/go/bin
9export PATH=$PATH:$(go env GOPATH)/bin
10EOF
11source ~/.profile
12go version

Cosmovisor only manages binaries and never needs your mnemonic or key files. Keep validator mnemonics offline. Leave priv_validator_key.json in ~/.gaia/config and keep its backup offline too.

How Cosmovisor works#

Cosmovisor is a process manager that wraps your chain binary. cosmovisor run start launches gaiad start as a child process and passes its output through.

When an upgrade plan reaches its height, the x/upgrade module writes $DAEMON_HOME/data/upgrade-info.json and halts the chain. Cosmovisor polls that file every 300 ms by default. When a new plan appears, Cosmovisor stops gaiad and backs up the data folder if backups are on. It then points the current symlink at the new binary, runs the new binary's pre-upgrade command and starts the node again.

Cosmovisor keeps its files under $DAEMON_HOME/cosmovisor:

text
1$DAEMON_HOME/cosmovisor
2├── current -> genesis or upgrades/<name>
3├── genesis
4│   └── bin
5│       └── gaiad
6└── upgrades
7    └── <name>
8        ├── bin
9        │   └── gaiad
10        └── upgrade-info.json

<name> is the upgrade plan name, lowercased. The cosmovisor binary itself can live anywhere on your PATH.

1. Install Cosmovisor#

bash
go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@v1.7.3
cosmovisor version --cosmovisor-only

Cosmovisor v1.7.3, released August 26, 2026, is the current release. v1.7.2 fixed builds with Go 1.26 and later, and v1.7.3 publishes prebuilt binaries with that fix. The Cosmovisor README (opens in a new tab) notes that only a binary installed with go install reports the correct version string.

The v1.7.3 release page (opens in a new tab) has prebuilt archives for linux/amd64, linux/arm64, darwin/amd64 and windows/amd64. If you use one, check its hash before you extract it:

bash
curl -LO https://github.com/cosmos/cosmos-sdk/releases/download/cosmovisor%2Fv1.7.3/cosmovisor-v1.7.3-linux-amd64.tar.gz
curl -LO https://github.com/cosmos/cosmos-sdk/releases/download/cosmovisor%2Fv1.7.3/SHA256SUMS-cosmovisor-v1.7.3.txt
sha256sum -c --ignore-missing SHA256SUMS-cosmovisor-v1.7.3.txt

2. Set the environment variables#

Cosmovisor reads its settings from environment variables. Add them to your shell profile so CLI commands see them:

bash
1cat >> ~/.profile << 'EOF'
2export DAEMON_NAME=gaiad
3export DAEMON_HOME=$HOME/.gaia
4export DAEMON_ALLOW_DOWNLOAD_BINARIES=false
5export DAEMON_RESTART_AFTER_UPGRADE=true
6export UNSAFE_SKIP_BACKUP=false
7EOF
8source ~/.profile

Here is what each variable controls:

  • DAEMON_NAME is the file name Cosmovisor looks for in each bin folder. For the Hub it is gaiad.
  • DAEMON_HOME is the node home that holds config, data and cosmovisor. It must be an absolute path and match the --home you start the node with.
  • DAEMON_ALLOW_DOWNLOAD_BINARIES lets Cosmovisor fetch the new binary from URLs in the upgrade plan. The default is false. The Cosmovisor docs say this option is meant for full nodes, not validators. A failed download stops the node. Keep it off and stage binaries yourself. If you turn it on for a full node, also set DAEMON_DOWNLOAD_MUST_HAVE_CHECKSUM=true.
  • DAEMON_RESTART_AFTER_UPGRADE restarts the node with the new binary after a successful switch. The default is true. With false, Cosmovisor exits after the switch and you start it again yourself.
  • UNSAFE_SKIP_BACKUP defaults to false. With false, Cosmovisor copies $DAEMON_HOME/data to data-backup-<date> before the switch, and the docs recommend keeping backups on. The copy needs as much free space as your data folder takes up, and the node stays down while it runs. To write the copy to another disk, set DAEMON_DATA_BACKUP_DIR to an existing absolute path. Gaia's sample unit file sets UNSAFE_SKIP_BACKUP=true. Only do that if you have a recent snapshot to restore from.

Compare the size of your data folder with the free space on its disk:

bash
du -sh $DAEMON_HOME/data
df -h $DAEMON_HOME

3. Initialize the folder layout#

Copy in the binary you run today#

The genesis folder holds the binary that matches your node's current height. For a synced node, that is your installed gaiad. At the time of writing, cosmoshub-4 nodes report v28.3.1. Confirm the current version on the Gaia releases page (opens in a new tab) before you continue.

bash
gaiad version
cosmovisor init $(which gaiad)
cosmovisor config

cosmovisor init creates cosmovisor/genesis/bin and copies the binary there. It also creates the current link and writes cosmovisor/config.toml. It is a different command from the chain's own gaiad init. cosmovisor config prints the values Cosmovisor sees. Run it after init, because it fails when the cosmovisor folder does not exist yet.

Carry over the last upgrade record#

An existing node usually has data/upgrade-info.json left from the previous upgrade. If current/upgrade-info.json is missing, Cosmovisor treats that old file as a new request and tries to switch on its first start. Copy it across:

bash
if [ -f "$DAEMON_HOME/data/upgrade-info.json" ]; then
  cp "$DAEMON_HOME/data/upgrade-info.json" "$DAEMON_HOME/cosmovisor/current/upgrade-info.json"
fi

4. Stage the upgrade binary#

Read the upgrade plan#

bash
gaiad query upgrade plan

The plan shows the upgrade name and height. Your upgrade folder must use that name. The plan stays empty until a software upgrade proposal passes.

At the time of writing, no plan is active on cosmoshub-4. Proposal 1059 is in its voting period until October 27, 2026. It proposes the name v29.0.0 at height 33618500, which matches the upgrade name in the Gaia v29.0.0 code. Check its current status:

bash
gaiad query gov proposal 1059

If the proposal fails or a new one replaces it, use the name and height from the plan that passes. The commands below use v29.0.0.

Download and verify the release binary#

Gaia v29.0.0 came out on October 19, 2026.

bash
cd ~
curl -LO https://github.com/cosmos/gaia/releases/download/v29.0.0/gaiad-v29.0.0-linux-amd64
curl -LO https://github.com/cosmos/gaia/releases/download/v29.0.0/gaiad-v29.0.0-linux-amd64.asc
curl -LO https://github.com/cosmos/gaia/releases/download/v29.0.0/SHA256SUMS-v29.0.0.txt
grep linux-amd64 SHA256SUMS-v29.0.0.txt | awk '{print $1"  gaiad-v29.0.0-linux-amd64"}' | sha256sum -c -

The checksum file lists the binary as ./gaiad-linux-amd64/gaiad, so the awk step swaps in the downloaded file name before sha256sum checks it. The output must end in OK.

Then check the signature, following the Gaia signing instructions (opens in a new tab):

bash
curl --fail -LO https://raw.githubusercontent.com/cosmos/security/main/release/gaia/signing/keys/cosmos-release.asc
gpg --show-keys --with-fingerprint cosmos-release.asc
gpg --import cosmos-release.asc
gpg --verify gaiad-v29.0.0-linux-amd64.asc gaiad-v29.0.0-linux-amd64

The fingerprint must be E625 9F16 9C7B 913D BB75 DE02 5395 305D 29B8 8B32, and gpg --verify must report Good signature. An [unknown] trust warning by itself is not a problem.

Gaia v29.0.0 ships binaries for linux/amd64 and darwin/amd64 only. On an arm64 server, build from source instead:

bash
cd ~
git clone https://github.com/cosmos/gaia.git
cd gaia
git checkout v29.0.0
CGO_ENABLED=1 make build

The build writes the binary to ~/gaia/build/gaiad. Unlike make install, make build leaves your installed gaiad untouched. Use that path in place of the downloaded file below.

Register it with add-upgrade#

bash
cd ~
chmod +x gaiad-v29.0.0-linux-amd64
./gaiad-v29.0.0-linux-amd64 version
cosmovisor add-upgrade v29.0.0 ./gaiad-v29.0.0-linux-amd64

The version command should print v29.0.0. add-upgrade copies the file to upgrades/v29.0.0/bin/gaiad. Do not pass --upgrade-height for a governance upgrade. That flag writes its own upgrade-info.json and exists for coordinated upgrades without a proposal.

5. Run Cosmovisor under systemd#

Only one process can use the node home at a time. If gaiad already runs as a service, stop and disable it. Replace <old-service> with its unit name:

bash
sudo systemctl stop <old-service>
sudo systemctl disable <old-service>

Create /etc/systemd/system/cosmovisor.service. Replace <your-user> with the Linux user that owns ~/.gaia, and confirm the binary path with which cosmovisor. Use absolute paths, because Environment= lines do not expand shell variables such as $HOME.

ini
1[Unit]
2Description=Cosmovisor for gaiad
3After=network-online.target
4Wants=network-online.target
5
6[Service]
7User=<your-user>
8ExecStart=/home/<your-user>/go/bin/cosmovisor run start --home /home/<your-user>/.gaia
9Restart=always
10RestartSec=3
11LimitNOFILE=50000
12Environment="DAEMON_NAME=gaiad"
13Environment="DAEMON_HOME=/home/<your-user>/.gaia"
14Environment="DAEMON_ALLOW_DOWNLOAD_BINARIES=false"
15Environment="DAEMON_RESTART_AFTER_UPGRADE=true"
16Environment="UNSAFE_SKIP_BACKUP=false"
17Environment="COSMOVISOR_COLOR_LOGS=false"
18
19[Install]
20WantedBy=multi-user.target

Cosmovisor handles the restart after an upgrade. With Restart=always, systemd restarts the service after a crash. COSMOVISOR_COLOR_LOGS=false keeps color codes out of the journal. Load and start the service:

bash
sudo systemctl daemon-reload
sudo systemctl enable cosmovisor
sudo systemctl start cosmovisor

Verify it works#

bash
systemctl status cosmovisor
sudo journalctl -u cosmovisor -f -o cat

Look for a running app line whose path ends in cosmovisor/genesis/bin/gaiad. Then check the link and the sync state:

bash
readlink -f $DAEMON_HOME/cosmovisor/current
gaiad status

gaiad status should show latest_block_height rising.

Watch an upgrade happen#

Before the plan height, confirm the plan and the staged folder, then follow the upgrade lines in the log:

bash
gaiad query upgrade plan
ls $DAEMON_HOME/cosmovisor/upgrades/
sudo journalctl -u cosmovisor -f -o cat | grep -iE "upgrade|backup|running app"

At the plan height you should see these lines, in order:

  1. UPGRADE "v29.0.0" NEEDED at height: <height> from gaiad as the chain halts.
  2. daemon shutting down in an attempt to restart from Cosmovisor.
  3. starting to take backup of data directory, then backup completed, if backups are on.
  4. pre-upgrade command does not exist. continuing the upgrade. Gaia v29.0.0 has no pre-upgrade command, so this line is expected.
  5. upgrade detected, relaunching, then running app with a path under upgrades/v29.0.0/bin.

Blocks resume once validators with more than two thirds of the voting power run the new binary. Then confirm the switch:

bash
readlink -f $DAEMON_HOME/cosmovisor/current
$DAEMON_HOME/cosmovisor/current/bin/gaiad version
$DAEMON_HOME/cosmovisor/current/bin/gaiad query upgrade applied v29.0.0

The gaiad in your Go bin folder stays at the old version. Use the current/bin/gaiad path for CLI work after the upgrade.

Troubleshooting#

Cosmovisor logs binary not present, downloading disabled#

No binary exists at upgrades/<name>/bin/gaiad. The usual cause is a folder name that differs from the plan name. Cosmovisor takes the data backup before it looks for the binary, so this mistake also costs you the backup time. Compare the folder list with the name in the halt file:

bash
ls $DAEMON_HOME/cosmovisor/upgrades/
cat $DAEMON_HOME/data/upgrade-info.json

Stop the service. Stage the binary with cosmovisor add-upgrade <name> <path-to-binary>, where <name> is the name field from the halt file. Then start the service.

The node logs upgrade handler is missing before the upgrade height#

The binary in cosmovisor/genesis/bin is already v29.0.0. That binary only carries the v29.0.0 handler, so it stops on the first block. This happens when make install replaced gaiad before you ran cosmovisor init. Stop the service. Put a verified v28.3.1 binary from the Gaia releases page (opens in a new tab) back in genesis/bin/gaiad.

Cosmovisor asks for an upgrade that already happened#

The node has an old data/upgrade-info.json and current/upgrade-info.json is missing. Stop the service, copy the file as shown in step 3 and start it again.

go install fails with invalid reference to encoding/json.unquoteBytes#

Cosmovisor v1.7.1 and older do not build with Go 1.26 and later. The Cosmos SDK Cosmovisor docs page (opens in a new tab) still shows v1.7.1. Install v1.7.3 with the command in step 1.

Next steps#

  • Rehearse the full flow on a local chain with the SimApp example in the Cosmovisor README (opens in a new tab).
  • Read each release's UPGRADING.md, such as the one for Gaia v29.0.0 (opens in a new tab), before you stage a binary.
  • Look at cosmovisor prepare-upgrade. It reads the active plan over gRPC from the address in DAEMON_GRPC_ADDRESS, which defaults to localhost:9090. When the plan lists download URLs, it fetches the binary. The node needs gRPC enabled. Test it on a full node before you rely on it.
  • Add an alert on block height, so you hear about a stalled upgrade before missed blocks pile up.
  • If you run a validator, read Validator security: sentry nodes, remote signing and firewalls next.

Keep reading

All guides