Local Development
Juno offers something most platforms don’t: a full local development environment that closely mirrors production.
When you develop locally, you're running an emulator that includes the well known infrastructure services — including the actual administration Console UI.
This enables:
- A development experience that mirrors mainnet, helping you build with confidence
- A smooth dev loop, from prototype to deployment
- A unique way to build, debug, and validate smart contract logic and frontend behavior — all in one place
Before you begin
Docker is used to run a self-contained local environment with all services, replicas, and the Console UI.
Make sure the tool is installed on your machine (Windows, MacOS, or Linux).
For MacBooks with M processors, it is important to use Docker Desktop version 4.25.0 or later, ideally the latest available version.
Getting Started
The easiest way to run the local developer environment is through the CLI.
If you haven’t installed it yet, run:
- npm
- yarn
- pnpm
npm i -g @junobuild/cli
yarn global add @junobuild/cli
pnpm add -g @junobuild/cli
Then, in your project folder, start the local emulator with:
juno dev start
This will launch the emulator along with all the services needed to develop your project.
We recommend running this in a dedicated terminal window or tab, while your frontend project (e.g. using Vite or Next.js) runs separately using npm run dev or similar.
To stop the emulator, run:
juno dev stop
Available Images
Juno supports two main environments for running your project locally, each tailored to different use cases.
Image | Description | Includes Console UI | Best for |
---|---|---|---|
junobuild/skylab | All-in-one local environment like production | ✅ | End-to-end dev, exploration |
junobuild/satellite | Lightweight setup running a single Satellite | ❌ | CI, focused app testing |
Both variants run on a local network, services and support live-reloading for serverless functions written in Rust or TypeScript.
- Use Skylab for the full experience, including the Console UI and supporting infrastructure.
- Use Satellite when you want a faster, minimal setup for a specific app or automated tests.
The table below shows which modules are available in each image and helps clarify what’s included when running locally with Skylab or Satellite.
Module | Skylab ✅ | Satellite ✅ |
---|---|---|
Console (Backend) | ✅ | ❌ |
Console (UI) | ✅ | ❌ |
Create Satellites / Orbiters via Console UI | ✅ | ❌ |
Default (auto-deployed) Satellite | ❌ | ✅ |
Observatory | ✅ | ❌ |
Internet Identity | ✅ | ✅ |
ICP Ledger | ✅ | ✅ |
ICP Index | ✅ | ✅ |
NNS Governance | ✅ | ❌ |
Cycles Minting (CMC) | ✅ | ❌ |
The default (auto-deployed) Satellite is available with a predefined canister ID jx5yt-yyaaa-aaaal-abzbq-cai
.
Hot Reload
The local container supports live reloading. When you modify your configuration or build custom Functions to enhance Juno's capabilities with serverless features, those changes will be automatically redeployed.
Options
Modify the following information of the docker-compose.yml
file to tweak the container behavior to your needs:
Ports
The default port 5987
is used for communication with the locally deployed satellites and other modules in the local environment (replica). This is the primary port for interaction with the application.
The container also exposes:
-
a small admin server for internal management on port
5999
-
the Console UI on port
5866
If you want to use a different port, such as 8080, update for example the mapping from 5987:5987
to 8080:5987
, where the first value (8080) is the port you can call, and the second (5987) is the actual container port.
Volumes
The image requires a volume to preserve its state. This ensures that when you stop and restart your container, it will resume with the previous state - for instance, if you persist data in its Datastore or Storage, those files will be retained.
The Docker Compose feature automatically creates the volume, so all you need to do is specify it once.
Naming the volume is particularly useful when developing multiple dApps locally, as it allows you to maintain separate states for each project.
For example, if you are developing a "Hello World" project, you could change the volume name to "hello_world".
services:
juno-skylab:
image: junobuild/skylab:latest
ports:
- 5987:5987
- 5999:5999
- 5866:5866
volumes:
- hello_world:/juno/.juno # <-------- hello_world modified here
- ./juno.config.ts:/juno/juno.config.ts
- ./target/deploy:/juno/target/deploy/
volumes:
hello_world: # <-------- and here
Usage
During local development, your app connects to the local emulator (container) by default — no extra configuration needed.
This is handled automatically when using the plugins, or when starting from a template.
If needed, you can opt out of the container behavior by explicitly setting container: false
.
Manual Initialization
If you're not using a plugin and are initializing Juno manually, here's how to configure it to use the local container:
import { initSatellite } from "@junobuild/core";
const container = import.meta.env.DEV === true;
await initSatellite({
satelliteId: container
? "jx5yt-yyaaa-aaaal-abzbq-cai"
: "aaaaa-bbbbb-ccccc-ddddd-cai",
container
});
The SDK will automatically detect the container in local development. If you want to disable that behavior and connect directly to a remote canister (e.g. in CI or production testing), you can do:
await initSatellite({
satelliteId: "aaaaa-bbbbb-ccccc-ddddd-cai",
container: false
});
Administration
The admin server running on port 5999
provides a variety of internal management. Below are some tips and example scripts to make use of this little server.
Get ICP
If you're using the full environment, the Console UI includes a "Get ICP" button in the wallet. It’s a quick way to get ICP out of the box.
You might want to transfer some ICP from the ledger to a specified principal, which can be particularly useful when you're just getting started developing your app and no users currently own ICP. This can be achieved by querying:
http://localhost:5999/ledger/transfer/?to=$PRINCIPAL
For example, you can use the following script:
#!/usr/bin/env bash
# Check if a principal is passed as an argument; otherwise, prompt for it
if [ -z "$1" ]; then
read -r -p "Enter the Wallet ID (owner account, principal): " PRINCIPAL
else
PRINCIPAL=$1
fi
# Make a transfer request to the admin server
curl "http://localhost:5999/ledger/transfer/?to=$PRINCIPAL"