Setting Up a StarkNet Dev Environment with Docker

David Barreto
Starknet Edu
Published in
13 min readNov 22, 2022

TL;DR; To create a StarkNet dev environment that encapsulates project and global dependencies, you’ll need to have installed Docker Desktop, VSCode, the VSCode extension Dev Containers, a Dockerfile and a .devcontainer.json file, both of them at the root of your project folder.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev build-base nodejs npm git zsh curl
RUN sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
RUN python -m pip install --upgrade pip
RUN pip install cairo-lang openzeppelin-cairo-contracts
WORKDIR /app

.devcontainer.json

{
"name": "starknet-dev",
"build": {
"dockerfile": "Dockerfile",
"context": "."
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"Starkware.cairo",
"esbenp.prettier-vscode"
]
}
},
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh"
}
}

To re-launch the IDE inside the container, open the project on VSCode, go to “View → Command Palette” and run the command “Dev Containers: Rebuild and Reopen in Container”.

You can find the code on Github.

The Problem We Want to Solve

In a previous article we discussed how to create a simple dev environment for StarkNet using Python’s virtual environment (venv) to encapsulate dependencies at the project level. This guarantees that when your teammates clone your repository they will all end up with the exact same dependencies for your Cairo project and that they are able to manage multiple projects (Cairo or other) that use different versions of the same Python packages without collusion.

However, one important thing was left out from the project encapsulation: Python’s binary. Depending on the version of Cairo that your project uses, you might need a different version of Python. Right now, Cairo requires the use of Python 3.9. Using the latest version of Python (3.10) actually causes problems when trying to compile a smart contract written in Cairo.

With the simplistic dev environment mentioned above your only option to “guarantee” that all the developers in your team would use the right version of Python for your Cairo project is documentation. This creates a new problem as the steps to install a particular version of Python differ depending on your OS and for some of them is surprisingly challenging (try getting Python 3.9 with the right version of pip on Ubuntu, ugh…).

Your global dependencies will increase over time as the complexity of your Cairo project grows. NodeJS is another common global dependency for a Cairo project that is required if your team is building a decentralized application with a Web UI or a Mobile UI using React Native. Same problem as with the Python binary, how do you guarantee that all your developers have the same version of Node globally installed? How much effort do you put into documenting the installation for all the OS your teammates might use?

Our simple dev environment fails at encapsulating these global dependencies. Luckily we can use Docker to fully encapsulate an OS, global dependencies and project dependencies.

Creating a Docker Image for Python

The first step that you and your teammates will need to do is to install and run Docker Desktop. To verify that Docker is running correctly, you can interact with its CLI.

$ docker --version
>>>
Docker version 20.10.20, build 9fdeb9c

Now that we verified that it’s working, we can create a folder to work on. I’m calling it starknet-project but you can call it differently, it doesn’t matter.

$ mkdir starknet-project && cd starknet-project

Once inside the project, we can start defining our Docker image by creating the file Dockerfile that should live at the root of our project.

$ touch Dockerfile

Dockerfile

FROM python:3.9-alpine

I chose to base my image from Python’s official image for version 3.9 which uses Alpine Linux under the hood. Alpine Linux is commonly used as the base OS for Docker images because it has the smallest footprint of all Linux distros.

Besides image size, another reason to use Python’s official image instead of using Ubuntu as a base and adding Python to the image is convenience. As mentioned before, it is surprisingly difficult to install an “older” version of Python (3.9) on Ubuntu in a way that is compatible with pip.

To fire up our dev environment we will need first to build the image with the following command.

$ docker build -t starknet-dev .

In the above command we are instructing Docker to build an image from a Dockerfile present in the current directory (.) and that we are tagging (naming) as “starknet-dev”.

To verify that the image has been created and that it is in fact lightweight we can run the command below.

$ docker image ls
>>>
REPOSITORY TAG IMAGE ID CREATED SIZE
starknet-dev latest b8a48e9d8f39 2 days ago 47.4MB

We can now log into our dev environment by creating a container from our image.

$ docker run -it --rm starknet-dev sh
>>>
#

(Note: From this point on, every time you see “$ used as the terminal symbol it means that we are in the host machine. Conversely, when you see the symbol “#” it means that we are inside the container.)

In the command above we ran a container (docker run) in interactive mode (-it) from the image we just created (starknet-dev) and activate the terminal (sh). To avoid bloating our filesystem with unused containers, we also instructed Docker to destroy the container ( — rm) when we stop using it.

Now that we are inside the container we can verify that python 3.9 and pip are installed.

# python --version
>>>
Python 3.9.15

# pip --version
>>>
pip 22.0.4 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)

To exit our dev environment we can simply type exit inside the container and we will be back to our host OS.

# exit
>>>
$

Now that we manage to encapsulate Python’s binary using Docker we can move on to encapsulate Cairo dependencies.

Installing Cairo Dependencies

Besides Python’s binary, there’s another global dependency required by any Cairo project that we need to add to our project, gmp. On Alpine Linux, this package can be found as gmp-dev and we can install it using the apk package manager.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev

We are ready now to install cairo-lang as a project dependency using pip.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev
RUN pip install cairo-lang

If we try to rebuild the image from our modified Dockerfile we get an error.

$ docker build -t starknet-dev .
>>>
...
python setup.py bdist_wheel did not run successfully.
...

After some tinkering, I found out that the error goes away if I update pip to its latest version before attempting to install cairo-lang.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev
RUN python -m pip install --upgrade pip
RUN pip install cairo-lang

Attempting to build this image gives us a new type of error (that’s progress believe it or not).

$ docker build -t starknet-dev .
>>>
...
Target does not support gcc
...

The problem is that Alpine Linux doesn’t come with the gcc compiler out of the box. To install the compiler, we need to install the Alpine Linux package build-base.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev build-base
RUN python -m pip install --upgrade pip
RUN pip install cairo-lang

This time the image builds successfully although with a warning saying that we shouldn’t run pip as the root user. This warning makes sense when using pip directly in the host OS but we can safely ignore it when running pip inside a container as Docker gives us full isolation from the rest of the system.

To verify that Cairo’s CLI is now enabled, we start the container and query its version.

$ docker run -it --rm starknet-dev sh
# starknet-compile --version
>>>
starknet-compile 0.10.2

Our container is now Cairo aware.

Compiling a Cairo Contract

The next test we can perform to verify that all dependencies are working as expected is to compile a Cairo smart contract inside the container.

First, I’ll exit the container to go back to my host machine and create three directories in my source code.

$ mkdir contracts compiled abis

With these directories, our folder structure looks as follows:

$ tree .
>>>
.
├── Dockerfile
├── abis
├── compiled
└── contracts

Next, I’m going to head over to OpenZeppelin’s Cairo Wizard and grab the code for creating a simple ERC20 token.

contracts/ERC20.cairo

%lang starknet

from starkware.cairo.common.cairo_builtins import HashBuiltin
from starkware.cairo.common.uint256 import Uint256

from openzeppelin.token.erc20.library import ERC20

@constructor
func constructor{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
ERC20.initializer('MyToken', 'MTK', 18);
return ();
}

...

As you can see by the imports of the smart contracts, we will need to add open zeppelin’s Cairo library to our Docker image.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev build-base
RUN python -m pip install --upgrade pip
RUN pip install cairo-lang openzeppelin-cairo-contracts

We then rebuild our image and run a container.

$ docker build -t starknet-dev .
$ docker run -it --rm starknet-dev sh
>>>
#

If we list the files inside the container we realize two important things:

  1. We are at the root of the file system of the container
  2. We can’t see the files from our host system
# ls
>>>
drwxr-xr-x 1 root root 4096 Nov 18 00:48 bin
drwxr-xr-x 5 root root 360 Nov 18 01:17 dev
drwxr-xr-x 1 root root 4096 Nov 18 01:17 etc
drwxr-xr-x 2 root root 4096 Nov 11 18:03 home
drwxr-xr-x 1 root root 4096 Nov 18 00:48 lib
...

In order to compile our first Cairo smart contract inside the container, we need to create a directory for our project and make the source code of our project available to the container.

Making the Source Code Visible to the Container

To solve the first problem of creating a working directory inside the container for our project we can modify our image.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev build-base
RUN python -m pip install --upgrade pip
RUN pip install cairo-lang openzeppelin-cairo-contracts
WORKDIR /app

As always, everytime we modify our Dockerfile, we need to rebuild the image so the changes take place.

$ docker build -t starknet-dev .

If you are wondering why rebuilding the image this time was so much faster than before it is because Docker caches the result of executing every instruction of your Dockerfile (“RUN” for example) as stacked layers. To learn more about how Docker caching mechanism works, head over to the docs.

To make the source code visible inside the container, we will need to define a volume that will mount our current directory on our host computer, to the working directory inside the container (/app).

The volume is defined when running the container with the flag -v and the required mapping.

$ docker run -it --rm -v "$PWD":/app starknet-dev sh

Once inside the container we can verify that the container has access to our project source code in the host machine by listing all the files.

# ls -l
>>>
total 4
-rw-r--r-- 1 root root 176 Nov 18 01:22 Dockerfile
drwxr-xr-x 2 root root 64 Nov 18 01:00 abis
drwxr-xr-x 2 root root 64 Nov 18 00:58 compiled
drwxr-xr-x 3 root root 96 Nov 18 00:59 contracts

We have in theory everything we need to compile our smart contract inside the container.

# starknet-compile contracts/ERC20.cairo \
--output compiled/ERC20.json --abi abis/ERC20.json

We can tell that it worked because now we have the compiled and the abi file in their respective folder.

$ tree .
>>>
.
├── Dockerfile
├── abis
│ └── ERC20.json
├── compiled
│ └── ERC20.json
└── contracts
└── ERC20.cairo

Adding NodeJS and Typescript

As we mentioned in the introduction, if you are building a decentralized application on StarkNet, chances are that you will need NodeJS, Typescript and starknet.js.

We can make NodeJS available to our image by installing the Alpine Linux packages nodejs and npm.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev build-base nodejs npm
RUN python -m pip install --upgrade pip
RUN pip install cairo-lang openzeppelin-cairo-contracts
WORKDIR /app

The version of Alpine Linux used by the Python base image (alpine:3.16), has version 16 as the latest version of NodeJS available for this Linux distro as we can see below.

# node --version
>>>
v16.17.1

# npm --version
>>>
8.10.0

With NodeJS available inside our container we can proceed to install the javascript dependencies of our project but not before we create our configuration file.

package.json

{
"private": true
}

Because we are not planning to ever publish this package to npm, that’s all we really need in our configuration file.

Inside our container we can now install the required javascript libraries.

# npm install starknet typescript ts-node

We have included the package ts-node so we can directly execute Typescript files without having to explicitly transpile them to Javascript before executing them.

As with any Typescript project, we need to create a configuration file for the language.

tsconfig.json

{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"preserveConstEnums": true
},
"include": ["scripts/**/*"]
}

Instead of manually configuring all the options for a NodeJS project that uses Typescript, we are relying on a base configuration file explicitly made for NodeJS 16. This is of course an npm package that we need to add to our project.

# npm install @tsconfig/node16

With all the packages installed, our configuration file should look like the one below.

package.json

{
"private": true,
"dependencies": {
"@tsconfig/node16": "^1.0.3",
"starknet": "^4.10.0",
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
}
}

In the tsconfig.json file we have informed the Typescript compiler that we only ever plan to use the folder scripts to create Typescript files. So let’s create a sample file to verify that everything is working correctly and that we can read data from StarkNet.

scripts/test.ts

import { Provider } from 'starknet';

const provider = new Provider({
sequencer: {
network: 'goerli-alpha',
},
});

const main = async () => {
const chainId = await provider.getChainId();
console.log(chainId);
};

main();

Here we are just verifying that we are able to read the chain id of StarkNet’s testnet. We can execute our test file by running:

# npx ts-node scripts/test.ts
>>>
0x534e5f474f45524c49

Everything is working as expected.

At this point, our folder structure should look like this (showing only top level files and folders for readability):

$ tree . -L 1
>>>
.
├── Dockerfile
├── abis
├── compiled
├── contracts
├── node_modules
├── package-lock.json
├── package.json
├── scripts
└── tsconfig.json

Helping VSCode Resolve Cairo Dependencies

VSCode is able to resolve Javascript dependencies because it’s able to read the contents of the node_modules folder that it’s available both in the host computer and the container thanks to the Docker volume we defined.

On the other hand, our IDE is not able to resolve Cairo and Python dependencies because these packages are only visible inside the container, not on the host computer.

VSCode failing to resolve Cairo dependencies

To overcome this limitation, we will need to install a VSCode extension called Dev Containers. With this extension we can instruct the editor to build a modified version of our image that it’s compatible with the IDE and to launch the editor “inside” the container so all dependencies are visible.

For this extension to work, we need to create a special configuration file at the root of our project that is picked up by the Dev Container extension.

$ touch .devcontainer.json

.devcontainer.json

{
"name": "starknet-dev",
"build": {
"dockerfile": "Dockerfile",
"context": "."
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"Starkware.cairo"
]
}
},
"settings": {
"terminal.integrated.defaultProfile.linux": "sh"
}
}

Here we are informing the extension where our Dockerfile is located, which extensions to load in the editor when launched inside the container and what our preferred terminal is.

We can now instruct the editor to use our image by going to “View → Command Palette” and running the command “Dev Containers: Rebuild and Reopen in Container”.

Launching VSCode inside the container

By default, the Dev Containers extension mounts our source code as a volume and creates a working directory that’s different from the one we defined.

After the image finishes building, if we open an integrated terminal in the editor we can see the following:

076cd9df12d8:/workspaces/starknet-project#

Note that when opening our sample ERC20 Cairo smart contract file, VSCode is not complaining about dependencies as it’s able to resolve them.

No red squiggly lines

Improving the Integrated Terminal

Shell (sh) is a rather basic terminal not suitable for modern development. We can instead install oh-my-zsh which is in turn based on zsh. Git is another basic tool that we should go ahead and add as well.

Dockerfile

FROM python:3.9-alpine
RUN apk add --update gmp-dev build-base nodejs npm zsh git curl
RUN sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
RUN python -m pip install --upgrade pip
RUN pip install cairo-lang openzeppelin-cairo-contracts
WORKDIR /app

We have to also modify the Dev Containers configuration file so when VSCode relaunches, it uses zsh as its default terminal and not sh as it’s currently configured.

.devcontainer.json

{
...
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh"
}
}

If your VSCode is already running in a container, you can relaunch it to use the new image and configuration settings by going to “View → Command Palette” and selecting the command “Dev Containers: Rebuild Container”.

Rebuilding a container with the new settings

Once VSCode relaunches you’ll be able to see a much sexier terminal.

oh-my-zsh integrated into Dev Containers

You can find the final version of this project on Github.

Conclusion

Docker is a tool that enables you to encapsulate global dependencies as part of the source code of your project. When building decentralized applications for StarkNet with Cairo, you might find yourself depending on more than one binary or library that needs to be globally available like Python, gmp and NodeJS. Encapsulating these dependencies allows you to have full isolation between projects and consistency across the developers on your team.

Dev Containers is an extension for VSCode that allows you to launch the editor inside a Docker container so every import on your project is properly resolved by the IDE.

When working as part of a team, you can go one step further and create a canonical image for your company and share them on Docker Hub to standardize the StarkNet tools that your team should use. If your team uses Protostar or starknet-devnet as their preferred StarkNet dev tools, you can easily add them to the canonical image so every developer on your team uses the exact same tools.

--

--

David Barreto
Starknet Edu

Starknet Developer Advocate. Find me on Twitter as @barretodavid