How to Deploy and Verify Smart Contracts Locally with Hardhat & Docker
Published October 15, 2026 · Editorial policy

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.xLTS
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, replace127.0.0.1withhost.docker.internalin yourhardhat.config.tsnetwork 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.tslists 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 lowerrors. - 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 withnpx hardhat cleanand redeploy.


