Local Development
The tooling is designed with local-first development in mind. Whether you're using the official plugins (Next.js, Vite) or creating a new project with npm create juno@latest
, local development is the default experience.
When running npm run dev
(or start
), your app connects to a locally simulated Satellite via the provided emulator — so you can build and test your backend logic without deploying to the live network.
For continuous integration workflows or advanced setups, this guide shows how to run a Docker-based sandbox that closely mirrors the production environment.
Before you begin
Make sure you have Docker 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 a local Satellite along with a local Internet Identity, allowing you to develop and test without deploying anything live.
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
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
.
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.
Replace my_dapp
in the configuration with another volume name to suit your needs.
For example, if you are developing a "Hello World" project, you could change the volume name to "hello_world".
services:
juno-satellite:
image: junobuild/satellite:latest
ports:
- 5987:5987
- 5999:5999
volumes:
- hello_world:/juno/.juno # <-------- hello_world modified here
- ./juno.dev.config.json:/juno/juno.dev.config.json
- ./target/deploy:/juno/target/deploy/
volumes:
hello_world: # <-------- and here
Configuration
The behavior of the Satellite running in the Docker container can be configured with the help of a local configuration file commonly named juno.dev.config.json
.
This configuration file enables you to define the collections of the Datastore and Storage that run locally, but it also allows for defining additional controllers for your satellite.
The definition is as follows:
export type PermissionText = "public" | "private" | "managed" | "controllers";
export type MemoryText = "heap" | "stable";
export type RulesType = "db" | "storage";
export interface Rule {
collection: string;
read: PermissionText;
write: PermissionText;
memory: MemoryText;
createdAt?: bigint;
updatedAt?: bigint;
maxSize?: number;
maxCapacity?: number;
mutablePermissions: boolean;
maxTokens?: number;
}
export type SatelliteDevDbCollection = Omit<
Rule,
"createdAt" | "updatedAt" | "maxSize"
>;
export type SatelliteDevStorageCollection = Omit<
Rule,
"createdAt" | "updatedAt" | "maxCapacity"
>;
export interface SatelliteDevCollections {
db?: SatelliteDevDbCollection[];
storage?: SatelliteDevStorageCollection[];
}
export interface SatelliteDevController {
id: string;
scope: "write" | "admin";
}
export interface SatelliteDevConfig {
collections: SatelliteDevCollections;
controllers?: SatelliteDevController[];
}
export interface JunoDevConfig {
satellite: SatelliteDevConfig;
}
Example
If, for example, we want to configure a "metadata" collection in the Datastore, a "content" collection in the Storage, and provide an additional controller, we could use the following configuration:
{
"satellite": {
"collections": {
"db": [
{
"collection": "metadata",
"read": "managed",
"write": "managed",
"memory": "stable",
"mutablePermissions": true
}
],
"storage": [
{
"collection": "content",
"read": "public",
"write": "public",
"memory": "stable",
"mutablePermissions": true
}
]
},
"controllers": [
{
"id": "535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe",
"scope": "admin"
}
]
}
}
Path and name
The configuration can be placed in a location other than next to the compose file and can be named whatever suits your needs. If you do so, make sure to adapt the compose file accordingly.
services:
juno-satellite:
image: junobuild/satellite:latest
ports:
- 5987:5987
- 5999:5999
volumes:
- my_dapp:/juno/.juno
- /your/custom/path/your_config_file.json:/juno/juno.dev.config.json # <-------- Modify location and file name of the left hand part
volumes:
my_dapp:
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
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"
Tips and Tricks
In the local environment, several modules (also known as "canisters" on the Internet Computer) are automatically spun up. This ensures that developers have everything they need to start building right out of the box. Thanks to built-in plugins and tooling, these modules are automatically integrated into the environment, eliminating the need for devs to manually manage their bindings.
However, in some cases, it may be useful to explicitly reference module IDs. Below is a list of the modules and their respective IDs that are automatically mounted.
Except for the Satellite ID, which differs from your production environment, all other IDs match the actual smart contract IDs on the mainnet.
Module | ID |
---|---|
Satellite (local only) | x5yt-yyaaa-aaaal-abzbq-cai |
Internet Identity | rdmx6-jaaaa-aaaaa-aaadq-cai |
ICP Ledger | ryjl3-tyaaa-aaaaa-aaaba-cai |
ICP Index | qhbym-qaaaa-aaaaa-aaafq-cai |
If you're using the Docker image intended for developing the Console, you get access to some extra modules that we may eventually merge into the development container. Let us know if you're interested!
Module | ID |
---|---|
CMC | rkp4c-7iaaa-aaaaa-aaaca-cai |
NNS Governance | rrkah-fqaaa-aaaaa-aaaaq-cai |