Skip to content

Deploying to TestNet and MainNet

This guide takes an AlgoKit project that already deploys to LocalNet and deploys it to a public Algorand network: TestNet for staging or MainNet for production.

LocalNet gives you a node you control. On a public network you need an endpoint for algod to submit transactions and read current state, and an endpoint for indexer to resolve which of your applications already exist. Everything else about the deploy is the same.

  • An AlgoKit project that builds and deploys to LocalNet. If you don’t have one, work through Get Ready To Build and Your First Smart Contract first.
  • AlgoKit CLI installed and on your PATH.
  • An account you control to deploy from, with a mnemonic you can supply as an environment variable.

You have two options for reaching a public network.

Run your own node. You control availability, rate limits, and trust. This is the right choice for high-throughput production services and for anything where a third party seeing your traffic is unacceptable. It costs the hardware and ongoing operation of an algod node and an indexer, which the deploy needs to look up existing apps. See Algorand Node Types for hardware requirements and Indexer Installation for the indexer.

Use a hosted provider. Faster to start and free at low volume. Nodely runs free public algod and indexer endpoints, which AlgoKit uses by default.

AlgoKit loads a general .env file from your project directory first, then a network-specific .env.[network] file that overrides it. Network names are case-sensitive and match the argument you pass to the deploy command.

.
├── .algokit.toml # AlgoKit project configuration
├── .env # Shared across all networks (optional)
├── .env.testnet # TestNet overrides (optional)
└── .env.mainnet # MainNet overrides (optional)

These are the variables the deploy reads. localnet, testnet, and mainnet have default server URLs, so ALGOD_SERVER and INDEXER_SERVER are required only for other network names:

VariableRequiredDescription
ALGOD_SERVERCustom networks only. Ignored for testnet and mainnet unless exported in the shellBase URL of the algod endpoint, scheme included, e.g. https://testnet-api.4160.nodely.dev
ALGOD_PORTNoPort for algod. Omit for HTTPS endpoints on 443
ALGOD_TOKENNoAPI token for algod. Empty string for token-free public endpoints
INDEXER_SERVERCustom networks only. Ignored for testnet and mainnet unless exported in the shellBase URL of the indexer endpoint
INDEXER_PORTNoPort for indexer. Omit for HTTPS endpoints on 443
INDEXER_TOKENNoAPI token for indexer. Empty string for token-free public endpoints

For testnet and mainnet, AlgoKit applies its default ALGOD_SERVER and INDEXER_SERVER values after it reads the .env files. A server URL set in .env, .env.testnet, or .env.mainnet is replaced by the algonode.cloud default before your deploy command runs. AlgoKit replaces only those two variables. Tokens, ports, and every other variable from the .env files reach the deploy command unchanged.

Variables exported in the shell take precedence over both the .env files and the defaults. To deploy through Nodely’s current hostnames, a paid tier, a different provider, or your own node, export the server URLs:

export ALGOD_SERVER=https://testnet-api.4160.nodely.dev
export INDEXER_SERVER=https://testnet-idx.4160.nodely.dev
algokit project deploy testnet

In a continuous integration and continuous delivery (CI/CD) pipeline, set the same two variables in the pipeline’s environment.

Nodely’s free tier requires no token. A commercial provider gives you an API key for ALGOD_TOKEN and INDEXER_TOKEN, which can go in .env.[network] or be exported.

A network name other than localnet, testnet, or mainnet has no AlgoKit defaults. For a deploy such as algokit project deploy mynet, AlgoKit uses the values in .env.mynet as written, and fails if that file does not exist.

.algokit.toml defines what actually runs, and which variables are treated as secrets:

.algokit.toml
[project.deploy]
command = "poetry run python -m smart_contracts deploy"
environment_secrets = [
"DEPLOYER_MNEMONIC",
]
[project.deploy.localnet]
environment_secrets = []

Variables listed in environment_secrets are prompted for interactively if they aren’t already set in the environment. A network-specific [project.deploy.testnet] or [project.deploy.mainnet] section can override command and environment_secrets. Its environment_secrets list replaces the general list, so repeat DEPLOYER_MNEMONIC in it along with any new secret. A key the network section omits falls back to the general [project.deploy] section.

The deployer is the account that creates the application, and it becomes the application’s creator on chain. Name-based deployment resolves your app by creator address plus app name, so keep the same deployer for a given app across its lifetime. Deploying with a different account creates a second, unrelated application.

  1. Create an account and record its address and mnemonic. Any Algorand account works.

  2. Fund it.

    On TestNet, use the Lora dispenser at lora.algokit.io/testnet/fund, or the CLI:

    algokit dispenser login
    algokit dispenser fund \
    --receiver AAAQEAYEAUDAOCAJBIFQYDIOB4IBCEQTCQKRMFYYDENBWHA5DYP7MUPJQE \
    --amount 10 --whole-units

    On MainNet, fund the account with real ALGO from an exchange or an existing wallet. See Funding an Account for the full set of options.

  3. Supply the mnemonic as DEPLOYER_MNEMONIC. Never commit it. Keep it out of .env files that are tracked in git, and confirm .env* is covered by your .gitignore before you first run a deploy.

Run the deploy against the network you want:

algokit project deploy testnet

AlgoKit loads .env, then .env.testnet, then its default TestNet server URLs, then any variables exported in the shell. It prompts for any declared secrets that are still unset, then runs your deploy command against those settings. Swap testnet for mainnet when you’re ready for production.

An interactive mainnet deploy asks Warning: Proceed with MainNet deployment? before it runs. Answering n aborts the deploy.

In CI/CD pipelines there’s nobody to answer a prompt, so supply every secret as an environment variable and skip the prompts, including the MainNet confirmation:

algokit project deploy testnet --ci

AlgoKit’s deploy is idempotent, so running it again applies only what changed. On each run it queries indexer for applications created by the deployer address, matches yours by name, and then:

  • No existing app: creates it.
  • Logic unchanged: does nothing.
  • Logic changed: acts according to onUpdate: update in place, replace, append (create a new app, leaving the old one), or fail.
  • Schema grew: the app needs more global or local state, or more extra program pages, than it was created with. None of these can grow after creation. Acts according to onSchemaBreak: replace, append, or fail.

onUpdate and onSchemaBreak both default to fail. replace sends one atomic group that creates the new app and deletes the old one. If the old app is not deletable, the delete fails, the whole group fails, and neither app changes.

Because the lookup goes through indexer, INDEXER_SERVER must point at an indexer that has caught up with the network.

AlgoKit records the app’s name, version, updatability, and deletability in an Algorand Request for Comments 2 (ARC-2) transaction note on every create and update, which is how the next deploy finds it again:

ALGOKIT_DEPLOYER:j{"name":"MyApp","version":"1.0","updatable":true,"deletable":false}

Decide whether the MainNet app can be updated or deleted before its first MainNet deploy. The updatable and deletable deploy parameters set this on each deploy, so one codebase can ship an updatable app to TestNet and an immutable, permanent app to MainNet. Pass both explicitly in your MainNet deploy:

await factory.deploy({ updatable: false, deletable: false });

If you leave updatable unset and the contract references the TMPL_UPDATABLE template variable, AlgoKit Utils sets updatable to true whenever the contract handles UpdateApplication calls. deletable follows the same rule with TMPL_DELETABLE and DeleteApplication.

The parameters take effect only when the contract gates its update and delete handlers on the UPDATABLE and DELETABLE template variables. A contract with no update or delete handler is immutable and permanent whatever values you pass, because its approval program rejects those calls.

Your deploy command prints the application ID. Confirm it landed as expected:

  • Lora: open lora.algokit.io and switch to the matching network, then look the app up by ID to inspect its global state, schema, and creation transaction.
  • A block explorer: allo.info for MainNet and TestNet.

Check that the creator address is the deployer you intended, that global state holds the values your deploy set, and that the updatable and deletable flags in the ARC-2 note match what you meant to ship.

MainNet transactions cost real ALGO and cannot be reversed. An immutable, permanent app cannot be fixed after the fact. The only remedy is deploying a replacement and migrating users to it.

  • The contract has been exercised on TestNet against the same deploy path you’re about to run.
  • Updatability and permanence for MainNet are set explicitly in the deploy call.
  • The deployer account is dedicated to this purpose, its mnemonic lives in a secret manager, and no copy of it is in git history.
  • The deployer holds enough ALGO for the creation transaction, the application account’s minimum balance, and any funding the app needs to operate.
  • Global and local state schema is sized for what the app will need, since schema cannot grow after creation without replacing the app.
  • The contract has been reviewed, and audited if it will custody meaningful value.
  • You know which endpoint you’re deploying through and whether its rate limits suit your production traffic.