Skip to main content

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

A screenshot of the DEV Console UI login screen


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).

note

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 i -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.

ImageDescriptionIncludes Console UIBest for
junobuild/skylabAll-in-one local environment like productionEnd-to-end dev, exploration
junobuild/satelliteLightweight setup running a single SatelliteCI, 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.

ModuleSkylab ✅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)
note

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".

docker-compose.yml
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.

A screenshot of the wallet with the Get ICP call to action of Console UI in dev mode

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"