DevZero Logo
DevZero

Recipe Workflow

Understanding how a Recipe is built and executed in DevZero.

Overview

When you create a Recipe on the DevZero Dashboard, it undergoes multiple stages before launching a workspace. These stages ensure that your development environment is correctly configured and optimized for performance.

Execution Stages

Recipes execute in three primary stages:

  1. Build-time: When a Recipe is saved and validated, generating a container image.
  2. Launch-time: When a workspace starts, running initial setup commands.
  3. Run-time: While the workspace is active, allowing interactive execution of commands.

Irrespective of the stage, command blocks will be executed in order, meaning later commands depend on the successful completion of previous ones.

Execution stages (difference between build- and launch-time)


Build Process

Once a Recipe is created, DevZero's system converts it into an LLB (Low-Level Build definition) and processes it in several steps:

1. Recipe to LLB Conversion

  • Environment variables are injected (if applicable).
  • Secrets stored in Vault are referenced securely.
  • Commands are encoded and prepared for execution.

2. Build Execution in buildqd Workers

  • LLBs are passed to buildkitd for structured execution.
  • Secrets are securely retrieved and injected.
  • Caching accelerates repeat builds, reducing setup time.
  • Logs are streamed to the Web UI and CLI for monitoring.

3. Final Image Creation & Storage

  • The built image is pushed to a team-specific OCI image registry.
  • Cached layers improve performance for subsequent builds.

How It Works

Recipe Build Process


Build-Time

The build phase is responsible for preparing and caching the workspace environment before it is launched. Every build step becomes a layer in the workspace container image, optimizing performance.

Each command block can be thought of as a layer in a Docker image. They are wrapped in a script and executed within a bash context. If directory is specified:

  • If absolute, it will be used as is.
  • If relative, it will be relative to /home/devzero.
  • If unspecified, it defaults to /home/devzero.

Use Build-Time for:

āœ” Installing dependencies (apt-get, dnf, npm install, etc.) āœ” Setting up libraries or runtimes (Python, Node.js, Docker) āœ” Copying files or setting up directories

Example:

version: "3"
build:
  steps:
    - type: apt-get
      packages: ["curl", "git", "tar", "unzip"]
    - type: command
      command: |
        curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
        export NVM_DIR=$HOME/.nvm && [ -s $NVM_DIR/nvm.sh ] && \. $NVM_DIR/nvm.sh
        nvm install 21.0.0
    - type: command
      command: |
        sudo apt-get install -y docker-ce docker-ce-cli containerd.io

Best Practices for Build-Time

  • āœ… Install dependencies at build-time to reduce startup delays.
  • āœ… Keep frequently updated packages near the end of the steps to preserve caching.
  • āœ… Use absolute paths when referencing binaries (/usr/bin/python3 instead of python3).
  • āŒ Do not add interactive prompts (use -y for package installations).
  • āŒ Do not start background services (systemctl start). These should be part of launch-time.

If a lower-layer build step changes, all subsequent steps will be rebuilt. Modify lower layers cautiously to retain cache efficiency.

Environment variables set via export are not retained across steps. Write them to a file or /etc/environment instead.

If using a non-Debian-based OS, replace apt-get with: - yum for CentOS/RHEL - dnf for Fedora - apk for Alpine Linux

Do not use build steps for executing daemonized processes (e.g., systemctl start ...). Operations like IDE indexing should be in the launch-time stage.


Launch-Time

Once a workspace starts, launch-time steps execute essential startup configurations. These steps are not cached and run every time a workspace is initialized.

Use Launch-Time for:

  • Starting services (e.g., docker, postgresql)
  • Configuring access permissions
  • Running setup scripts

Example:

launch:
  steps:
    - type: command
      command: |
        systemctl start docker
      directory: /home/devzero
      user: root
 
    - type: command
      command: |
        systemctl start postgresql.service
        echo 'postgres ALL=NOPASSWD: ALL' | sudo tee /etc/sudoers.d/100-postgres
        sudo -u postgres bash -c "psql -c \"CREATE USER pguser WITH PASSWORD 'test1234';\""
        sudo -u postgres createdb testdb -O pguser
      directory: /home/devzero
      user: root

Best Practices for Launch-Time

  • āœ… Use systemctl start for services instead of installing them at launch.
  • āœ… Limit heavy operations (installing large packages belongs in build-time).
  • āœ… Ensure user permissions are properly set.

Launch-time commands cannot be interactive. If a step requires user input, it will get stuck in the logs.

Cacheable steps that make filesystem updates should be in the build-time stage. Binaries and files that users need immediately should be pre-installed.


Run-Time

Run-time steps execute while the workspace is active, meaning users can manually execute commands in a running workspace.

Use Run-Time for:

  • Setting user-specific environment variables
  • Running scripts dynamically based on user actions
  • Configuring local workspace preferences

Example:

runtime:
  environment:
    - name: PATH
      value: "/opt/custom/bin:$PATH"

Unlike build-time and launch-time, run-time configurations are not predefined in the Recipe but instead happen dynamically while users interact with their workspaces.

User secrets cannot be accessed during build time. However, all user secrets are automatically available as environment variables within each user's workspace.

Final Notes

  • Build-time is for installing dependencies & caching.
  • Launch-time is for starting services & configuring settings.
  • Run-time is for interactive environment adjustments.

Each stage plays a specific role in optimizing workspace performance while ensuring flexibility.