> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Devin blueprint template library

> Configure Devin environments with blueprint templates for common languages, private registries, and enterprise infrastructure.

Copy-paste blueprints for common languages and use cases. Combine them to build your full configuration. Examples use Bash on Linux; replace sample hosts, scopes, and commands with your own.

For a full breakdown of every field, see the [Blueprint reference](/onboard-devin/environment/blueprint-reference).

<Info>
  **Secrets:** Configure the named secrets in the **Secrets** tab within each blueprint editor. Build steps receive the applicable blueprint secrets. During a session, ensure each command receives the secrets it needs: repo-scoped secrets require explicit `exec` environment bindings, such as `env={"REGISTRY_TOKEN": "secret:repo:owner/repo:REGISTRY_TOKEN"}`. See [Secrets](/product-guides/secrets). Never hardcode credentials in your blueprint.
</Info>

<Warning>
  **Build steps are not session startup hooks.** `initialize` and `maintenance` run while building snapshots; `maintenance` is not automatically run at session start. `knowledge` supplies instructions to Devin, not executable hooks.

  Keep credentials out of persistent files, including `$ENVRC`. Snapshot cleanup removes Devin's injected secret files, not arbitrary files written by your commands. Use literal variable references supported by the consuming tool, or pass credentials in the command's environment. When a tool requires credential files, create them for one operation and clean them up before it finishes. Repeat that setup explicitly when needed in a session.
</Warning>

***

## Quick start

Minimal blueprints for the most common setups. Copy one, paste it into the blueprint editor, and you're done.

<AccordionGroup>
  <Accordion title="Node.js project">
    ```yaml theme={null}
    initialize: |
      npm install -g pnpm

    maintenance: |
      pnpm install

    knowledge:
      - name: lint
        contents: |
          Run `pnpm lint` to check for errors.
      - name: test
        contents: |
          Run `pnpm test` for the full suite.
    ```
  </Accordion>

  <Accordion title="Python project">
    ```yaml theme={null}
    initialize: |
      curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance: |
      uv sync

    knowledge:
      - name: lint
        contents: |
          Run `uv run ruff check .` to lint.
      - name: test
        contents: |
          Run `uv run pytest` for the full suite.
    ```
  </Accordion>

  <Accordion title="Full-stack (Node + Python)">
    ```yaml theme={null}
    initialize:
      - name: Install pnpm
        run: npm install -g pnpm
      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance:
      - name: Frontend deps
        run: (cd frontend && pnpm install)
      - name: Backend deps
        run: (cd backend && uv sync)

    knowledge:
      - name: structure
        contents: |
          - `frontend/` — React app (pnpm)
          - `backend/` — Python API (uv)
      - name: test
        contents: |
          Frontend: cd frontend && pnpm test
          Backend: cd backend && uv run pytest
    ```
  </Accordion>
</AccordionGroup>

***

## Repository blueprints

Per-repo build steps, dependency management, and knowledge entries. Set these in **Settings > Environment > Blueprints > \[your repo]**.

### Python

<Tabs>
  <Tab title="uv (recommended)">
    Recommended setup for Python projects using [uv](https://docs.astral.sh/uv/) for dependency management.

    ```yaml theme={null}
    initialize: |
      curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance: |
      uv sync

    knowledge:
      - name: lint
        contents: |
          uv run ruff check .

          Auto-fix:
          uv run ruff check --fix .

      - name: test
        contents: |
          uv run pytest

      - name: build
        contents: |
          uv run python -m build
    ```
  </Tab>

  <Tab title="pip + venv">
    Traditional Python setup with pip and venv. Use when your project relies on `requirements.txt`.

    ```yaml theme={null}
    initialize: |
      python3 -m venv .venv

    maintenance: |
      source .venv/bin/activate
      pip install -r requirements.txt

    knowledge:
      - name: lint
        contents: |
          source .venv/bin/activate
          flake8 .

      - name: test
        contents: |
          source .venv/bin/activate
          pytest

      - name: build
        contents: |
          source .venv/bin/activate
          python -m build
    ```
  </Tab>
</Tabs>

### Node.js

<Tabs>
  <Tab title="npm">
    Standard Node.js setup with npm.

    ```yaml theme={null}
    initialize: |
      nvm install 20
      nvm use 20

    maintenance: |
      npm install

    knowledge:
      - name: lint
        contents: |
          npx eslint .

      - name: test
        contents: |
          npm test

      - name: build
        contents: |
          npm run build
    ```

    <Info>
      Use `npm install` (not `npm ci`) in `maintenance`. It does an incremental update, while `npm ci` deletes `node_modules` and reinstalls from scratch each time the command runs.
    </Info>
  </Tab>

  <Tab title="pnpm">
    For projects using pnpm.

    ```yaml theme={null}
    initialize: |
      npm install -g pnpm

    maintenance: |
      pnpm install --frozen-lockfile

    knowledge:
      - name: lint
        contents: |
          pnpm lint

      - name: test
        contents: |
          pnpm test

      - name: build
        contents: |
          pnpm build
    ```
  </Tab>
</Tabs>

### Go

Standard Go setup with modules.

```yaml theme={null}
initialize: |
  GO_VERSION=1.23.5
  ARCH=$(dpkg --print-architecture)
  curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \
    | sudo tar -xz -C /usr/local
  echo 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"' \
    | sudo tee /etc/profile.d/golang.sh > /dev/null

  go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

maintenance: |
  go mod download

knowledge:
  - name: lint
    contents: |
      golangci-lint run

  - name: test
    contents: |
      go test ./...

  - name: build
    contents: |
      go build ./...
```

### Java

<Tabs>
  <Tab title="Gradle">
    Java setup with Gradle.

    <Info>
      JDK 17 is **pre-installed** on Devin's base image. Skip the JDK install step if the default OpenJDK 17 is sufficient.
    </Info>

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Gradle
        run: |
          GRADLE_VERSION=8.12
          curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
            -o /tmp/gradle.zip
          sudo unzip -qo /tmp/gradle.zip -d /opt
          sudo ln -sf /opt/gradle-${GRADLE_VERSION}/bin/gradle /usr/local/bin/gradle
          rm /tmp/gradle.zip

    maintenance: |
      ./gradlew dependencies

    knowledge:
      - name: lint
        contents: |
          ./gradlew check

      - name: test
        contents: |
          ./gradlew test

      - name: build
        contents: |
          ./gradlew build
    ```
  </Tab>

  <Tab title="Maven">
    Java setup with Maven.

    <Info>
      JDK 17 is **pre-installed** on Devin's base image. Skip the JDK install step if the default OpenJDK 17 is sufficient.
    </Info>

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Maven
        run: |
          MAVEN_VERSION=3.9.9
          curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
            | sudo tar -xz -C /opt
          sudo ln -sf /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn

    maintenance: |
      mvn dependency:resolve

    knowledge:
      - name: test
        contents: |
          mvn test

      - name: build
        contents: |
          mvn package
    ```
  </Tab>
</Tabs>

### Ruby on Rails

Rails setup with PostgreSQL.

```yaml theme={null}
initialize:
  - name: Install Ruby 3.3
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ruby-full libpq-dev postgresql-client

maintenance: |
  bundle install
  rails db:migrate

knowledge:
  - name: lint
    contents: |
      bundle exec rubocop

  - name: test
    contents: |
      bundle exec rspec

  - name: build
    contents: |
      rails assets:precompile
```

### Rust

Standard Rust setup with cargo.

<Info>
  **Rust** (via rustup) and **Cargo** are **pre-installed** on Devin's base image. Skip the install step if the default stable toolchain is sufficient. You only need the dependency fetch.
</Info>

```yaml theme={null}
initialize: |
  curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
  source ~/.cargo/env

maintenance: |
  cargo fetch

knowledge:
  - name: lint
    contents: |
      cargo clippy -- -D warnings

  - name: test
    contents: |
      cargo test

  - name: build
    contents: |
      cargo build --release
```

### Monorepos

<Tabs>
  <Tab title="Multi-language">
    Monorepo with a Node.js frontend and Python backend. Each subproject gets its own knowledge entries.

    ```yaml theme={null}
    initialize:
      - name: Install pnpm
        run: npm install -g pnpm
      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance:
      - name: Install frontend dependencies
        run: (cd packages/frontend && pnpm install)
      - name: Install backend dependencies
        run: (cd packages/backend && uv sync)
      - name: Build shared library
        run: (cd packages/shared && pnpm install && pnpm build)

    knowledge:
      - name: structure
        contents: |
          This is a monorepo with three packages:
          - `packages/frontend` — React app (TypeScript, pnpm)
          - `packages/backend` — Python API (FastAPI, uv)
          - `packages/shared` — Shared TypeScript utilities (must be built before frontend)
      - name: frontend
        contents: |
          Run `cd packages/frontend && pnpm dev` to start the dev server.
          Run `cd packages/frontend && pnpm lint` to lint.
          Run `cd packages/frontend && pnpm test` to test.
      - name: backend
        contents: |
          Run `cd packages/backend && uv run uvicorn app.main:app --reload` to start the API.
          Run `cd packages/backend && uv run ruff check .` to lint.
          Run `cd packages/backend && uv run pytest` to test.
    ```

    <Tip>
      Use subshells `(cd dir && command)` instead of `cd dir && command` so the working directory resets between steps.
    </Tip>
  </Tab>

  <Tab title="Multiple JDK versions">
    A Java monorepo where different services require different JDK versions. Install both JDKs at setup, then use `knowledge` entries to tell Devin which `JAVA_HOME` to use for each service.

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17 (primary)
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install JDK 11 (legacy service)
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-11-jdk-headless

    maintenance:
      - name: Warm dependency caches
        run: |
          export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
          (cd services/api && ./gradlew dependencies --refresh-dependencies)

          export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
          (cd services/legacy && ./gradlew dependencies --refresh-dependencies)

    knowledge:
      - name: build_api
        contents: |
          Build the API service (JDK 17):
            JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
            cd services/api && ./gradlew clean build
      - name: build_legacy
        contents: |
          Build the legacy service (JDK 11):
            JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 \
            cd services/legacy && ./gradlew clean build
      - name: test_all
        contents: |
          Run tests for all services:
            JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
            (cd services/api && ./gradlew test)

            JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 \
            (cd services/legacy && ./gradlew test)
    ```
  </Tab>
</Tabs>

***

## Private package registries

Configure package managers to resolve dependencies from private registries. Set these in **Settings > Environment > Blueprints > Org-wide setup** (or per-repo if only one repo needs it).

<Warning>
  **Store references, not expanded credentials.** Quoted heredoc delimiters such as `<<'EOF'` preserve variable references in the file. npm, Yarn Berry, Maven, and Gradle can resolve the references shown below when they run. pip does not expand shell variables in `pip.conf`, so use environment variables instead. Shell setup files must be explicitly sourced in the same command as the tool; do not assume a new shell sources them or refreshes credentials.

  URLs written directly to persistent configuration, such as a Cargo index, Docker mirror, or APT mirror, must not contain credentials. Keep authentication in the separate secrets shown by each template.

  If you used an older template that wrote credentials to disk, remove those files and rebuild from a clean base rather than a snapshot that contains them. Rotate credentials that may have been captured.
</Warning>

<Info>
  If your private registry uses a corporate CA, make sure the [CA certificate](#corporate-ca-certificate) is installed at the enterprise level first. The configuration below assumes HTTPS trust is already established.
</Info>

### Node.js registries

<Tabs>
  <Tab title="npm (scoped)">
    Configure npm to resolve scoped packages (e.g., `@myorg/*`) from a private registry, while public packages continue to come from the default npm registry.

    <Accordion title="Required secrets">
      * `GITHUB_PACKAGES_TOKEN` — Personal access token or GitHub App token with `read:packages` scope
    </Accordion>

    ```yaml theme={null}
    maintenance:
      - name: Configure npm scoped registry
        run: |
          npm config set @myorg:registry https://npm.pkg.github.com
          npm config set //npm.pkg.github.com/:_authToken '${GITHUB_PACKAGES_TOKEN}'
    ```

    <Tip>
      Replace `@myorg` with your npm scope. Common private registry URLs:

      * **GitHub Packages:** `https://npm.pkg.github.com`
      * **Artifactory:** `https://artifactory.example.com/artifactory/api/npm/npm-virtual`
      * **Nexus:** `https://nexus.example.com/repository/npm-group`
      * **GitLab:** `https://gitlab.example.com/api/v4/packages/npm`
      * **AWS CodeArtifact:** `https://<domain>.d.codeartifact.<region>.amazonaws.com/npm/<repo>`
    </Tip>
  </Tab>

  <Tab title="npm (full mirror)">
    Route **all** npm packages through your private registry (not just scoped packages).

    <Accordion title="Required secrets">
      * `NPM_REGISTRY_URL` — Full URL of your npm registry (e.g., `https://artifactory.example.com/artifactory/api/npm/npm-virtual`)
      * `NPM_REGISTRY_HOST` — Hostname only, without protocol (e.g., `artifactory.example.com`)
      * `REGISTRY_TOKEN` — npm auth token for the registry
    </Accordion>

    ```yaml theme={null}
    maintenance:
      - name: Configure npm to use private registry
        run: |
          npm config set registry '${NPM_REGISTRY_URL}'
          npm config set "//${NPM_REGISTRY_HOST}/:_authToken" '${REGISTRY_TOKEN}'
          npm config set strict-ssl true
    ```
  </Tab>

  <Tab title="pnpm">
    Configure pnpm to resolve packages from a private registry.

    <Accordion title="Required secrets">
      * `NPM_REGISTRY_URL` — Full URL of your npm registry
      * `NPM_REGISTRY_HOST` — Hostname only, without protocol
      * `REGISTRY_TOKEN` — npm auth token for the registry
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install pnpm
        run: npm install -g pnpm

    maintenance:
      - name: Configure pnpm for private registry
        run: |
          npm config set registry '${NPM_REGISTRY_URL}'
          npm config set "//${NPM_REGISTRY_HOST}/:_authToken" '${REGISTRY_TOKEN}'
    ```
  </Tab>

  <Tab title="Yarn">
    Configure Yarn (Classic v1 or Berry v2+) to resolve packages from a private registry.

    <Accordion title="Required secrets">
      * `NPM_REGISTRY_URL` — Full URL of your npm/Yarn registry
      * `REGISTRY_TOKEN` — Auth token for the registry
    </Accordion>

    **Yarn Classic (v1):**

    ```yaml theme={null}
    initialize:
      - name: Install Yarn Classic
        run: npm install -g yarn

    maintenance:
      - name: Configure Yarn for private registry
        run: |
          npm config set registry '${NPM_REGISTRY_URL}'
          npm config set //registry.example.com/:_authToken '${REGISTRY_TOKEN}'
    ```

    Replace `registry.example.com` with your registry host. Yarn Classic reads npm's `.npmrc`.

    **Yarn Berry (v2+):**

    ```yaml theme={null}
    maintenance:
      - name: Configure Yarn Berry for private registry
        run: |
          yarn config set npmRegistryServer '${NPM_REGISTRY_URL}'
          yarn config set npmAuthToken '${REGISTRY_TOKEN}'
          # For scoped packages:
          # yarn config set npmScopes.myorg.npmRegistryServer "https://npm.pkg.github.com"
          # yarn config set npmScopes.myorg.npmAuthToken '${GITHUB_PACKAGES_TOKEN}'
    ```
  </Tab>
</Tabs>

### Python registries

<Tabs>
  <Tab title="pip / uv">
    Configure pip and uv to resolve packages from your private PyPI registry (e.g., Nexus, Artifactory).

    <Accordion title="Required secrets">
      * `PYPI_REGISTRY_URL` — Full URL of your PyPI index, including credentials if required (e.g., `https://user:token@nexus.example.com/repository/pypi-proxy/simple`)
    </Accordion>

    ```yaml theme={null}
    maintenance:
      - name: Configure pip/uv for private registry
        run: |
          mkdir -p ~/.config
          cat > ~/.config/private-pypi.sh <<'EOF'
          export PIP_INDEX_URL="$PYPI_REGISTRY_URL"
          export UV_INDEX_URL="$PYPI_REGISTRY_URL"
          EOF

    knowledge:
      - name: private_python_packages
        contents: |
          With PYPI_REGISTRY_URL available in the command's environment, run:
            source ~/.config/private-pypi.sh && uv sync
          Or:
            source ~/.config/private-pypi.sh && pip install -r requirements.txt
    ```

    To warm dependencies during a build, add the same `source ... &&` command to `maintenance` after the configuration step. Percent-encode special characters in URL credentials.

    <Tip>
      Common PyPI registry URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple`
      * **Nexus:** `https://nexus.example.com/repository/pypi-proxy/simple`
      * **AWS CodeArtifact:** `https://aws:TOKEN@domain-owner.d.codeartifact.region.amazonaws.com/pypi/repo/simple/`
      * **Azure Artifacts:** `https://pkgs.dev.azure.com/org/project/_packaging/feed/pypi/simple`
      * **GitLab:** `https://gitlab.example.com/api/v4/groups/<group-id>/-/packages/pypi/simple`
    </Tip>
  </Tab>

  <Tab title="Poetry">
    Configure Poetry to resolve packages from a private PyPI registry.

    <Accordion title="Required secrets">
      * `POETRY_REGISTRY_URL` — Full URL of your PyPI-compatible registry
      * `REGISTRY_USER` — Registry username
      * `REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install Poetry
        run: curl -sSL https://install.python-poetry.org | python3 -

    maintenance:
      - name: Configure Poetry for private registry
        run: |
          poetry config repositories.private "$POETRY_REGISTRY_URL"
          mkdir -p ~/.config
          cat > ~/.config/private-poetry.sh <<'EOF'
          export POETRY_HTTP_BASIC_PRIVATE_USERNAME="$REGISTRY_USER"
          export POETRY_HTTP_BASIC_PRIVATE_PASSWORD="$REGISTRY_PASS"
          EOF

    knowledge:
      - name: private_poetry_packages
        contents: |
          With REGISTRY_USER and REGISTRY_PASS available, run:
            source ~/.config/private-poetry.sh && poetry install
    ```

    For dependency installation, also declare the `private` package source in your project's `pyproject.toml`. `poetry config repositories.private` configures the publishing repository; the environment credentials apply to the matching source name.
  </Tab>
</Tabs>

### JVM registries

<Tabs>
  <Tab title="Maven">
    Install JDK and configure Maven to mirror all dependency resolution through your private registry (e.g., Artifactory, Nexus).

    <Info>
      JDK 17 is **pre-installed** on Devin's base image. Skip the install step if the default OpenJDK 17 is sufficient. You only need the Maven install and registry configuration.
    </Info>

    <Accordion title="Required secrets">
      * `MAVEN_REGISTRY_URL` — URL of your Maven registry (e.g., `https://artifactory.example.com/artifactory/maven-virtual`)
      * `REGISTRY_USER` — Registry username
      * `REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Maven
        run: |
          MAVEN_VERSION=3.9.9
          curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
            | sudo tar -xz -C /opt
          sudo ln -sf /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn

    maintenance:
      - name: Configure Maven for private registry
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml <<'EOF'
          <settings>
            <mirrors>
              <mirror>
                <id>private-registry</id>
                <mirrorOf>*</mirrorOf>
                <url>${env.MAVEN_REGISTRY_URL}</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>private-registry</id>
                <username>${env.REGISTRY_USER}</username>
                <password>${env.REGISTRY_PASS}</password>
              </server>
            </servers>
          </settings>
          EOF
    ```

    <Tip>
      Common registry URL patterns for Maven:

      * **Artifactory:** `https://artifactory.example.com/artifactory/maven-virtual`
      * **Nexus:** `https://nexus.example.com/repository/maven-public`
      * **Azure Artifacts:** `https://pkgs.dev.azure.com/org/project/_packaging/feed/maven/v1`
      * **GitHub Packages:** `https://maven.pkg.github.com`
      * **GitLab:** `https://gitlab.example.com/api/v4/groups/<group-id>/-/packages/maven`
      * **AWS CodeArtifact:** `https://<domain>.d.codeartifact.<region>.amazonaws.com/maven/<repo>`
    </Tip>
  </Tab>

  <Tab title="Gradle">
    Install JDK and configure Gradle to resolve all dependencies through your private registry.

    <Info>
      JDK 17 is **pre-installed** on Devin's base image. Skip the JDK install step if the default is sufficient.
    </Info>

    <Accordion title="Required secrets">
      * `GRADLE_REGISTRY_URL` — URL of your Gradle/Maven registry
      * `REGISTRY_USER` — Registry username
      * `REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Gradle
        run: |
          GRADLE_VERSION=8.12
          curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
            -o /tmp/gradle.zip
          sudo unzip -qo /tmp/gradle.zip -d /opt
          sudo ln -sf /opt/gradle-${GRADLE_VERSION}/bin/gradle /usr/local/bin/gradle
          rm /tmp/gradle.zip

    maintenance:
      - name: Configure Gradle for private registry
        run: |
          mkdir -p ~/.gradle
          cat > ~/.gradle/init.gradle <<'EOF'
          allprojects {
              repositories {
                  maven {
                      url System.getenv("GRADLE_REGISTRY_URL")
                      credentials {
                          username = System.getenv("REGISTRY_USER")
                          password = System.getenv("REGISTRY_PASS")
                      }
                      allowInsecureProtocol = false
                  }
              }
          }
          EOF
    ```
  </Tab>
</Tabs>

### Other registries

<AccordionGroup>
  <Accordion title="Go module proxy">
    Install Go and configure it to resolve modules through a private module proxy (e.g., Athens, Artifactory, or a GOPROXY endpoint).

    <Accordion title="Required secrets">
      * `GO_PROXY_URL` — URL of your Go module proxy (e.g., `https://athens.corp.internal`)
    </Accordion>

    Connect the Git provider and grant access to the repositories that host your private modules through [Git integrations](/integrations/overview). Use ordinary HTTPS Git URLs with Devin's configured authentication. Do not embed a token in a Git URL rewrite.

    ```yaml theme={null}
    initialize:
      - name: Install Go
        run: |
          GO_VERSION=1.23.5
          ARCH=$(dpkg --print-architecture)
          curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \
            | sudo tar -xz -C /usr/local
          echo 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"' \
            | sudo tee /etc/profile.d/golang.sh > /dev/null

      - name: Configure Go for private modules
        run: |
          cat << 'GOENV' | sudo tee /etc/profile.d/go-private.sh > /dev/null
          export GOPROXY="$GO_PROXY_URL,direct"
          export GONOSUMCHECK="corp.internal/*,github.com/myorg/*"
          export GOPRIVATE="corp.internal/*,github.com/myorg/*"
          GOENV

    knowledge:
      - name: private_go_modules
        contents: |
          From the repository root, with GO_PROXY_URL available, run:
            source /etc/profile.d/go-private.sh && go mod download
    ```

    To warm the module cache during builds, add that command to the **repository** blueprint's `maintenance`, where `go.mod` is available. Do not run it in org-wide setup, which runs before repositories are cloned.

    <Tip>
      Common Go proxy URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/go-virtual`
      * **Nexus:** `https://nexus.example.com/repository/go-proxy`
      * **Athens:** `https://athens.corp.internal`
    </Tip>
  </Accordion>

  <Accordion title=".NET / NuGet">
    Configure NuGet to resolve packages from a private feed.

    <Accordion title="Required secrets">
      * `NUGET_SOURCE_URL` — URL of your NuGet feed
      * `NuGetPackageSourceCredentials_private` — Feed credentials in NuGet's environment format: `Username=any;Password=<PAT>` (use your feed's required username)
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install .NET SDK
        run: |
          curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0
          echo 'export PATH="$HOME/.dotnet:$PATH"' \
            | sudo tee /etc/profile.d/dotnet.sh > /dev/null

    maintenance:
      - name: Configure NuGet for private feed
        run: |
          dotnet nuget add source "$NUGET_SOURCE_URL" \
            --name private 2>/dev/null || \
          dotnet nuget update source private \
            --source "$NUGET_SOURCE_URL"

    knowledge:
      - name: private_nuget_packages
        contents: |
          Run dotnet restore with NuGetPackageSourceCredentials_private in its
          environment. The suffix must match the source name "private".
    ```
  </Accordion>

  <Accordion title="Docker">
    Configure Docker to pull from a private container registry.

    Docker login can write credentials to its config directory. Use an isolated directory for each operation, and remove it even if the pull fails. Run the login-and-pull sequence again when you need another image in a session.

    <Accordion title="Required secrets">
      * `DOCKER_MIRROR_URL` *(optional)* — URL of your Docker Hub mirror (e.g., `https://mirror.corp.internal`)
      * `DOCKER_REGISTRY_URL` — URL of your private container registry (e.g., `registry.corp.internal:5000`)
      * `DOCKER_REGISTRY_USER` — Registry username
      * `DOCKER_REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Create Docker config directory
        run: sudo mkdir -p /etc/docker

    maintenance:
      - name: Configure Docker for private registry
        run: |
          # Configure registry mirror (optional — routes Docker Hub pulls through your registry)
          cat << EOF | sudo tee /etc/docker/daemon.json > /dev/null
          {
            "registry-mirrors": ["$DOCKER_MIRROR_URL"]
          }
          EOF
          sudo systemctl restart docker || true

    knowledge:
      - name: private_container_images
        contents: |
          With the registry secrets available, run this in one shell.
          Replace team/image:tag with the image to pull:
            (
              set -euo pipefail
              umask 077
              DOCKER_CONFIG=$(mktemp -d)
              export DOCKER_CONFIG
              trap 'rm -rf "$DOCKER_CONFIG"' EXIT
              printf '%s' "$DOCKER_REGISTRY_PASS" |
                docker login "$DOCKER_REGISTRY_URL" \
                  --username "$DOCKER_REGISTRY_USER" --password-stdin
              docker pull "$DOCKER_REGISTRY_URL/team/image:tag"
            )
    ```

    To cache an image in the snapshot, put the same complete subshell in a build step. Do not split login and cleanup across steps, and verify cleanup succeeds before imaging. A registry mirror URL must not contain credentials.

    <Tip>
      Common container registry URLs:

      * **Amazon ECR:** `<account-id>.dkr.ecr.<region>.amazonaws.com`
      * **Azure Container Registry:** `<name>.azurecr.io`
      * **Google Artifact Registry:** `<region>-docker.pkg.dev`
      * **GitHub Container Registry:** `ghcr.io`
      * **GitLab Container Registry:** `registry.gitlab.example.com`
      * **Nexus:** `https://nexus.example.com:8443`
      * **JFrog:** `<name>.jfrog.io`
    </Tip>
  </Accordion>

  <Accordion title="Rust / Cargo">
    Configure Cargo to resolve crates from a private registry.

    <Info>
      **Rust** (via rustup) and **Cargo** are **pre-installed** on Devin's base image. Skip the install step if the default stable toolchain is sufficient. You only need the registry configuration.
    </Info>

    <Accordion title="Required secrets">
      * `CARGO_REGISTRY_INDEX` — URL of the private registry index (e.g., `sparse+https://cargo.corp.internal/api/v1/crates/`)
      * `CARGO_REGISTRIES_PRIVATE_TOKEN` — Auth token for the registry named `private`
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install Rust
        run: |
          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
            | sh -s -- -y --default-toolchain stable
          echo 'source "$HOME/.cargo/env"' \
            | sudo tee /etc/profile.d/rust.sh > /dev/null

    maintenance:
      - name: Configure Cargo for private registry
        run: |
          mkdir -p ~/.cargo
          cat > ~/.cargo/config.toml << EOF
          [registry]
          global-credential-providers = ["cargo:token"]

          [registries.private]
          index = "$CARGO_REGISTRY_INDEX"

          [source.crates-io]
          replace-with = "private"

          [source.private]
          registry = "$CARGO_REGISTRY_INDEX"
          EOF

    knowledge:
      - name: private_rust_packages
        contents: |
          Run cargo fetch with CARGO_REGISTRIES_PRIVATE_TOKEN in its environment.
    ```

    The registry index URL must not contain credentials. Cargo's `cargo:token` provider reads the named registry token from the environment; do not run `cargo login` in a build step.

    <Tip>
      If you only need to **add** a private registry without replacing crates.io, remove the `[source.crates-io]` and `[source.private]` sections and use `cargo install --registry private` or `[dependencies] my-crate = { version = "1.0", registry = "private" }` in `Cargo.toml`.
    </Tip>
  </Accordion>

  <Accordion title="Ruby / Bundler">
    Install Ruby and configure Bundler to resolve gems from a private gem server.

    <Accordion title="Required secrets">
      * `GEM_SERVER_URL` — URL of your private gem server (e.g., `https://artifactory.example.com/artifactory/api/gems/gems-virtual`)
      * `BUNDLE_ARTIFACTORY__EXAMPLE__COM` — `username:password` for `artifactory.example.com`; change the variable name to match your gem server
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install Ruby
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ruby-full

    maintenance:
      - name: Configure Bundler for private gem server
        run: |
          bundle config set mirror.https://rubygems.org "$GEM_SERVER_URL"

    knowledge:
      - name: private_ruby_packages
        contents: |
          Run bundle install with BUNDLE_ARTIFACTORY__EXAMPLE__COM in its environment.
    ```

    Use a credential-free `GEM_SERVER_URL`. For Bundler's credential variable, prefix the uppercase hostname with `BUNDLE_`, replace each dot with `__`, and each hyphen with `___`.

    <Tip>
      Common gem server URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/api/gems/gems-virtual`
      * **Nexus:** `https://nexus.example.com/repository/rubygems-proxy`
      * **Gemfury:** `https://gem.fury.io/<org>`
    </Tip>
  </Accordion>

  <Accordion title="PHP / Composer">
    Install PHP and configure Composer to resolve packages from a private Packagist or Satis registry.

    <Accordion title="Required secrets">
      * `COMPOSER_REGISTRY_URL` — URL of your private Composer registry (e.g., `https://repo.packagist.com/<org>`)
      * `COMPOSER_AUTH` — JSON credentials for the registry host, such as `{"http-basic":{"repo.packagist.com":{"username":"<user>","password":"<token>"}}}`
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install PHP and Composer
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
            php-cli php-mbstring php-xml php-curl unzip

          # Install Composer
          curl -sS https://getcomposer.org/installer | php
          sudo mv composer.phar /usr/local/bin/composer

    maintenance:
      - name: Configure Composer for private registry
        run: |
          composer config --global repositories.private \
            composer "$COMPOSER_REGISTRY_URL"

    knowledge:
      - name: private_php_packages
        contents: |
          Run composer install with COMPOSER_AUTH in its environment.
          Do not save these credentials to auth.json.
    ```

    Use a credential-free `COMPOSER_REGISTRY_URL`.

    <Tip>
      Common Composer registry URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/api/composer/packagist-virtual`
      * **Nexus:** `https://nexus.example.com/repository/packagist-proxy`
      * **Private Packagist:** `https://repo.packagist.com/<org>`
      * **Satis:** `https://satis.corp.internal`
    </Tip>
  </Accordion>

  <Accordion title="AWS CodeArtifact token refresh">
    AWS CodeArtifact tokens normally expire after 12 hours. Create a script containing literal references, then explicitly source it in the same shell command as npm, pip, or Maven to obtain a token. Saving the script in a blueprint does not execute it at session start.

    <Info>
      **`awscli` is pre-installed** on Devin's base image. You only need the token refresh and registry configuration.
    </Info>

    <Accordion title="Required secrets">
      * `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` — IAM credentials with `codeartifact:GetAuthorizationToken` and `sts:GetServiceBearerToken` permissions
      * `CA_DOMAIN` — Your CodeArtifact domain name
      * `CA_DOMAIN_OWNER` — AWS account ID that owns the domain
      * `CA_REGION` — AWS region (e.g., `us-east-1`)
      * `CA_NPM_REPO`, `CA_PYPI_REPO`, `CA_MAVEN_REPO` — Repository names for each ecosystem
    </Accordion>

    ```yaml theme={null}
    maintenance:
      - name: Configure CodeArtifact clients and refresh script
        run: |
          mkdir -p ~/.config
          cat > ~/.config/codeartifact.sh <<'SCRIPT'
          CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \
            --domain "$CA_DOMAIN" \
            --domain-owner "$CA_DOMAIN_OWNER" \
            --region "$CA_REGION" \
            --query authorizationToken \
            --output text) || return
          export CODEARTIFACT_AUTH_TOKEN
          export CA_ENDPOINT="https://${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com"
          export PIP_INDEX_URL="https://aws:${CODEARTIFACT_AUTH_TOKEN}@${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com/pypi/${CA_PYPI_REPO}/simple/"
          SCRIPT

          # Configure npm
          npm config set registry '${CA_ENDPOINT}/npm/${CA_NPM_REPO}/'
          npm config set "//${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com/npm/${CA_NPM_REPO}/:_authToken" '${CODEARTIFACT_AUTH_TOKEN}'

          # Configure Maven (optional)
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml <<'EOF'
          <settings>
            <servers>
              <server>
                <id>codeartifact</id>
                <username>aws</username>
                <password>${env.CODEARTIFACT_AUTH_TOKEN}</password>
              </server>
            </servers>
            <mirrors>
              <mirror>
                <id>codeartifact</id>
                <mirrorOf>*</mirrorOf>
                <url>${env.CA_ENDPOINT}/maven/${env.CA_MAVEN_REPO}/</url>
              </mirror>
            </mirrors>
          </settings>
          EOF

    knowledge:
      - name: codeartifact_packages
        contents: |
          With AWS credentials and the CA_* variables available, run the appropriate
          command in one shell; source the script again for later invocations:
            source ~/.config/codeartifact.sh && npm install
            source ~/.config/codeartifact.sh && pip install -r requirements.txt
            source ~/.config/codeartifact.sh && mvn dependency:resolve
    ```

    To warm a dependency cache in the snapshot, add the appropriate `source ... &&` command as a later build step. The fetched token stays in that shell's environment; do not persist it with `aws codeartifact login`, `npm config set` using an expanded token, or a credential-bearing `pip.conf`.
  </Accordion>
</AccordionGroup>

***

## Enterprise infrastructure

Machine-level infrastructure that applies across all orgs and repos. Set these in **Enterprise Settings > Environments > Blueprint** (for enterprise-wide) or **Settings > Environment > Blueprints > Org-wide setup** (for org-wide).

### Network and connectivity

<AccordionGroup>
  <Accordion title="Corporate CA certificate">
    Your organization uses a private certificate authority for internal services. Devin needs the root certificate to reach internal registries and tools over HTTPS.

    <Accordion title="Required secrets">
      * `CORP_ROOT_CA_B64` — Base64-encoded PEM certificate from your corporate CA. Generate with: `cat corp-root-ca.crt | base64 -w0`
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install corporate CA certificate
        run: |
          set -o pipefail
          printf '%s' "$CORP_ROOT_CA_B64" | base64 -d | openssl x509 -outform PEM \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates
          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null
    ```

    These inputs are public certificates. `openssl x509` writes only the parsed certificate, so an accidentally included private key is not copied into the trust store. Supply each CA certificate separately, as in the next example.
  </Accordion>

  <Accordion title="Multiple CA certificates">
    If your organization uses multiple CA certificates (e.g., separate CAs for different internal services).

    <Accordion title="Required secrets">
      * `CORP_ROOT_CA_B64` — Base64-encoded primary CA certificate
      * `CORP_INTERMEDIATE_CA_B64` — Base64-encoded intermediate CA certificate
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install corporate CA certificates
        run: |
          set -o pipefail
          printf '%s' "$CORP_ROOT_CA_B64" | base64 -d | openssl x509 -outform PEM \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          printf '%s' "$CORP_INTERMEDIATE_CA_B64" | base64 -d | openssl x509 -outform PEM \
            | sudo tee /usr/local/share/ca-certificates/corp-intermediate-ca.crt > /dev/null
          sudo update-ca-certificates

          # Create a combined bundle for tools that need a single CA file
          cat /usr/local/share/ca-certificates/corp-*.crt \
            | sudo tee /usr/local/share/ca-certificates/corp-bundle.crt > /dev/null

          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-bundle.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null
    ```
  </Accordion>

  <Accordion title="HTTP/HTTPS proxy">
    Route all network traffic through a corporate proxy.

    <Accordion title="Required secrets">
      * `CORP_HTTP_PROXY` — HTTP proxy URL (e.g., `http://proxy.corp.example.com:8080`)
      * `CORP_HTTPS_PROXY` — HTTPS proxy URL
      * `CORP_NO_PROXY` — Comma-separated list of hosts to bypass proxy (e.g., `localhost,127.0.0.1,.corp.example.com`)
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Configure system-wide proxy
        run: |
          cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$CORP_HTTP_PROXY"
          export https_proxy="$CORP_HTTPS_PROXY"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$CORP_HTTP_PROXY"
          export HTTPS_PROXY="$CORP_HTTPS_PROXY"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

    knowledge:
      - name: proxy_commands
        contents: |
          With CORP_HTTP_PROXY, CORP_HTTPS_PROXY and CORP_NO_PROXY available, source
          the proxy setup in each command that needs it, for example:
            source /etc/profile.d/proxy.sh && npm install
            source /etc/profile.d/proxy.sh && git fetch
    ```
  </Accordion>

  <Accordion title="Authenticated proxy">
    If your corporate proxy requires username/password authentication.

    <Accordion title="Required secrets">
      * `PROXY_USER` — Proxy username
      * `PROXY_PASS` — Proxy password
      * `PROXY_HOST` — Proxy hostname and port (e.g., `proxy.corp.example.com:8080`)
      * `CORP_NO_PROXY` — Hosts to bypass proxy
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Configure authenticated proxy
        run: |
          cat <<'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="http://${PROXY_USER}:${PROXY_PASS}@${PROXY_HOST}"
          export https_proxy="$http_proxy"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$http_proxy"
          export HTTPS_PROXY="$https_proxy"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

    knowledge:
      - name: authenticated_proxy_commands
        contents: |
          With the proxy secrets available, source the proxy setup in the same
          command as each tool that needs it, for example:
            source /etc/profile.d/proxy.sh && git fetch
    ```

    Percent-encode special characters in the proxy username and password. Git and npm can use proxy environment variables; do not copy a credential-bearing proxy URL into their persistent configuration.
  </Accordion>

  <Accordion title="CA certificate + proxy (combined)">
    Combined setup for environments that need both a corporate CA and a proxy. This is common in enterprise environments where internal services use private certificates and all traffic must route through a proxy.

    <Accordion title="Required secrets">
      * `CORP_ROOT_CA_B64` — Base64-encoded corporate CA certificate
      * `CORP_HTTP_PROXY`, `CORP_HTTPS_PROXY` — Proxy URLs
      * `CORP_NO_PROXY` — Hosts to bypass proxy
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install corporate CA certificate
        run: |
          set -o pipefail
          printf '%s' "$CORP_ROOT_CA_B64" | base64 -d | openssl x509 -outform PEM \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates
          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null

      - name: Configure system-wide proxy
        run: |
          cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$CORP_HTTP_PROXY"
          export https_proxy="$CORP_HTTPS_PROXY"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$CORP_HTTP_PROXY"
          export HTTPS_PROXY="$CORP_HTTPS_PROXY"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

    knowledge:
      - name: proxy_commands
        contents: |
          With the proxy secrets available, run:
            source /etc/profile.d/proxy.sh && git fetch
    ```
  </Accordion>

  <Accordion title="VPN connection">
    Your private registries, Git servers, or other internal services are only reachable over VPN. Install the VPN client in the blueprint. Establish the tunnel explicitly for the operations that need it, and disconnect and remove credential files afterward.

    <Accordion title="Required secrets">
      **OpenVPN:**

      * `VPN_CONFIG_B64` — Base64-encoded OpenVPN config file (`.ovpn`). Generate with: `cat corp.ovpn | base64 -w0`
      * `VPN_AUTH_USER` *(optional)* — VPN username, if your VPN requires username/password auth
      * `VPN_AUTH_PASS` *(optional)* — VPN password

      **WireGuard:**

      * `WG_CONFIG_B64` — Base64-encoded WireGuard config file. Generate with: `cat wg0.conf | base64 -w0`
    </Accordion>

    **OpenVPN:**

    ```yaml theme={null}
    initialize:
      - name: Install OpenVPN
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openvpn
    ```

    Run this block explicitly in a session with the VPN secrets available. Replace the `curl` command with the work that needs the tunnel. The config must contain the required certificates and keys inline and use `dev tun0`; do not enable a persistent VPN service.

    ```bash theme={null}
    (
      set -euo pipefail
      umask 077
      VPN_DIR=$(mktemp -d)
      cleanup() {
        if [ -f "$VPN_DIR/openvpn.pid" ]; then
          sudo kill "$(cat "$VPN_DIR/openvpn.pid")" || true
        fi
        sudo rm -rf "$VPN_DIR"
      }
      trap cleanup EXIT
      printf '%s' "$VPN_CONFIG_B64" | base64 -d > "$VPN_DIR/corp.conf"
      if [ -n "${VPN_AUTH_USER:-}" ] && [ -n "${VPN_AUTH_PASS:-}" ]; then
        printf '%s\n%s\n' "$VPN_AUTH_USER" "$VPN_AUTH_PASS" > "$VPN_DIR/auth.txt"
        printf 'auth-user-pass %s/auth.txt\n' "$VPN_DIR" >> "$VPN_DIR/corp.conf"
      fi
      sudo openvpn --config "$VPN_DIR/corp.conf" \
        --writepid "$VPN_DIR/openvpn.pid" --daemon
      for i in $(seq 1 30); do
        if ip link show tun0 >/dev/null 2>&1; then break; fi
        sleep 1
      done
      ip link show tun0 >/dev/null
      curl --fail https://service.corp.internal/health
    )
    ```

    **WireGuard:**

    ```yaml theme={null}
    initialize:
      - name: Install WireGuard
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq wireguard-tools
    ```

    Run this block explicitly with `WG_CONFIG_B64` available, replacing the `curl` command with your operation:

    ```bash theme={null}
    (
      set -euo pipefail
      umask 077
      VPN_DIR=$(mktemp -d)
      cleanup() {
        sudo wg-quick down "$VPN_DIR/corp.conf" || true
        sudo rm -rf "$VPN_DIR"
      }
      trap cleanup EXIT
      printf '%s' "$WG_CONFIG_B64" | base64 -d > "$VPN_DIR/corp.conf"
      sudo wg-quick up "$VPN_DIR/corp.conf"
      curl --fail https://service.corp.internal/health
    )
    ```

    If a snapshot build needs VPN access, wrap **all** dependent work in the same block so cleanup completes before imaging. Merely moving credential writes to `maintenance` does not make them temporary. Do not snapshot an active tunnel or private VPN configuration.

    <Tip>
      For more details on VPN setup, see [VPN configuration](/onboard-devin/vpn).
    </Tip>
  </Accordion>

  <Accordion title="Custom DNS resolution">
    Your internal services use private DNS names that aren't resolvable by public DNS.

    ```yaml theme={null}
    initialize:
      - name: Configure custom DNS resolution
        run: |
          # Add internal hostnames
          cat << 'HOSTS' | sudo tee -a /etc/hosts > /dev/null
          10.0.1.50  nexus.corp.internal
          10.0.1.51  git.corp.internal
          10.0.1.52  artifactory.corp.internal
          HOSTS

          # Optionally configure custom nameservers
          sudo mkdir -p /etc/systemd/resolved.conf.d
          cat << 'DNS' | sudo tee /etc/systemd/resolved.conf.d/corp.conf > /dev/null
          [Resolve]
          DNS=10.0.0.53 10.0.0.54
          Domains=corp.internal
          DNS
          sudo systemctl restart systemd-resolved || true
    ```
  </Accordion>
</AccordionGroup>

### Identity and security

<AccordionGroup>
  <Accordion title="GPG commit signing">
    Your organization requires all Git commits to be signed, and you want GitHub to mark Devin's commits as **Verified**.

    <Accordion title="Required secrets">
      * `GPG_PRIVATE_KEY_B64` — Base64-encoded GPG private key. Generate with: `gpg --export-secret-keys <key-id> | base64 -w0`
      * `GPG_SIGNING_KEY` — Full fingerprint of the signing key
      * `GIT_USER_NAME` — Git author name (e.g., `Devin AI`)
      * `GIT_USER_EMAIL` — Git author email. **Must match a UID on the GPG key**, otherwise GitHub will not verify the signature.
    </Accordion>

    <Note>
      Also upload the matching **public** key to the GitHub account whose credentials Devin uses to push (under [GitHub Settings > SSH and GPG keys](https://github.com/settings/keys)). GitHub only marks commits as Verified when the signing public key is registered on the account that authored the commit.
    </Note>

    Do not import private signing keys during snapshot builds. After staging the intended changes, explicitly run this block in a session with the named secrets available. It uses a temporary keyring and Git settings that apply only to this commit:

    ```bash theme={null}
    (
      set -euo pipefail
      umask 077
      GNUPGHOME=$(mktemp -d)
      export GNUPGHOME
      trap 'gpgconf --kill gpg-agent; rm -rf "$GNUPGHOME"' EXIT
      printf '%s' "$GPG_PRIVATE_KEY_B64" | base64 -d | gpg --batch --import
      git -c user.name="$GIT_USER_NAME" -c user.email="$GIT_USER_EMAIL" \
        -c user.signingkey="$GPG_SIGNING_KEY" -c commit.gpgsign=true \
        -c gpg.program=gpg commit -m "Describe the change"
    )
    ```

    If your key requires a passphrase, complete its approved GPG agent/pinentry flow during the operation. Do not save the passphrase or keyring in the blueprint or snapshot. Repeat the temporary-keyring setup for later commits or signed tags.
  </Accordion>

  <Accordion title="Git identity and SSH keys">
    Configure Devin's Git identity and SSH keys for accessing private Git servers.

    Prefer [Git integrations](/integrations/overview) for supported Git providers. For a server that requires an explicit SSH key, keep the private key out of the blueprint build and use it only for the session operation below.

    <Accordion title="Required secrets">
      * `GIT_USER_NAME` — Git author name
      * `GIT_USER_EMAIL` — Git author email
      * `SSH_PRIVATE_KEY_B64` — Base64-encoded SSH private key. Generate with: `cat ~/.ssh/id_ed25519 | base64 -w0`
      * `SSH_KNOWN_HOSTS_B64` — Base64-encoded known hosts entries verified against your server administrator's published host key fingerprints
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Configure git identity
        run: |
          git config --global user.name "$GIT_USER_NAME"
          git config --global user.email "$GIT_USER_EMAIL"
    ```

    Run this block explicitly in the session, replacing the Git URL with your server and repository:

    ```bash theme={null}
    (
      set -euo pipefail
      umask 077
      SSH_DIR=$(mktemp -d)
      trap 'rm -rf "$SSH_DIR"' EXIT
      printf '%s' "$SSH_PRIVATE_KEY_B64" | base64 -d > "$SSH_DIR/id_ed25519"
      printf '%s' "$SSH_KNOWN_HOSTS_B64" | base64 -d > "$SSH_DIR/known_hosts"
      export GIT_SSH_COMMAND="ssh -F /dev/null -i '$SSH_DIR/id_ed25519' -o IdentitiesOnly=yes -o UserKnownHostsFile='$SSH_DIR/known_hosts' -o StrictHostKeyChecking=yes"
      git ls-remote git@git.corp.internal:team/repo.git
    )
    ```

    <Tip>
      If you need custom SSH options, add them to this invocation without copying a private key into a persistent SSH config or home directory. Repeat the setup for each operation.
    </Tip>
  </Accordion>
</AccordionGroup>

### System configuration

<AccordionGroup>
  <Accordion title="System packages">
    Install system-level packages that aren't in the default Devin image (e.g., native libraries for image processing or PDF generation).

    ```yaml theme={null}
    initialize:
      - name: Install system packages
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
            libpq-dev \
            libmagickwand-dev \
            poppler-utils \
            ffmpeg
    ```
  </Accordion>

  <Accordion title="Custom environment variables">
    Set persistent, non-secret environment variables that should be available in every session. Do not write secret values or derived credentials to `$ENVRC`.

    The recommended approach is to write `KEY=VALUE` lines to the `$ENVRC` file. Variables written to `$ENVRC` are automatically exported for all subsequent steps and the Devin session (similar to GitHub Actions' `$GITHUB_ENV`).

    ```yaml theme={null}
    initialize:
      - name: Set custom environment variables
        run: |
          echo "CORPORATE_ENV=production" >> $ENVRC
          echo "DEFAULT_REGION=us-east-1" >> $ENVRC
          echo "MAX_RETRIES=3" >> $ENVRC
    ```

    <Tip>
      You can also write env vars to `/etc/profile.d/` scripts for system-wide availability:

      ```bash theme={null}
      cat << 'ENVVARS' | sudo tee /etc/profile.d/custom-env.sh > /dev/null
      export CORPORATE_ENV=production
      export DEFAULT_REGION=us-east-1
      ENVVARS
      ```

      Both approaches work. `$ENVRC` is simpler and recommended for most cases.
    </Tip>
  </Accordion>

  <Accordion title="Locale and timezone">
    Default base images may have broken locale settings. Configure locale and timezone to prevent warnings from build tools, Java, Python, and Git.

    ```yaml theme={null}
    initialize:
      - name: Configure locale and timezone
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq locales

          # Generate and set the locale
          sudo sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
          sudo locale-gen
          sudo update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8

          cat << 'LOCALE' | sudo tee /etc/profile.d/locale.sh > /dev/null
          export LANG="en_US.UTF-8"
          export LC_ALL="en_US.UTF-8"
          LOCALE

          # Set timezone
          sudo timedatectl set-timezone UTC 2>/dev/null || \
            sudo ln -sfn /usr/share/zoneinfo/UTC /etc/localtime
    ```
  </Accordion>

  <Accordion title="Resource limits (ulimits)">
    Java, Gradle, and Node.js builds frequently hit the default 1024 open-file limit. Raise it to prevent build failures.

    ```yaml theme={null}
    initialize:
      - name: Raise resource limits
        run: |
          cat << 'LIMITS' | sudo tee /etc/security/limits.d/99-devin.conf > /dev/null
          *    soft    nofile    65536
          *    hard    nofile    65536
          *    soft    nproc     65536
          *    hard    nproc     65536
          LIMITS

          # Also set the kernel max
          echo "fs.file-max = 65536" | sudo tee /etc/sysctl.d/99-devin-filemax.conf > /dev/null
          sudo sysctl -p /etc/sysctl.d/99-devin-filemax.conf 2>/dev/null || true
    ```
  </Accordion>

  <Accordion title="APT mirror replacement">
    In air-gapped or restricted environments, replace the default Ubuntu APT sources with an internal mirror.

    <Accordion title="Required secrets">
      * `APT_MIRROR_URL` — URL of your internal APT mirror (e.g., `https://artifactory.example.com/artifactory/ubuntu-remote`)
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Replace APT sources with internal mirror
        run: |
          # Backup original sources
          sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

          # Replace all Ubuntu mirrors with your internal mirror
          sudo sed -i "s|http://archive.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
          sudo sed -i "s|http://security.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list

          sudo apt-get update -qq
    ```

    <Tip>
      Common APT mirror URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/ubuntu-remote`
      * **Nexus:** `https://nexus.example.com/repository/ubuntu-proxy`
    </Tip>
  </Accordion>
</AccordionGroup>

***

## Advanced patterns

<AccordionGroup>
  <Accordion title="Environment variables with direnv">
    Devin's base environment includes [direnv](https://direnv.net/). Use `initialize` to create `.envrc` files. Direnv loads them automatically.

    ```yaml theme={null}
    initialize: |
      cat <<'EOF' > .envrc
      export DATABASE_URL=postgresql://localhost:5432/myapp_dev
      export REDIS_URL=redis://localhost:6379
      export APP_ENV=development
      EOF

    maintenance: |
      direnv allow .
    ```

    <Info>
      direnv is pre-hooked into Devin's shell, so `.envrc` variables load automatically. No manual sourcing needed.
    </Info>

    <Tip>
      For **sensitive** environment variables (API keys, tokens, database passwords), use [repo secrets](/product-guides/secrets) instead of `.envrc` files, and bind them explicitly to session commands that need them. Expanding a secret into a file during a build can still put it in the snapshot.
    </Tip>
  </Accordion>

  <Accordion title="Per-repository Node version switching">
    Use nvm (pre-installed) to switch Node.js versions per repository via `.nvmrc`.

    ```yaml theme={null}
    initialize: |
      nvm install 18
      nvm install 20
      nvm install 22

    maintenance: |
      nvm use
    ```

    <Info>
      `nvm use` reads `.nvmrc` from the repo root. Make sure your repository has one (e.g., containing `20`).
    </Info>
  </Accordion>

  <Accordion title="Browser authentication (Playwright)">
    Devin provides a Chrome browser with a CDP endpoint at `localhost:29229` **during sessions**. Use Playwright scripts to automate browser-based login.

    <Warning>
      The browser is only available during sessions, not snapshot builds. Install Playwright in `initialize` and keep login scripts in your repo.
    </Warning>

    ```yaml theme={null}
    initialize: |
      pip install playwright
      playwright install chromium

    maintenance: |
      npm install

    knowledge:
      - name: browser-auth
        contents: |
          This project requires browser authentication.
          Run the login script before interacting with the app:
          python scripts/login.py

          Devin's Chrome browser is accessible via CDP at:
          http://localhost:29229
    ```

    Example login script (`scripts/login.py`):

    ```python theme={null}
    from playwright.sync_api import sync_playwright
    import os

    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp("http://localhost:29229")
        context = browser.contexts[0]
        page = context.pages[0] if context.pages else context.new_page()

        page.goto("https://internal-tool.example.com/login")
        page.fill("#username", os.environ["TOOL_USERNAME"])
        page.fill("#password", os.environ["TOOL_PASSWORD"])
        page.click('button[type="submit"]')
        page.wait_for_url("**/dashboard")
    ```

    <Warning>
      Store login credentials as secrets, not in source code. For long-lived authentication, commit login scripts to `.agents/skills/` so Devin can re-authenticate automatically.
    </Warning>
  </Accordion>

  <Accordion title="Custom system tools and PATH">
    Install system packages, custom binaries, and configure PATH in `initialize`.

    ```yaml theme={null}
    initialize:
      - name: Install system packages
        run: |
          apt-get update
          apt-get install -y \
            jq \
            ripgrep \
            fd-find \
            protobuf-compiler \
            libssl-dev

      - name: Install custom CLI tool
        run: |
          curl -L https://github.com/example/tool/releases/download/v1.0/tool-linux-amd64 \
            -o /usr/local/bin/mytool
          chmod +x /usr/local/bin/mytool

      - name: Add custom bin directory to PATH
        run: |
          mkdir -p ~/bin
          echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc

    knowledge:
      - name: tools
        contents: |
          Custom tools available:
          - mytool: installed at /usr/local/bin/mytool
          - Additional binaries can be placed in ~/bin
    ```
  </Accordion>

  <Accordion title="GitHub Actions for tool setup">
    Devin supports running GitHub Actions directly in a blueprint's `initialize` section. This is useful for installing specific tool versions via the same actions your CI uses.

    ```yaml theme={null}
    initialize:
      - name: "Install Node.js 20"
        uses: github.com/actions/setup-node@v4
        with:
          node-version: "20"

      - name: "Install Python 3.12"
        uses: github.com/actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: "Install Go 1.22"
        uses: github.com/actions/setup-go@v5
        with:
          go-version: "1.22"

      - name: "Install Java 21"
        uses: github.com/actions/setup-java@v4
        with:
          java-version: "21"
          distribution: "temurin"

      - name: "Install Gradle"
        uses: github.com/gradle/actions/setup-gradle@v4

      - name: "Install Ruby 3.3"
        uses: github.com/ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"

      - name: "Install additional tools"
        run: |
          pip install uv
          npm install -g pnpm turbo
          go install golang.org/x/tools/gopls@latest

    maintenance: |
      echo "All runtimes are available:"
      node --version
      python --version
      go version
      java --version
    ```

    <Info>
      Actions like `setup-node` and `setup-python` modify PATH and environment variables. Binaries installed by one action are available in all subsequent steps and in `maintenance`. Node.js and composite actions are supported; Docker actions are supported on Linux builds only. `uses` steps can't run in `maintenance`. See [GitHub Actions limitations](/onboard-devin/environment/github-actions#limitations).
    </Info>

    <Tip>
      You don't need GitHub Actions for basic tool setup. Direct shell commands (`nvm install 20`, `curl ... | sh`, `apt-get install`) work just as well and are often simpler. GitHub Actions are most useful when you want to match your CI setup exactly or need the convenience of actions like `setup-java` which handle multiple distributions.
    </Tip>
  </Accordion>

  <Accordion title="Local HTTPS reverse proxy for multiple apps">
    Run several services behind real-looking hostnames over HTTPS, such as `app.example.com`, `api.example.com`, and `admin.example.com`. Install a single reverse proxy in `initialize` and route each hostname to a different local upstream port.

    [Caddy](https://caddyserver.com/) handles routing and local TLS in one tool. A Caddyfile maps each hostname to an upstream, and `tls internal` auto-issues a trusted certificate per hostname from Caddy's built-in CA. `caddy trust` installs that CA root into the system trust store, and adding the same root to the NSS database lets the browser accept it.

    Upload your Caddyfile via the **File attachments** section of the blueprint editor; it is then available as `$FILE_CADDYFILE`.

    ```caddyfile Caddyfile theme={null}
    app.example.com {
      tls internal
      reverse_proxy 127.0.0.1:3000
    }

    api.example.com {
      tls internal
      reverse_proxy 127.0.0.1:3001
    }

    admin.example.com {
      tls internal
      reverse_proxy 127.0.0.1:3002
    }
    ```

    ```yaml theme={null}
    initialize:
      - name: Install Caddy and NSS tools
        run: |
          sudo apt-get install -y debian-keyring debian-archive-keyring \
            apt-transport-https curl libnss3-tools
          curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
            | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
          curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
            | sudo tee /etc/apt/sources.list.d/caddy-stable.list
          sudo apt-get update -qq
          sudo apt-get install -y caddy

      - name: Install Caddyfile, hosts entries, and trust internal CA
        run: |
          sudo install -m 0644 "$FILE_CADDYFILE" /etc/caddy/Caddyfile

          for host in app.example.com api.example.com admin.example.com; do
            grep -q " $host$" /etc/hosts \
              || echo "127.0.0.1 $host" | sudo tee -a /etc/hosts > /dev/null
          done

          sudo systemctl enable --now caddy
          sudo caddy trust

          mkdir -p ~/.pki/nssdb
          [ -f ~/.pki/nssdb/cert9.db ] \
            || certutil -d sql:$HOME/.pki/nssdb -N --empty-password

          CADDY_ROOT=/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt
          for _ in $(seq 1 30); do
            sudo test -f "$CADDY_ROOT" && break
            sleep 1
          done

          if sudo test -f "$CADDY_ROOT" \
             && ! certutil -d sql:$HOME/.pki/nssdb -L | grep -q "Caddy Local Authority"; then
            sudo install -o "$USER" -m 0644 "$CADDY_ROOT" /tmp/caddy-root.crt
            certutil -d sql:$HOME/.pki/nssdb -A -t "C,," \
              -n "Caddy Local Authority" -i /tmp/caddy-root.crt
            rm -f /tmp/caddy-root.crt
          fi
    ```

    <Info>
      The `/etc/hosts` loop is what makes `app.example.com` resolve to `127.0.0.1` inside the session. Add an entry for every hostname you put in the Caddyfile.
    </Info>

    <Tip>
      To add a service, append a three-line block to the Caddyfile and one entry to the `/etc/hosts` loop, then visit its hostname over HTTPS. Caddy mints a certificate on first request, so no per-app cert generation is needed.
    </Tip>
  </Accordion>
</AccordionGroup>

***

## Full-stack examples

These examples show how enterprise and org-level configurations combine. In practice, you'd split these across scopes. They're shown together here for reference.

<AccordionGroup>
  <Accordion title="Full enterprise stack (Artifactory)">
    A complete enterprise environment: corporate CA certificate, proxy, Java (Maven), Python (pip/uv), Node.js (npm), and Docker, all pointed at a single Artifactory instance.

    <Accordion title="Required secrets">
      **Network & trust (account-wide):**

      * `CORP_ROOT_CA_B64` — Base64-encoded corporate CA certificate
      * `CORP_HTTP_PROXY` — HTTP proxy URL
      * `CORP_HTTPS_PROXY` — HTTPS proxy URL
      * `CORP_NO_PROXY` — Hosts to bypass proxy

      **Registry credentials (org-wide):**

      * `ARTIFACTORY_USER` — Artifactory username
      * `ARTIFACTORY_TOKEN` — Artifactory API token or password
      * `ARTIFACTORY_MAVEN_URL` — Maven repository URL (e.g., `https://artifactory.example.com/artifactory/maven-virtual`)
      * `ARTIFACTORY_PYPI_URL` — PyPI repository URL (e.g., `https://user:token@artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple`)
      * `ARTIFACTORY_NPM_URL` — npm repository URL (e.g., `https://artifactory.example.com/artifactory/api/npm/npm-virtual`)
      * `ARTIFACTORY_DOCKER_URL` — Docker registry URL (e.g., `artifactory.example.com`)
    </Accordion>

    This would typically be split across three scopes:

    * **Account-wide (`initialize`):** Certificate and proxy
    * **Org-wide (`initialize`):** Language runtime installs
    * **Org-wide (`maintenance`):** Registry configuration containing literal references
    * **Session commands:** Explicitly supply secrets, source environment setup, and log in for individual Docker operations

    Shown here combined for reference:

    ```yaml theme={null}
    initialize:
      # ── Account-wide: network and trust ──────────────────────────────────────

      - name: Install corporate CA certificate
        run: |
          set -o pipefail
          printf '%s' "$CORP_ROOT_CA_B64" | base64 -d | openssl x509 -outform PEM \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates
          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null

      - name: Configure system-wide proxy
        run: |
          cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$CORP_HTTP_PROXY"
          export https_proxy="$CORP_HTTPS_PROXY"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$CORP_HTTP_PROXY"
          export HTTPS_PROXY="$CORP_HTTPS_PROXY"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

      # ── Org-wide: language runtimes ──────────────────────────────────────────

      - name: Install JDK 17 + Maven
        run: |
          set -o pipefail
          source /etc/profile.d/proxy.sh
          sudo --preserve-env=http_proxy,https_proxy,no_proxy,HTTP_PROXY,HTTPS_PROXY,NO_PROXY \
            apt-get update -qq
          sudo --preserve-env=http_proxy,https_proxy,no_proxy,HTTP_PROXY,HTTPS_PROXY,NO_PROXY \
            DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

          MAVEN_VERSION=3.9.9
          curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
            | sudo tar -xz -C /opt
          sudo ln -sf /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn

      - name: Install uv
        run: |
          set -o pipefail
          source /etc/profile.d/proxy.sh
          curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance:
      # ── Org-wide: registry configuration ───────────────────────────────────

      - name: Configure Maven → Artifactory
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml <<'EOF'
          <settings>
            <mirrors>
              <mirror>
                <id>artifactory</id>
                <mirrorOf>*</mirrorOf>
                <url>${env.ARTIFACTORY_MAVEN_URL}</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>artifactory</id>
                <username>${env.ARTIFACTORY_USER}</username>
                <password>${env.ARTIFACTORY_TOKEN}</password>
              </server>
            </servers>
          </settings>
          EOF

      - name: Configure pip/uv → Artifactory PyPI
        run: |
          mkdir -p ~/.config
          cat > ~/.config/private-pypi.sh <<'EOF'
          export PIP_INDEX_URL="$ARTIFACTORY_PYPI_URL"
          export UV_INDEX_URL="$ARTIFACTORY_PYPI_URL"
          EOF

      - name: Configure npm → Artifactory
        run: |
          npm config set registry '${ARTIFACTORY_NPM_URL}'
          REGISTRY_HOST=$(echo "$ARTIFACTORY_NPM_URL" | sed 's|https\?://||;s|/.*||')
          npm config set "//${REGISTRY_HOST}/:_authToken" '${ARTIFACTORY_TOKEN}'

    knowledge:
      - name: private_packages
        contents: |
          With the corresponding secrets available, source setup in each command:
            source /etc/profile.d/proxy.sh && mvn dependency:resolve
            source /etc/profile.d/proxy.sh && npm install
            source /etc/profile.d/proxy.sh && source ~/.config/private-pypi.sh && uv sync
      - name: private_container_images
        contents: |
          With the Artifactory and proxy secrets available, run in one shell.
          Replace team/image:tag with the image to pull:
            (
              set -euo pipefail
              umask 077
              source /etc/profile.d/proxy.sh
              DOCKER_CONFIG=$(mktemp -d)
              export DOCKER_CONFIG
              trap 'rm -rf "$DOCKER_CONFIG"' EXIT
              printf '%s' "$ARTIFACTORY_TOKEN" |
                docker login "$ARTIFACTORY_DOCKER_URL" \
                  --username "$ARTIFACTORY_USER" --password-stdin
              docker pull "$ARTIFACTORY_DOCKER_URL/team/image:tag"
            )
    ```

    <Info>
      In this example, all registries point at the same Artifactory instance but use different URL paths. Each package ecosystem has its own endpoint format. Maven, PyPI, npm, and Docker URLs are all different even for the same registry.
    </Info>
  </Accordion>

  <Accordion title="Multi-language with different registries">
    When different languages use different private registries (e.g., Maven from Nexus, npm from GitHub Packages, Python from Artifactory).

    <Accordion title="Required secrets">
      * `NEXUS_MAVEN_URL` — Nexus Maven repository URL
      * `NEXUS_USER` — Nexus username
      * `NEXUS_PASS` — Nexus password
      * `GITHUB_PACKAGES_TOKEN` — GitHub personal access token with `read:packages` scope
      * `ARTIFACTORY_USER` — Artifactory username
      * `ARTIFACTORY_TOKEN` — Artifactory API token
    </Accordion>

    Connect your Git provider and grant access to the repositories that host the Go modules through [Git integrations](/integrations/overview).

    ```yaml theme={null}
    maintenance:
      # Maven → Nexus
      - name: Configure Maven → Nexus
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml <<'EOF'
          <settings>
            <mirrors>
              <mirror>
                <id>nexus</id>
                <mirrorOf>*</mirrorOf>
                <url>${env.NEXUS_MAVEN_URL}</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>nexus</id>
                <username>${env.NEXUS_USER}</username>
                <password>${env.NEXUS_PASS}</password>
              </server>
            </servers>
          </settings>
          EOF

      # npm → GitHub Packages (scoped)
      - name: Configure npm → GitHub Packages
        run: |
          npm config set @myorg:registry https://npm.pkg.github.com
          npm config set //npm.pkg.github.com/:_authToken '${GITHUB_PACKAGES_TOKEN}'

      # Python → Artifactory
      - name: Configure pip → Artifactory
        run: |
          mkdir -p ~/.config
          cat > ~/.config/private-pypi.sh <<'EOF'
          export PIP_INDEX_URL="https://$ARTIFACTORY_USER:$ARTIFACTORY_TOKEN@artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple"
          EOF

      # Go → private modules via the connected Git provider
      - name: Configure Go private modules
        run: |
          go env -w GOPRIVATE=github.com/myorg/*

    knowledge:
      - name: private_python_packages
        contents: |
          With URL-encoded ARTIFACTORY_USER and ARTIFACTORY_TOKEN available, run:
            source ~/.config/private-pypi.sh && pip install -r requirements.txt
    ```
  </Accordion>

  <Accordion title="Air-gapped environment with private mirrors">
    In a fully air-gapped environment, Devin cannot reach any public URLs. All tools, runtimes, and packages must come from internal mirrors.

    <Accordion title="Required secrets">
      **Certificates:**

      * `CORP_ROOT_CA_B64` — Base64-encoded corporate CA certificate

      **Mirror access:**

      * `APT_MIRROR_URL` — Internal Ubuntu APT mirror URL
      * `MIRROR_USER` — Mirror authentication username
      * `MIRROR_PASS` — Mirror authentication password
      * `JDK_TARBALL_URL` — URL to download JDK tarball from internal mirror
      * `NODE_TARBALL_URL` — URL to download Node.js tarball from internal mirror

      **Package registries:**

      * `INTERNAL_MAVEN_URL` — Internal Maven registry URL
      * `INTERNAL_NPM_URL` — Internal npm registry URL
      * `INTERNAL_PYPI_URL` — Internal PyPI registry URL
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install corporate CA certificate
        run: |
          set -o pipefail
          printf '%s' "$CORP_ROOT_CA_B64" | base64 -d | openssl x509 -outform PEM \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates

      - name: Replace apt sources with internal mirror
        run: |
          sudo sed -i "s|http://archive.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
          sudo sed -i "s|http://security.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
          sudo apt-get update -qq

      - name: Install JDK from internal mirror
        run: |
          # Download JDK tarball from internal artifact store
          curl -fsSL -u "$MIRROR_USER:$MIRROR_PASS" "$JDK_TARBALL_URL" \
            | sudo tar -xz -C /usr/local
          sudo ln -sf /usr/local/jdk-17.*/bin/java /usr/local/bin/java
          sudo ln -sf /usr/local/jdk-17.*/bin/javac /usr/local/bin/javac
          echo "export JAVA_HOME=$(ls -d /usr/local/jdk-17.*)" \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Node.js from internal mirror
        run: |
          curl -fsSL -u "$MIRROR_USER:$MIRROR_PASS" "$NODE_TARBALL_URL" \
            | sudo tar -xz -C /usr/local --strip-components=1

    maintenance:
      - name: Configure all package managers for internal registry
        run: |
          # Maven
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml <<'EOF'
          <settings>
            <mirrors>
              <mirror>
                <id>internal</id>
                <mirrorOf>*</mirrorOf>
                <url>${env.INTERNAL_MAVEN_URL}</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>internal</id>
                <username>${env.MIRROR_USER}</username>
                <password>${env.MIRROR_PASS}</password>
              </server>
            </servers>
          </settings>
          EOF

          # npm
          npm config set registry '${INTERNAL_NPM_URL}'

          # pip
          mkdir -p ~/.config
          cat > ~/.config/private-pypi.sh <<'EOF'
          export PIP_INDEX_URL="$INTERNAL_PYPI_URL"
          EOF

    knowledge:
      - name: internal_python_packages
        contents: |
          With INTERNAL_PYPI_URL available, run:
            source ~/.config/private-pypi.sh && pip install -r requirements.txt
    ```

    <Warning>
      In air-gapped environments, all tools Devin needs (language runtimes, CLI tools, etc.) must be available on your internal mirrors. Public registries and download sites are unreachable.
    </Warning>
  </Accordion>

  <Accordion title="VPN + certificates + proxy + languages">
    A comprehensive enterprise setup combining VPN tooling, certificates, proxy configuration, and multi-language support. The blueprint installs tools and stores configuration with literal references. Connect the VPN explicitly when running operations that need internal access.

    <Accordion title="Required secrets">
      **VPN:**

      * `VPN_CONFIG_B64` — Base64-encoded OpenVPN config file

      **Network & trust:**

      * `CORP_ROOT_CA_B64` — Base64-encoded corporate CA certificate
      * `CORP_HTTP_PROXY` — HTTP proxy URL
      * `CORP_HTTPS_PROXY` — HTTPS proxy URL
      * `CORP_NO_PROXY` — Hosts to bypass proxy

      **Registry credentials:**

      * `MAVEN_REGISTRY_URL` — Maven registry URL
      * `NPM_REGISTRY_URL` — npm registry URL
      * `PYPI_REGISTRY_HOST` — PyPI registry hostname
      * `REGISTRY_USER` — Registry username (for Maven and pip)
      * `REGISTRY_PASS` — Registry password (for Maven and pip)
      * `REGISTRY_TOKEN` — npm auth token
    </Accordion>

    ```yaml theme={null}
    initialize:
      # 1. VPN client
      - name: Install VPN client
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openvpn

      # 2. DNS — resolve internal hostnames
      - name: Configure DNS
        run: |
          sudo mkdir -p /etc/systemd/resolved.conf.d
          cat << 'DNS' | sudo tee /etc/systemd/resolved.conf.d/corp.conf > /dev/null
          [Resolve]
          DNS=10.0.0.53
          Domains=corp.internal
          DNS
          sudo systemctl restart systemd-resolved || true

      # 3. Certificates — trust internal CAs
      - name: Install CA certificate
        run: |
          set -o pipefail
          printf '%s' "$CORP_ROOT_CA_B64" | base64 -d | openssl x509 -outform PEM \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates
          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null

      # 4. Proxy — route traffic through corporate proxy
      - name: Configure proxy
        run: |
          cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$CORP_HTTP_PROXY"
          export https_proxy="$CORP_HTTPS_PROXY"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$CORP_HTTP_PROXY"
          export HTTPS_PROXY="$CORP_HTTPS_PROXY"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

      # 5. Language runtimes
      - name: Install language runtimes over VPN
        run: |
          (
            set -euo pipefail
            umask 077
            VPN_DIR=$(mktemp -d)
            cleanup() {
              if [ -f "$VPN_DIR/openvpn.pid" ]; then
                sudo kill "$(cat "$VPN_DIR/openvpn.pid")" || true
              fi
              sudo rm -rf "$VPN_DIR"
            }
            trap cleanup EXIT
            printf '%s' "$VPN_CONFIG_B64" | base64 -d > "$VPN_DIR/corp.conf"
            sudo openvpn --config "$VPN_DIR/corp.conf" \
              --writepid "$VPN_DIR/openvpn.pid" --daemon
            for i in $(seq 1 30); do
              if ip link show tun0 >/dev/null 2>&1; then break; fi
              sleep 1
            done
            ip link show tun0 >/dev/null
            source /etc/profile.d/proxy.sh
            sudo --preserve-env=http_proxy,https_proxy,no_proxy,HTTP_PROXY,HTTPS_PROXY,NO_PROXY \
              apt-get update -qq
            sudo --preserve-env=http_proxy,https_proxy,no_proxy,HTTP_PROXY,HTTPS_PROXY,NO_PROXY \
              DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
            echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
              | sudo tee /etc/profile.d/java.sh > /dev/null
            npm install -g pnpm
            curl -LsSf https://astral.sh/uv/install.sh | sh
          )

    maintenance:
      - name: Configure Maven
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml <<'EOF'
          <settings>
            <mirrors>
              <mirror>
                <id>corp</id>
                <mirrorOf>*</mirrorOf>
                <url>${env.MAVEN_REGISTRY_URL}</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>corp</id>
                <username>${env.REGISTRY_USER}</username>
                <password>${env.REGISTRY_PASS}</password>
              </server>
            </servers>
          </settings>
          EOF

      - name: Configure npm
        run: |
          npm config set registry '${NPM_REGISTRY_URL}'
          NPM_HOST=$(echo "$NPM_REGISTRY_URL" | sed 's|https\?://||;s|/.*||')
          npm config set "//${NPM_HOST}/:_authToken" '${REGISTRY_TOKEN}'

      - name: Configure pip/uv
        run: |
          mkdir -p ~/.config
          cat > ~/.config/private-pypi.sh <<'EOF'
          export PIP_INDEX_URL="https://$REGISTRY_USER:$REGISTRY_PASS@${PYPI_REGISTRY_HOST}/simple"
          export UV_INDEX_URL="$PIP_INDEX_URL"
          EOF

    knowledge:
      - name: internal_packages
        contents: |
          Use the temporary OpenVPN setup/cleanup block from the template library
          when an operation needs VPN access. Inside that block, with the registry
          and proxy secrets available, replace the sample curl command with:
            source /etc/profile.d/proxy.sh
            mvn dependency:resolve
            npm install
            source ~/.config/private-pypi.sh
            uv sync
          Percent-encode the pip URL credentials. Disconnect the VPN and remove its
          temporary files when the operation finishes.
    ```

    <Info>
      **Bootstrap access is required.** The OpenVPN client must be preinstalled or installable before connecting the tunnel. `VPN_CONFIG_B64` must contain inline certificates/keys, use `dev tun0`, and connect without interactive authentication. The runtime downloads run inside one tunnel lifecycle and explicitly load the proxy environment. Repeat the VPN setup/cleanup block under [Network and connectivity](#network-and-connectivity) for session operations; the blueprint does not reconnect the VPN at session start.
    </Info>
  </Accordion>
</AccordionGroup>

***

## Tips for writing good blueprints

* **Test commands in a session first.** Run commands manually in a Devin session before adding them to your blueprint. This is faster than waiting for a full build cycle.
* **Use `initialize` for install-once tools, `maintenance` for deps.** Anything that takes minutes to install (compilers, large binaries, global tools) belongs in `initialize`. Quick dependency commands (`npm install`, `uv sync`) go in `maintenance`.
* **Keep `maintenance` commands fast.** Aim for under 2 minutes. These run during builds and are surfaced to the agent at session start.
* **Use `$ENVRC` for non-secret environment variables.** Don't write credentials or derived tokens to `$ENVRC`, `.bashrc`, or `.profile`. Source secret-dependent shell setup explicitly in the command that needs it.
* **Name your steps.** The expanded form with `name` fields makes build log failures much easier to identify.
* **Use subshells for monorepos.** `(cd packages/foo && npm install)` runs in a subshell so subsequent steps aren't affected by the directory change.
* **Use `npm install`, not `npm ci`.** `npm ci` deletes `node_modules` and reinstalls from scratch, which is slow for `maintenance`.
* **Use repo secrets for sensitive values.** Configure them in the **Secrets** tab of the repository's blueprint editor instead of hardcoding in blueprints.

For syntax details, see the [Blueprint reference](/onboard-devin/environment/blueprint-reference). For troubleshooting build failures, see [Declarative configuration > Troubleshooting](/onboard-devin/environment/blueprints#troubleshooting-builds).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.