Upgrading
Upgrades touch three things that must stay consistent: the running code, the database schema, and the environment. This page covers the order that keeps them aligned.
Always read the release notes for the version you are moving to before starting.
Bare-Metal and systemd Deployments
- Stop the indexer so the deployed code and the database checkpoint cannot diverge.
sudo systemctl stop ethsync.service- Update the full checkout. Runtime modules, dependencies, and SQL schema files are versioned together; updating only one file produces a mismatched deployment.
git fetch --tags
git checkout <version-tag>- Install dependencies, which is how
python-dotenvand any other new requirement arrive.
pip3 install -r requirements.txt- Apply the schema as a PostgreSQL administrator, because the script manages the
citextextension, theweb_anonrole, and grants. It is idempotent: it creates missing objects, initializessync_statefrom the existing highest block, grants permissions to existingapi_userandapp_userroles, and refreshes themax_blockview. It never deletes transaction data.
sudo -u postgres psql -v ON_ERROR_STOP=1 -d index < create_tables.sqlVerify the environment. Confirm the service still supplies the production
DB_NAME,ETH_URL,START_BLOCK,CONFIRMATIONS_BLOCK,PERIOD, andLOG_FILEvalues. An existing systemd unit can stay as it is: process environment values take precedence over.env, and the address filter defaults to disabled.Start and confirm progress.
sudo systemctl start ethsync.service
psql -d index -c 'SELECT * FROM public.max_block;'max must advance within a few PERIOD intervals.
WARNING
Do not replace a working ethsync.service with the repository template until a production .env exists with equivalent values. The template requires EnvironmentFile to be present, and .env.example ships safe standalone defaults, not your endpoints or credentials.
Docker Compose Deployments
The schema must be in place before the new image starts, for the same reason it must on bare metal.
Update the checkout, so
create_tables.sqlanddocker-compose.ymlmatch the new image.Stop the indexer:
docker compose stop eth-storage- Apply the schema, because the PostgreSQL entrypoint runs
create_tables.sqlonly on an empty data directory:
docker compose exec -T db sh -c \
'psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB"' \
< create_tables.sql- Pin the new version in
.env:
ETH_INDEXER_IMAGE=ghcr.io/adamant-im/eth-transactions-storage:2.5.0- Pull and start the new indexer:
docker compose pull eth-storage
docker compose up -d eth-storage- Verify:
docker compose logs --tail=50 eth-storage
curl -s http://127.0.0.1:3000/max_blockWARNING
Do not start the new image before applying the schema. On a database created before sync_state existed, the startup checkpoint query fails with relation "public.sync_state" does not exist and the indexer exits with status 1. The Compose service declares no restart policy, so applying the schema afterwards does not bring the container back and indexing stays stopped until you start it again.
Rolling back is setting ETH_INDEXER_IMAGE to the previous version tag and repeating steps 5 and 6. See Docker image.
Switching to the Minimal Index Set
Existing deployments created eight indexes. The current recommendation is five. Adding the new ones does not remove the old ones, so reclaim the space explicitly.
- Create the minimal set:
psql -d index -f create_indexes.sql
psql -d index -f create_indexes_add.sqlOn a live database use CREATE INDEX CONCURRENTLY instead, because CREATE INDEX takes a ShareLock that blocks the indexer's inserts.
- Confirm the new indexes are valid and in use, then drop the redundant ones without blocking writes:
DROP INDEX CONCURRENTLY IF EXISTS public.contract_to_index;
DROP INDEX CONCURRENTLY IF EXISTS public.txto_index;
DROP INDEX CONCURRENTLY IF EXISTS public.txto_txfrom_index;This frees roughly 90–110 GB on a one-year dataset of about 490 million rows. If a third-party consumer issues queries the minimal set does not cover, keep the legacy indexes instead — create_indexes_legacy.sql recreates them. Details in Database and Indexes.
Switching PostgREST to web_anon
Order matters, or the public API returns 500 for every request:
- Apply the role and grants with
create_tables.sql - Verify:
psql -d index -c "SET ROLE web_anon; SELECT 1 FROM public.ethtxs LIMIT 1; SELECT * FROM public.max_block; SELECT * FROM public.aval;"- Set
db-anon-role = "web_anon"anddb-max-rows = 10000inpostgrest.conf - Restart PostgREST
Older databases upgraded without re-running create_tables.sql must at minimum have GRANT SELECT ON public.max_block TO web_anon;. Without it the /max_block health check fails with a permission error, and clients that poll it treat the node as unavailable.
Re-indexing a Block Range
Both the rows and the checkpoint have to move together, or the indexer will not revisit the range.
Full rebuild from a new START_BLOCK:
BEGIN;
TRUNCATE TABLE public.ethtxs;
TRUNCATE TABLE public.sync_state;
COMMIT;Partial rescan from block N:
BEGIN;
DELETE FROM public.ethtxs WHERE block >= 21000000;
UPDATE public.sync_state SET last_block = 20999999 WHERE singleton = TRUE;
COMMIT;Stop the indexer before either operation and start it afterwards. Re-indexing re-fetches every block in the range from the node, so a wide range is expensive in RPC calls and time.
Version Compatibility
- The database schema is additive.
create_tables.sqlfrom a newer version can be applied to an older database without losing data - API endpoint names, column names, and response shapes are treated as a stable contract. Existing consumers keep working across upgrades
- Downgrading the indexer while keeping a newer schema works, because the extra objects are ignored. Downgrading past the introduction of
sync_stateis not supported