Web3

How to Deploy and Verify Smart Contracts Locally with Hardhat & Docker

Tech Setup4 min read
TS

Tech Setup

Published October 15, 2026 · Editorial policy

How to Deploy and Verify Smart Contracts Locally with Hardhat & Docker

Testing smart contracts against a public testnet like Sepolia means waiting on block times, managing faucet drips for test ETH, and dealing with flaky RPC endpoints. You can bypass all of that by spinning up a local Ethereum node inside a Docker container, deploying your contracts with Hardhat, and programmatically verifying them via Etherscan-compatible APIs before your code ever touches a live network.

By the end of this guide, you will have a fully containerized local blockchain, a Hardhat workspace configured to deploy to it, and an automated verification script that runs in seconds.

The Stack at a Glance

Before writing code, let's align on the exact toolchain versions to avoid common peer-dependency mismatches.

  • Hardhat: v2.22.x
  • Hardhat-Deploy: v0.12.x
  • Docker Engine: v24.x+ (with Compose V2)
  • Node.js: v20.x LTS

Quick TL;DR Fix

If you are in a rush and just want the orchestration running, clone your project structure and execute:

docker compose up -d
npx hardhat deploy --network localhost

If the deployment throws a connect ECONNREFUSED 127.0.0.1:8545 error, your Hardhat network config is pointing to the host machine while your Hardhat task is running inside a container, or vice-versa. Ensure your local Hardhat commands execute on the host machine while hitting the Dockerized RPC port 8545.


Step 1: Containerize Anvil (Foundry) or Hardhat Node

While Hardhat comes with a built-in node (npx hardhat node), running it inside Docker gives you an isolated, ephemeral environment that you can spin down and wipe with a single command. We will use foundry's anvil binary inside a lightweight Rust container because it boots instantly and handles state resets more reliably than the default Hardhat node, while remaining 100% JSON-RPC compatible.

Create a docker-compose.yml file in your project root:

version: '3.8'

services:
  local-node:
    image: ghcr.io/foundry-rs/foundry:latest
    container_name: local-evm-node
    command: anvil --host 0.0.0.0 --port 8545 --chain-id 31337
    ports:
      - "8545:8545"
    restart: unless-stopped

Spin up the container in detached mode:

docker compose up -d

Verify the container is healthy and responding to JSON-RPC calls by querying its block number:

curl -X POST \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id:1}' \
  http://localhost:8545

Expected Output:

{"jsonpx":"2.0","id":1,"result":"0x0"}

If the result returns 0x0 (or the current hex block), your local EVM node is up and accepting transactions.


Step 2: Configure the Hardhat Workspace

Initialize a fresh Hardhat project in your workspace directory if you haven't already:

npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

Choose Create a TypeScript project and accept the default paths.

Next, open hardhat.config.ts and replace its contents with the configuration below. This explicitly sets up the localhost network to point to our Docker container's RPC port, uses chain ID 31337, and injects a deterministic test private key provided by Anvil:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

// Anvil's default first account private key
const TEST_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

const config: HardhatUserConfig = {
  solidity: "0.8.24",
  networks: {
    hardhat: {
      chainId: 31337,
    },
    localhost: {
      url: "http://127.0.0.1:8545",
      accounts: [TEST_PRIVATE_KEY],
      chainId: 31337,
    },
  },
};

export default config;

Create a sample smart contract under contracts/Vault.sol to deploy:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Vault {
    address public owner;
    uint256 public balance;

    event Deposited(address indexed sender, uint256 amount);

    constructor() {
        owner = msg.sender;
    }

    function deposit() external payable {
        require(msg.value > 0, "Zero deposit");
        balance += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external {
        require(msg.sender == owner, "Not owner");
        require(amount <= balance, "Insufficient balance");
        balance -= amount;
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "Transfer failed");
    }
}

Compile the contract to ensure there are no syntax errors:

npx hardhat compile

Expected Output:

Compiled 1 Solidity file successfully (evm target: paris).

Step 3: Write and Execute the Deployment Script

Create a deployment script inside scripts/deploy.ts. This script will deploy the Vault contract to our Dockerized node and log the deployed address.

import { ethers } from "hardhat";

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log(`Deploying contracts with the account: ${deployer.address}`);

  const vault = await ethers.deployContract("Vault");

  await vault.waitForDeployment();

  console.log(`Vault deployed to: ${await vault.getAddress()}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run the deployment script against the localhost network:

npx hardhat run scripts/deploy.ts --network localhost

Expected Output:

Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Vault deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

Check your Docker container logs (docker logs local-evm-node) to confirm the transaction was mined instantly.


Step 4: Verify Contracts Locally

Contract verification is typically associated with Etherscan, but testing verification workflows locally is critical for CI/CD pipelines. To verify contracts locally without hitting public APIs, you can run a local block explorer backend or use Hardhat's internal artifact matching checks.

However, if you are testing programmatic verification workflows (such as integrating with Sourcify or custom block explorers like Blockscout in your Docker stack), you configure Hardhat's etherscan plugin block.

Install the required verification plugins if not already present:

npm install --save-dev @nomicfoundation/hardhat-verify

Update your hardhat.config.ts to include the custom verification target pointing to a local Blockscout or Sourcify instance if running one, or use Sourcify's decentralized verification API which supports local chain validation:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@nomicfoundation/hardhat-verify";

const TEST_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

const config: HardhatUserConfig = {
  solidity: "0.8.24",
  networks: {
    localhost: {
      url: "http://127.0.0.1:8545",
      accounts: [TEST_PRIVATE_KEY],
      chainId: 31337,
    },
  },
  sourcify: {
    enabled: true
  }
};

export default config;

To verify your contract against Sourcify (which runs verification locally via bytecode matching against published compiler metadata), run:

npx hardhat verify --network localhost <DEPLOYED_VAULT_ADDRESS>

(Note: Replace <DEPLOYED_VAULT_ADDRESS> with the actual address output from Step 3, e.g., 0x5FbDB2315678afecb367f032d93F642f64180aa3)

If you are running a fully local Blockscout container alongside Anvil in your docker-compose.yml, configure custom etherscan custom chains in hardhat.config.ts:

etherscan: {
  apiKey: {
    localChain: "empty"
  },
  customChains: [
    {
      network: "localChain",
      chainId: 31337,
      urls: {
        apiURL: "http://localhost:4000/api",
        browserURL: "http://localhost:4000"
      }
    }
  ]
}

Troubleshooting Common Failures

1. Connection Refused (ECONNREFUSED)

  • Symptom: Hardhat throws an error stating it cannot connect to 127.0.0.1:8545.
  • Fix: Ensure your Docker container is actually running (docker ps). If you are running Hardhat commands inside another Docker container rather than your host machine, replace 127.0.0.1 with host.docker.internal in your hardhat.config.ts network URL.

2. Invalid Account / Insufficient Funds

  • Symptom: Transactions fail with sender doesn't have enough funds to send tx.
  • Fix: Anvil and Hardhat nodes pre-fund deterministic accounts with 10,000 ETH. Ensure your hardhat.config.ts lists the correct private key matching the pre-funded accounts array, and that you aren't accidentally pointing to a live network config.

3. Nonce Too Low

  • Symptom: Deployment transactions hang or throw nonce too low errors.
  • Fix: If you restarted your Docker container (docker compose restart), the local blockchain state and nonce counters were wiped, but your local Hardhat cache might still expect incremented nonces. Clear the local cache with npx hardhat clean and redeploy.