In this article, we will learn how to build a Blockchain based Certificate Verification DApp (Decentralized App) designed to provide a secure and transparent way to verify and validate certificates issued by educational institutions, organizations, or certification authorities. Built on the Ethereum blockchain, this DApp leverages smart contracts to ensure the integrity and authenticity of certificates. The goal of this project is to streamline the process of certificate generation and validation and ensure the integrity of the certificate by exploiting the transaction immutability property of blockchain. We will also store the digital PDF copy of the certificate on IPFS to make it easily accessible.
The tech stack for this entire project includes:
- Ganache-cli: As the local blockchain network
- Truffle: For smart contract development and deployment
- Streamlit: For developing the web application
- Pinata: As an IPFS client
- Docker: To containerize the entire application into different services
You can clone the project from this GitHub link.
Let’s now take a deeper dive into the application!
The system comprises of 2 main entities:
- Institute: Responsible for generating and issuing certificates. Has the functionality to generate and view certificates.
- Verifier: Responsible for verifying certificates. Has the functionality to verify certificates by either uploading a certificate pdf or by inputting the certificate id.
Before starting, there are some prerequisites that you have to download/setup.
- Node (21.0.0 or higher)
- Python (3.9.10 or higher)
- Docker
- Globally installed packages for truffle and ganache-cli
npm install -g truffle ganache-cli
Let’s start coding the application now!
Create a Project directory (Certificate Validation System)
Open this directory in command prompt and enter the command truffle init
to initialize this directory as our project directory.
Now you project directory should look like:
Certificate Validation System
|
│ truffle-config.js
│
├───contracts
│ .gitkeep
│
├───migrations
│ .gitkeep
│
└───test
.gitkeep
Let’s start writing our main smart contract now.
Create a file Certification.sol inside contracts:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Certification {
struct Certificate {
string uid;
string candidate_name;
string course_name;
string org_name;
string ipfs_hash;
}
mapping(string => Certificate) public certificates;
event certificateGenerated(string certificate_id);
function generateCertificate(
string memory _certificate_id,
string memory _uid,
string memory _candidate_name,
string memory _course_name,
string memory _org_name,
string memory _ipfs_hash
) public {
// Check if certificate with the given ID already exists
require(
bytes(certificates[_certificate_id].ipfs_hash).length == 0,
"Certificate with this ID already exists"
);
// Create the certificate
Certificate memory cert = Certificate({
uid: _uid,
candidate_name: _candidate_name,
course_name: _course_name,
org_name: _org_name,
ipfs_hash: _ipfs_hash
});
// Store the certificate in the mapping
certificates[_certificate_id] = cert;
// Emit an event
emit certificateGenerated(_certificate_id);
}
function getCertificate(
string memory _certificate_id
)
public
view
returns (
string memory _uid,
string memory _candidate_name,
string memory _course_name,
string memory _org_name,
string memory _ipfs_hash
)
{
Certificate memory cert = certificates[_certificate_id];
// Check if the certificate with the given ID exists
require(
bytes(certificates[_certificate_id].ipfs_hash).length != 0,
"Certificate with this ID does not exist"
);
// Return the values from the certificate
return (
cert.uid,
cert.candidate_name,
cert.course_name,
cert.org_name,
cert.ipfs_hash
);
}
function isVerified(
string memory _certificate_id
) public view returns (bool) {
return bytes(certificates[_certificate_id].ipfs_hash).length != 0;
}
}
- The contract defines a structure called
Certificate
with fields such as UID, candidate name, course name, organization name, and an IPFS hash. - The contract uses a mapping named
certificates
to store certificates. Certificates are indexed by a unique identifier (_certificate_id
). - An event named
certificateGenerated
is emitted whenever a new certificate is generated. This event can be used to track certificate creation.
Functions:
generateCertificate
: This function allows the creation of a new certificate. It checks if a certificate with the given ID already exists and, if not, creates a new certificate and stores it in the mapping. It also emits thecertificateGenerated
event.getCertificate
: This function retrieves the details of a certificate by its ID. It returns the UID, candidate name, course name, organization name, and IPFS hash associated with the certificate.isVerified
: This function checks if a certificate with a given ID exists (i.e., is verified) by checking the length of the IPFS hash associated with it. If the length is non-zero, the certificate is considered verified.
You can optionally create a Migrations.sol contract to keep track of all the deployed contracts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Migrations {
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) _;
}
constructor() {
owner = msg.sender;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}
Now, let’s write the migration scripts:
If you created the Migrations.sol contract, create this deployment script 1_initial_migration.js in the migrations folder else you can ignore this.
let Migrations = artifacts.require("./Migrations.sol");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
Create the main deployment script 2_deploy_contracts under migrations for deploy our Certification.sol contract.
let Certification = artifacts.require("./Certification.sol");
const fs = require('fs');
module.exports = async function (deployer) {
await deployer.deploy(Certification);
const deployedCertification = await Certification.deployed();
// Always start with an empty object for configData
let configData = {};
// Update or add the contract address
configData.Certification = deployedCertification.address;
// Save the updated configuration back to the file
fs.writeFileSync('./deployment_config.json', JSON.stringify(configData, null, 2));
console.log(`Certification contract deployed at address: ${deployedCertification.address}`);
};
We would like to dynamically fetch the deployed contract address instead of manually inputting it every time it is deployed, so we add a small part in the above code to write our deployed contract address to a deployment_config.json file in our project’s root directory.
Next, make the following changes in your truffle-config.js file:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*",
},
},
compilers: {
solc: {
version: "0.8.13",
},
},
};
Now that we are finished writing our smart contracts, let’s move on to the web application part.
In our project’s root directory, create a folder named application. This folder will contain the entire code for our streamlit app.
Create a requirements.txt
file in this directory.
pdfplumber==0.10.3
Pillow==10.1.0
Pyrebase4==4.7.1
python-dotenv==1.0.0
reportlab==4.0.7
requests==2.29.0
streamlit==1.28.2
streamlit_extras==0.3.5
web3==5.24.0
Install this requirements by opening this directory in command prompt and using the command pip install -r requirements.txt
.
For authenticating our users, we will use Firebase authentication service. Create a project on Firebase Console. Enable email/password sign in method under Authentication in the Build section. Go to project settings. Add new app. Note the following details in a .env file inside the project’s root directory.
FIREBASE_API_KEY
FIREBASE_AUTH_DOMAIN
FIREBASE_DATABASE_URL (Set this to "")
FIREBASE_PROJECT_ID
FIREBASE_STORAGE_BUCKET
FIREBASE_MESSAGING_SENDER_ID
FIREBASE_APP_ID
Create a db directory inside the application directory and inside it create a file firebase_app.py.
import pyrebase
from dotenv import load_dotenv
import os
load_dotenv()
config = {
"apiKey": os.getenv("FIREBASE_API_KEY"),
"authDomain": os.getenv("FIREBASE_AUTH_DOMAIN"),
"databaseURL": os.getenv("FIREBASE_DATABASE_URL"),
"projectId": os.getenv("FIREBASE_PROJECT_ID"),
"storageBucket": os.getenv("FIREBASE_STORAGE_BUCKET"),
"messagingSenderId": os.getenv("FIREBASE_MESSAGING_SENDER_ID"),
"appId": os.getenv("FIREBASE_APP_ID"),
}
firebase = pyrebase.initialize_app(config)
auth = firebase.auth()
def register(email, password):
try:
auth.create_user_with_email_and_password(email, password)
return "success"
except Exception as e:
print(f"Error: {e}")
return "failure"
def login(email, password):
try:
auth.sign_in_with_email_and_password(email, password)
return "success"
except Exception as e:
print(f"Error: {e}")
return "failure"
We also need a Pinata account for uploading our certificates onto IPFS. Create an account on Pinata. Go to the API keys section and generate a new key. Note the API key and secret key in .env file.
Finally, the .env file should contain:
PINATA_API_KEY = "<Your Pinata API key>"
PINATA_API_SECRET = "<Your Pinata Secret Key>"
FIREBASE_API_KEY = "<Your Firebase API key>"
FIREBASE_AUTH_DOMAIN = "<Your Firebase auth domain>"
FIREBASE_DATABASE_URL = ""
FIREBASE_PROJECT_ID = "<Your Firebase project id>"
FIREBASE_STORAGE_BUCKET = "<Your Firebase Storage Bucket>"
FIREBASE_MESSAGING_SENDER_ID = "<Your Firebase messaging sender id>"
FIREBASE_APP_ID = "<Your Firebase app id>"
institute_email = "institute@gmail.com" # Feel free to modify this
institute_password = "123456" # Feel free to modify this
We also define an institute_email
and institute_password
which will be specifically used for authenticating the institute.
Inside the application directory, create a connection.py file defining the connection to the ganache-cli.
import json
from pathlib import Path
from web3 import Web3
# Connect to a local Ethereum node
w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
def get_contract_abi():
certification_json_path = Path('../build/contracts/Certification.json')
try:
with open(certification_json_path, 'r') as json_file:
certification_data = json.load(json_file)
return certification_data.get('abi', [])
except FileNotFoundError:
print(f"Error: {certification_json_path} not found.")
return []
contract_abi = get_contract_abi()
deployment_config_fpath = Path("../deployment_config.json")
with open(deployment_config_fpath, 'r') as json_file:
address_data = json.load(json_file)
contract_address = address_data.get('Certification')
# Interact with the smart contract
contract = w3.eth.contract(address=contract_address, abi=contract_abi)
Now, create the main file for our streamlit application named app.py inside the application directory.
import streamlit as st
from PIL import Image
from utils.streamlit_utils import hide_icons, hide_sidebar, remove_whitespaces
from streamlit_extras.switch_page_button import switch_page
st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
hide_icons()
hide_sidebar()
remove_whitespaces()
st.title("Certificate Validation System")
st.write("")
st.subheader("Select Your Role")
col1, col2 = st.columns(2)
institite_logo = Image.open("../assets/institute_logo.png")
with col1:
st.image(institite_logo, output_format="jpg", width=230)
clicked_institute = st.button("Institute")
company_logo = Image.open("../assets/company_logo.jpg")
with col2:
st.image(company_logo, output_format="jpg", width=230)
clicked_verifier = st.button("Verifier")
if clicked_institute:
st.session_state.profile = "Institute"
switch_page('login')
elif clicked_verifier:
st.session_state.profile = "Verifier"
switch_page('login')
Now create a pages directory inside application directory. Add the following python files in the pages directory.
1. login.py
import streamlit as st
from db.firebase_app import login
from dotenv import load_dotenv
import os
from streamlit_extras.switch_page_button import switch_page
from utils.streamlit_utils import hide_icons, hide_sidebar, remove_whitespaces
st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
hide_icons()
hide_sidebar()
remove_whitespaces()
load_dotenv()
form = st.form("login")
email = form.text_input("Enter your email")
password = form.text_input("Enter your password", type="password")
if st.session_state.profile != "Institute":
clicked_register = st.button("New user? Click here to register!")
if clicked_register:
switch_page("register")
submit = form.form_submit_button("Login")
if submit:
if st.session_state.profile == "Institute":
valid_email = os.getenv("institute_email")
valid_pass = os.getenv("institute_password")
if email == valid_email and password == valid_pass:
switch_page("institute")
else:
st.error("Invalid credentials!")
else:
result = login(email, password)
if result == "success":
st.success("Login successful!")
switch_page("verifier")
else:
st.error("Invalid credentials!")
2. register.py
import streamlit as st
from db.firebase_app import register
from streamlit_extras.switch_page_button import switch_page
from utils.streamlit_utils import hide_icons, hide_sidebar, remove_whitespaces
st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
hide_icons()
hide_sidebar()
remove_whitespaces()
form = st.form("login")
email = form.text_input("Enter your email")
password = form.text_input("Enter your password", type="password")
clicked_login = st.button("Already registered? Click here to login!")
if clicked_login:
switch_page("login")
submit = form.form_submit_button("Register")
if submit:
result = register(email, password)
if result == "success":
st.success("Registration successful!")
if st.session_state.profile == "Institute":
switch_page("institute")
else:
switch_page("verifier")
else:
st.error("Registration unsuccessful!")
3. institute.py
import streamlit as st
import requests
import json
import os
from dotenv import load_dotenv
import hashlib
from utils.cert_utils import generate_certificate
from utils.streamlit_utils import view_certificate
from connection import contract, w3
from utils.streamlit_utils import hide_icons, hide_sidebar, remove_whitespaces
st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
hide_icons()
hide_sidebar()
remove_whitespaces()
load_dotenv()
api_key = os.getenv("PINATA_API_KEY")
api_secret = os.getenv("PINATA_API_SECRET")
def upload_to_pinata(file_path, api_key, api_secret):
# Set up the Pinata API endpoint and headers
pinata_api_url = "https://api.pinata.cloud/pinning/pinFileToIPFS"
headers = {
"pinata_api_key": api_key,
"pinata_secret_api_key": api_secret,
}
# Prepare the file for upload
with open(file_path, "rb") as file:
files = {"file": (file.name, file)}
# Make the request to Pinata
response = requests.post(pinata_api_url, headers=headers, files=files)
# Parse the response
result = json.loads(response.text)
if "IpfsHash" in result:
ipfs_hash = result["IpfsHash"]
print(f"File uploaded to Pinata. IPFS Hash: {ipfs_hash}")
return ipfs_hash
else:
print(f"Error uploading to Pinata: {result.get('error', 'Unknown error')}")
return None
options = ("Generate Certificate", "View Certificates")
selected = st.selectbox("", options, label_visibility="hidden")
if selected == options[0]:
form = st.form("Generate-Certificate")
uid = form.text_input(label="UID")
candidate_name = form.text_input(label="Name")
course_name = form.text_input(label="Course Name")
org_name = form.text_input(label="Org Name")
submit = form.form_submit_button("Submit")
if submit:
pdf_file_path = "certificate.pdf"
institute_logo_path = "../assets/logo.jpg"
generate_certificate(pdf_file_path, uid, candidate_name, course_name, org_name, institute_logo_path)
# Upload the PDF to Pinata
ipfs_hash = upload_to_pinata(pdf_file_path, api_key, api_secret)
os.remove(pdf_file_path)
data_to_hash = f"{uid}{candidate_name}{course_name}{org_name}".encode('utf-8')
certificate_id = hashlib.sha256(data_to_hash).hexdigest()
# Smart Contract Call
contract.functions.generateCertificate(certificate_id, uid, candidate_name, course_name, org_name, ipfs_hash).transact({'from': w3.eth.accounts[0]})
st.success(f"Certificate successfully generated with Certificate ID: {certificate_id}")
else:
form = st.form("View-Certificate")
certificate_id = form.text_input("Enter the Certificate ID")
submit = form.form_submit_button("Submit")
if submit:
try:
view_certificate(certificate_id)
except Exception as e:
st.error("Invalid Certificate ID!")
4. verifier.py
import streamlit as st
import os
import hashlib
from utils.cert_utils import extract_certificate
from utils.streamlit_utils import view_certificate
from connection import contract
from utils.streamlit_utils import displayPDF, hide_icons, hide_sidebar, remove_whitespaces
st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
hide_icons()
hide_sidebar()
remove_whitespaces()
options = ("Verify Certificate using PDF", "View/Verify Certificate using Certificate ID")
selected = st.selectbox("", options, label_visibility="hidden")
if selected == options[0]:
uploaded_file = st.file_uploader("Upload the PDF version of the certificate")
if uploaded_file is not None:
bytes_data = uploaded_file.getvalue()
with open("certificate.pdf", "wb") as file:
file.write(bytes_data)
try:
(uid, candidate_name, course_name, org_name) = extract_certificate("certificate.pdf")
displayPDF("certificate.pdf")
os.remove("certificate.pdf")
# Calculating hash
data_to_hash = f"{uid}{candidate_name}{course_name}{org_name}".encode('utf-8')
certificate_id = hashlib.sha256(data_to_hash).hexdigest()
# Smart Contract Call
result = contract.functions.isVerified(certificate_id).call()
if result:
st.success("Certificated validated successfully!")
else:
st.error("Invalid Certificate! Certificate might be tampered")
except Exception as e:
st.error("Invalid Certificate! Certificate might be tampered")
elif selected == options[1]:
form = st.form("Validate-Certificate")
certificate_id = form.text_input("Enter the Certificate ID")
submit = form.form_submit_button("Validate")
if submit:
try:
view_certificate(certificate_id)
# Smart Contract Call
result = contract.functions.isVerified(certificate_id).call()
if result:
st.success("Certificated validated successfully!")
else:
st.error("Invalid Certificate ID!")
except Exception as e:
st.error("Invalid Certificate ID!")
After this, create a utils directory inside application directory and add the following files.
1. cert_utils.py
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
import pdfplumber
def generate_certificate(output_path, uid, candidate_name, course_name, org_name, institute_logo_path):
# Create a PDF document
doc = SimpleDocTemplate(output_path, pagesize=letter)
# Create a list to hold the elements of the PDF
elements = []
# Add institute logo and institute name
if institute_logo_path:
logo = Image(institute_logo_path, width=150, height=150)
elements.append(logo)
# Add institute name
institute_style = ParagraphStyle(
"InstituteStyle",
parent=getSampleStyleSheet()["Title"],
fontName="Helvetica-Bold",
fontSize=15,
spaceAfter=40,
)
institute = Paragraph(org_name, institute_style)
elements.extend([institute, Spacer(1, 12)])
# Add title
title_style = ParagraphStyle(
"TitleStyle",
parent=getSampleStyleSheet()["Title"],
fontName="Helvetica-Bold",
fontSize=25,
spaceAfter=20,
)
title1 = Paragraph("Certificate of Completion", title_style)
elements.extend([title1, Spacer(1, 6)])
# Add recipient name, UID, and course name with increased line space
recipient_style = ParagraphStyle(
"RecipientStyle",
parent=getSampleStyleSheet()["BodyText"],
fontSize=14,
spaceAfter=6,
leading=18,
alignment=1
)
recipient_text = f"This is to certify that<br/><br/>\
<font color='red'> {candidate_name} </font><br/>\
with UID <br/> \
<font color='red'> {uid} </font> <br/><br/>\
has successfully completed the course:<br/>\
<font color='blue'> {course_name} </font>"
recipient = Paragraph(recipient_text, recipient_style)
elements.extend([recipient, Spacer(1, 12)])
# Build the PDF document
doc.build(elements)
print(f"Certificate generated and saved at: {output_path}")
def extract_certificate(pdf_path):
with pdfplumber.open(pdf_path) as pdf:
# Extract text from each page
text = ""
for page in pdf.pages:
text += page.extract_text()
lines = text.splitlines()
org_name = lines[0]
candidate_name = lines[3]
uid = lines[5]
course_name = lines[-1]
return (uid, candidate_name, course_name, org_name)
2. streamlit_utils.py
import streamlit as st
import base64
import requests
import os
from connection import contract
def displayPDF(file):
# Opening file from file path
with open(file, "rb") as f:
base64_pdf = base64.b64encode(f.read()).decode('utf-8')
# Embedding PDF in HTML
pdf_display = F'<iframe src="data:application/pdf;base64,{base64_pdf}" width="700" height="1000" type="application/pdf"></iframe>'
# Displaying File
st.markdown(pdf_display, unsafe_allow_html=True)
def view_certificate(certificate_id):
# Smart Contract Call
result = contract.functions.getCertificate(certificate_id).call()
ipfs_hash = result[4]
pinata_gateway_base_url = 'https://gateway.pinata.cloud/ipfs'
content_url = f"{pinata_gateway_base_url}/{ipfs_hash}"
response = requests.get(content_url)
with open("temp.pdf", 'wb') as pdf_file:
pdf_file.write(response.content)
displayPDF("temp.pdf")
os.remove("temp.pdf")
def hide_icons():
hide_st_style = """
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>"""
st.markdown(hide_st_style, unsafe_allow_html=True)
def hide_sidebar():
no_sidebar_style = """
<style>
div[data-testid="stSidebarNav"] {visibility: hidden;}
</style>"""
st.markdown(no_sidebar_style, unsafe_allow_html=True)
def remove_whitespaces():
st.markdown("""
<style>
.css-18e3th9 {
padding-top: 0rem;
padding-bottom: 10rem;
padding-left: 5rem;
padding-right: 5rem;
}
.css-1d391kg {
padding-top: 3.5rem;
padding-right: 1rem;
padding-bottom: 3.5rem;
padding-left: 1rem;
}
</style>""", unsafe_allow_html=True)
We are done creating the streamlit application.
Finally, create an assets directory and add company_logo.png, institute_logo.png and logo.jpg images in it.
Finally, the project directory structure should look like:
Certificate Validation System
│
│ docker-compose.yml
│ Dockerfile.ganache
│ Dockerfile.streamlit
│ truffle-config.js
│
├───application
│ │ app.py
│ │ connection.py
│ │ requirements.txt
│ │
│ ├───db
│ │ firebase_app.py
│ │
│ ├───pages
│ │ institute.py
│ │ login.py
│ │ register.py
│ │ verifier.py
│ │
│ ├───utils
│ cert_utils.py
│ streamlit_utils.py
│
├───assets
│ company_logo.jpg
│ institute_logo.png
│ logo.jpg
│
├───build
│ └───contracts
│ Certification.json
│ Migrations.json
│
├───contracts
│ Certification.sol
│ Migrations.sol
│
└───migrations
1_initial_migration.js
2_deploy_contracts.js
Now, our DApp is ready!
To run it, first open a terminal and enter the command:
ganache-cli -h 127.0.0.1 -p 8545
This command will start the ganache-cli.
Next, to deploy our contracts on ganache-cli, open command prompt in our project’s root directory and enter the following command:
truffle migrate
Now, to finally start the streamlit app, open the application directory inside our project in command prompt and enter the command:
streamlit run app.py
You can now view the app on your browser running on localhost:8501.
Streamlit application screenshots:
Now, to containerize our application, we use docker.
We create 2 Dockerfiles (inside our project’s root directory).
1. Dockerfile.ganache: For running the ganache blockchain.
FROM node:16
WORKDIR /app
RUN npm install -g ganache-cli
RUN npm install -g truffle
CMD ["bash", "-c", "ganache-cli -h ganache & sleep 5 && truffle migrate && tail -f /dev/null"]
2. Dockerfile.streamlit: For running the streamlit web application.
FROM python:3.9.10
WORKDIR /app
COPY . /app
WORKDIR /app/application
RUN pip install -r requirements.txt
EXPOSE 8501
CMD ["streamlit", "run", "app.py"]
Finally, create a docker-compose.yml file in our project’s root directory. This will help us to easily build the docker images for our containers from Dockerfiles and it will also help us to quickly start and stop our services using docker-compose up
and docker-compose down
.
version: '3'
services:
ganache:
image: sahil1810/cvs-ganache
build:
context: .
dockerfile: Dockerfile.ganache
ports:
- "8545:8545"
volumes:
- blockchain_data:/app
streamlit-app:
image: sahil1810/cvs-streamlitapp
build:
context: .
dockerfile: Dockerfile.streamlit
ports:
- "8501:8501"
depends_on:
- ganache
volumes:
- blockchain_data:/app
volumes:
blockchain_data:
Before building the images, first make the below changes in application/connection.py and truffle-config.js:
In application/connection.py, on line 6:
w3 = Web3(Web3.HTTPProvider('http://ganache:8545'))
In truffle-config.js, on line 4:
host: "ganache",
For building the images, open command prompt in the project’s root directory and enter the command:
docker-compose build
You only have to build the images once.
For starting the containers, simply use:
docker-compose up
This will start the ganache and streamlit services. Go to your browser and visit localhost:8501
to view the streamlit application.
To stop the containers, use the command:
docker-compose down
Note: In the docker-compose file, I have included the image references for ganache and streamlit from docker hub. So you can directly start the containers using docker-compose up
without the build step. The first time you execute this command, it will fetch the images from docker hub and from next time it will use the locally fetched images.
Hope you liked this in-depth tutorial. Please comment if you have any queries or suggestions!