Upgrade and rollback
Currencyinfo keeps its REST responses and its stored documents backward compatible within a major version, so most upgrades are a container tag change or a git pull plus a rebuild, and a rollback is the same operation in reverse.
Three upgrades need explicit steps, and the first of them is not optional:
- 4.1.2 to 4.2.0 needs a configuration migration. A stock 4.1.2
config.jsoncdoes not start on 4.2.0. See configuration migration and do it before you change the tag - 4.1.2 to 4.2.0 also rebuilds the
tickersindexes, see index rebuild - 4.0.x to 4.1.0 changes the stored document layout, see document migration
Before any upgrade
- Read the release notes for every version between yours and the target
- Back up the database, see backup
- Back up
config.jsonc, which holds every provider key and the ADAMANT notification passphrase - Note the version you are on.
/statusreports it, and it is the tag you roll back to
curl -s http://localhost:36661/status | grep -o '"version":"[^"]*"'- Migrate the configuration if you are coming from 4.1.2 or older, and validate it before restarting anything
Backup
tickers and timestamps are one logical unit. A snapshot is complete only when both collections contain it, so dump them together:
mongodump --uri="mongodb://127.0.0.1:27017/tickersdb" --out=./backup-$(date +%F)Restore into an empty database:
mongorestore --uri="mongodb://127.0.0.1:27017" ./backup-2026-09-04With the shipped Compose file the database lives in the mongo_data named volume, so dumping from inside the container writes to a path you also need to copy out:
docker compose exec -T mongodb \
mongodump --archive --db=tickersdb > tickersdb-$(date +%F).archive
docker compose exec -T mongodb \
mongorestore --archive --drop < tickersdb-2026-09-04.archiveHistory is append-only, so a backup is never invalidated by a later upgrade. It only loses the snapshots recorded after it was taken.
Upgrading a container deployment
Coming from 4.1.2 or older
Migrate config.jsonc first, or the new container exits during validation instead of starting. See configuration migration.
# Pin the target version rather than pulling `latest` into production
docker compose pull
docker compose up -d
docker compose logs -f appWatch for the configuration report and the first Rates from N/M sources saved successfully. line. Then confirm readiness:
curl -s http://localhost:36661/statusIf you pin exact versions, edit the tag in the Compose file first:
services:
app:
image: ghcr.io/adamant-im/currencyinfo:4.2.0Upgrading a source installation
The same configuration migration applies: see configuration migration when coming from 4.1.2 or older.
cd /opt/currencyinfo
git fetch --tags
git checkout v4.2.0
pnpm install --ignore-scripts --frozen-lockfile
pnpm run deps:setup
pnpm run build
sudo systemctl restart currencyinfo--frozen-lockfile fails rather than silently resolving a different dependency tree than the release was tested with.
git checkout aborts on untracked files the new version tracks
A file you created by hand on an older release can become a tracked file in the target one. Git refuses to overwrite it and changes nothing, which is the safe outcome but stops the upgrade:
error: The following untracked working tree files would be overwritten by checkout:
pnpm-workspace.yaml
Please move or remove them before you switch branches.
AbortingMove the file aside and re-run the checkout. Keep the copy until you have compared it with the version the release ships — pnpm-workspace.yaml in particular carries the install-script allow-list, and a hand-written one from 4.1.x is not the same file:
mv pnpm-workspace.yaml ../pnpm-workspace.yaml.local
git checkout v4.2.0config.jsonc and logs/ are git-ignored, so they are never touched by a checkout.
Rollback
A rollback is safe as long as the target version can read the stored documents, which is true for every 4.x release from 4.1.0 onwards.
# Container
docker compose down
# edit the image tag back to the previous version
docker compose up -d
# Source
git checkout v4.1.2
pnpm install --ignore-scripts --frozen-lockfile
pnpm run deps:setup
pnpm run build
sudo systemctl restart currencyinfoRolling back below 4.1.0 requires restoring a backup taken before the 4.1.0 database migration, because the document layout changed.
Indexes created by a newer version are left in place by an older one. They cost write amplification and disk but are never read incorrectly, so a rollback does not require dropping them. Drop them only if you intend to stay on the older version.
4.1.2 to 4.2.0: configuration migration
A stock 4.1.2 configuration does not start on 4.2.0
Validation runs before the HTTP port opens and exits non-zero, so changing the tag and restarting takes the service down rather than degrading it. Migrate the file first.
4.2.0 makes an API key mandatory for two sources that were previously usable without one, because both upstreams changed:
App configuration is invalid:
cryptocompare: api_key: Provide a CoinDesk Data (former CryptoCompare) API key when
CryptoCompare is enabled. The free tier was retired on 21 May 2026
coingecko: api_key: Provide a free CoinGecko Demo API key when CoinGecko is enabled.
Get one at https://www.coingecko.com/en/developers/dashboard
Cannot start the app.Both blocks are enabled in the 4.1.2 template, and an omitted enabled counts as enabled, so an untouched configuration hits both errors.
1. Resolve the two blocking sources
Pick per source. Disabling is the smallest change:
{
"cryptocompare": { "enabled": false },
"coingecko": { "enabled": false }
}Or keep them by supplying credentials:
- CoinGecko needs a free Demo plan key, no credit card. Create one at the developer dashboard and set
coingecko.api_key. See CoinGecko - CryptoCompare needs a paid CoinDesk Data subscription. Without one, leave it disabled. See CryptoCompare
The shipped placeholders are recognised as no credential, so pasting "API key for CryptoCompare" back in does not satisfy the check.
2. Restore coverage
On 4.1.2 those two were the only free crypto sources, so disabling both leaves no crypto coverage at all. Add the keyless sources 4.2.0 ships instead. Copy the blocks from config.default.jsonc and keep your own coin lists:
{
"coinpaprika": { "enabled": true, "ids": ["btc-bitcoin", "eth-ethereum"], "bulk_limit": 200, "max_individual_requests": 5 },
"coinlore": { "enabled": true, "ids": { "BTC": 90, "ETH": 80 } },
"binance": { "enabled": true, "quote_asset": "USDT", "coins": ["BTC", "ETH"] },
"exchange_rate_api": { "enabled": true, "url": "https://open.er-api.com/v6/latest/USD", "codes": ["USD", "EUR"] }
}coinpaprika, coinlore and binance are optional blocks, but exchange_rate_api is validated as a whole: if you add it at all, enabled and url are both required. Per-source identifiers, quotas and terms are in the source reference.
3. Update priorities
priorities is a plain list of names, so an entry for a disabled source is inert rather than an error, but a missing entry for a new source drops it below every listed one. Remove CryptoCompare and add whatever you enabled:
{
"priorities": [
"ExchangeRateHost",
"Coinmarketcap",
"Coingecko",
"CoinPaprika",
"CoinLore",
"Binance",
"ExchangeRateApi",
"CurrencyApi",
"MOEX"
]
}4. Keep everything else
Nothing else changed shape. Your server, notify, base_coins, mappings, strategy, rateDifferencePercentThreshold, groupPercentage, minSources, rateLifetime and refreshInterval values carry over untouched, as do the coin lists of any source you keep.
The schema is strict, so a key that no longer exists is an error rather than something ignored. Merge the new blocks into your file instead of replacing it with the template, or you lose your own settings.
5. Validate before restarting
Config validation happens before anything else, so a throwaway container tells you the verdict without touching your deployment or your database:
docker run --rm \
-v "$(pwd)/config.jsonc:/usr/src/currencyinfo/config.jsonc:ro" \
ghcr.io/adamant-im/currencyinfo:4.2.0A rejected configuration prints the report above and exits non-zero. An accepted one prints InfoService successfully read the configuration file and then fails on the database it cannot reach, which is the expected outcome for this check — stop it with Ctrl-C.
For a source installation, the same check is pnpm run build && pnpm run start:prod against the migrated file.
After the upgrade
Watch the first cycle. minSources is measured against the sources that are actually enabled, so a thinner source mix shows up as a startup warning naming every pair below the threshold, and as expected N, but got M alerts. If you see those, add coverage rather than lowering minSources.
4.1.2 to 4.2.0: index rebuild
Every tickers index is now date-ordered, so /getHistory sorts are served by an index instead of a blocking in-memory sort. Three indexes replace three older ones:
| Created on first start of 4.2.0 | Superseded, not dropped automatically |
|---|---|
{ base: 1, date: -1 } | { base: 1 } |
{ quote: 1, date: -1 } | { quote: 1 } |
{ base: 1, quote: 1, date: -1 } | { base: 1, quote: 1 } |
{ date: 1 } is unchanged.
Mongoose autoIndex creates missing indexes on connect but never drops undeclared ones, so a direct upgrade builds all three new indexes at startup and keeps all three old ones. On a large history collection that is real I/O, and it can delay readiness.
What it costs
Measured on two production deployments, both holding roughly 238 million tickers documents (~19.5 GB of data) after four years of history at a 9 minute refresh interval:
| NVMe, 12 cores, 64 GB RAM, MongoDB 8.0 | SATA, 4 cores, 16 GB RAM, MongoDB 7.0 | |
|---|---|---|
| Build time, all three indexes | 17 minutes | 50 minutes |
indexSize before → after | 9.1 → 19.3 GB | 8.7 → 18.9 GB |
Budget the disk before you start: the three new indexes roughly double indexSize while the superseded ones are still in place. Both deployments kept serving throughout, though a /getHistory query over a wide range can time out behind a reverse proxy while the build saturates the disk.
Scale from your own collection size — the cost is driven by document count, not by the number of distinct pairs.
Building them
Build the new indexes out of band before deploying. One createIndexes command builds all three from a single collection scan, which is substantially cheaper than three separate calls:
db.runCommand({
createIndexes: 'tickers',
indexes: [
{ key: { base: 1, date: -1 }, name: 'base_1_date_-1' },
{ key: { quote: 1, date: -1 }, name: 'quote_1_date_-1' },
{ key: { base: 1, quote: 1, date: -1 }, name: 'base_1_quote_1_date_-1' },
],
});Watch the progress from another shell:
db.getSiblingDB('admin')
.aggregate([{ $currentOp: {} }])
.toArray()
.filter((op) => op.desc?.includes('IndexBuild'))
.forEach((op) => print(op.msg));The names above are the ones Mongoose derives from the same keys, so a later start of 4.2.0 finds them in place and creates nothing.
Once 4.2.0 is running and validated, drop the superseded ones to stop paying for their write amplification and disk:
db.tickers.dropIndex('base_1');
db.tickers.dropIndex('quote_1');
db.tickers.dropIndex('base_1_quote_1');Keep the old indexes until you are past the point of rolling back.
4.0.x to 4.1.0: document migration
4.1.0 changed the internal ticker document layout. Endpoint responses and _id values are unchanged, so API consumers see nothing, but the stored documents must be converted:
pnpm exec node scripts/migrate-db.mjsRun it interactively. The script prompts for the connection string with the input masked, which keeps database credentials out of shell history and out of the process list. If you must supply it non-interactively, load it from a protected file rather than typing it inline:
MIGRATE_DB_URL="$(< /run/secrets/mongo_uri)" pnpm exec node scripts/migrate-db.mjsTake a backup first. The migration rewrites documents in place.
v3 to v4
There is no in-place upgrade path. Install v4 from scratch, then convert the old configuration:
pnpm run migrate ./config.jsonThe script writes a config.jsonc with the migrated properties and prints a warning for every setting that needs attention. See installation for what it changes.
After every upgrade
/statusreportsready: trueand aversionmatching the target/getreturns the pairs your clients expect- The logs contain no repeated source failures and no configuration warnings
- The startup coverage warning lists no pair you rely on
