diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml
new file mode 100644
index 0000000000..cf300669c7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation.yml
@@ -0,0 +1,56 @@
+name: "Documentation ๐"
+description: Did you find an error in our documentation? Report your findings here.
+title: "[DOC] - 
"
+labels: ["area: documentation ๐"]
+
+body:
+  - type: markdown
+    attributes:
+      value: |
+        # Welcome ๐
+
+        Thanks for using Nebari and taking some time to contribute to this project.
+
+        Please fill out each section below. This info allows Nebari maintainers to diagnose (and fix!) your issue as
+        quickly as possible.
+        Before submitting a bug, please make sure the issue hasn't been already addressed by searching through
+        [the past issues](https://github.com/nebari-dev/nebari-docs/issues).
+
+        Useful links:
+
+        - Documentation: https://www.nebari.dev
+        - Contribution guidelines: https://www.nebari.dev/community/
+
+  - type: checkboxes
+    attributes:
+      label: Preliminary Checks
+      description: Please make sure that you verify each checkbox and follow the instructions for them.
+      options:
+        - label: "This issue is not a question, feature request, RFC, or anything other than a bug report. Please post those things in GitHub Discussions: https://github.com/nebari-dev/nebari/discussions"
+          required: true
+  - type: textarea
+    validations:
+      required: true
+    attributes:
+      label: Summary
+      description: |
+        What problem(s) did you run into that caused you to request a fix to the documentation or additional
+        documentation? What questions do you think we should answer?
+
+  - type: textarea
+    validations:
+      required: true
+    attributes:
+      label: Steps to Resolve this Issue
+      description: |
+        How can the problem be solved? Are there any additional steps required? Do any other pages need to be updated?
+      value: |
+        1.
+        2.
+        3.
+        ...
+
+  - type: markdown
+    attributes:
+      value: >
+        Thanks for contributing ๐!
diff --git a/.github/workflows/build_push_docker.yaml b/.github/workflows/build_push_docker.yaml
new file mode 100644
index 0000000000..af4ff271e9
--- /dev/null
+++ b/.github/workflows/build_push_docker.yaml
@@ -0,0 +1,123 @@
+# Build and push images to:
+# GitHub Container Registry (ghcr.io)
+# Red Hat Container Registry (quay.io)
+name: "Build Docker Images"
+
+on:
+  workflow_dispatch: null
+  push:
+    branches:
+      - "*"
+    paths:
+      - "Dockerfile"
+      - "dask-worker/*"
+      - "jupyterhub/*"
+      - "jupyterlab/*"
+      - "nebari-workflow-controller/*"
+
+      - "scripts/*"
+
+      - ".github/workflows/build-push-docker.yaml"
+    tags:
+      - "*"
+
+env:
+  DOCKER_ORG: nebari
+  GPU_BASE_IMAGE: nvidia/cuda:12.8.1-base-ubuntu24.04
+  GPU_IMAGE_SUFFIX: gpu
+  BASE_IMAGE: ubuntu:24.04
+
+permissions:
+  contents: read
+  packages: write
+  id-token: write
+  security-events: write
+
+# https://docs.github.com/en/actions/using-jobs/using-concurrency
+concurrency:
+  # only cancel in-progress jobs or runs for the current workflow - matches against branch & tags
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  build-images:
+    name: "Build Docker Images"
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        dockerfile:
+          - jupyterlab
+          - jupyterhub
+          - dask-worker
+          - workflow-controller
+        platform:
+          - gpu
+          - cpu
+        exclude:
+          # excludes JupyterHub/GPU, Workflow Controller/GPU
+          - dockerfile: jupyterhub
+            platform: gpu
+          - dockerfile: workflow-controller
+            platform: gpu
+
+    steps:
+      - name: "Checkout Repository ๐๏ธ"
+        uses: actions/checkout@v3
+
+      - name: "Set up Docker Buildx ๐ ๏ธ"
+        uses: docker/setup-buildx-action@v2
+
+      - name: "Login to GitHub Container Registry ๐"
+        uses: docker/login-action@v2
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.BOT_GHCR_TOKEN }}
+
+      - name: "Login to Quay Container Registry ๐"
+        uses: docker/login-action@v2
+        with:
+          registry: quay.io
+          username: ${{ secrets.QUAY_USERNAME }}
+          password: ${{ secrets.QUAY_TOKEN }}
+
+      - name: "Set BASE_IMAGE and Image Suffix ๐ท"
+        if: ${{ matrix.platform == 'gpu' }}
+        run: |
+          echo "GPU Platform Matrix"
+          echo "BASE_IMAGE=$GPU_BASE_IMAGE" >> $GITHUB_ENV
+          echo "IMAGE_SUFFIX=-$GPU_IMAGE_SUFFIX" >> $GITHUB_ENV
+
+      - name: "Generate Docker images tags ๐ท๏ธ"
+        id: meta
+        uses: docker/metadata-action@v4
+        with:
+          images: |
+            "quay.io/${{ env.DOCKER_ORG }}/nebari-${{ matrix.dockerfile }}${{ env.IMAGE_SUFFIX }}"
+            "ghcr.io/${{ github.repository_owner }}/nebari-${{ matrix.dockerfile }}${{ env.IMAGE_SUFFIX }}"
+          tags: |
+            # branch event -> e.g. `main-f0f6994-20221001`
+            type=ref, event=branch, suffix=-{{sha}}-{{date 'YYYYMMDD'}}
+            # needed for integration tests
+            type=ref, event=branch
+            # on tag push -> e.g. `2022.10.1`
+            type=ref, event=tag
+
+      - name: "Inspect image dir tree ๐"
+        run: |
+          sudo apt-get install tree
+          tree .
+
+      - name: "Build docker images ๐ณ"
+        uses: docker/build-push-action@v3
+        with:
+          context: .
+          file: "Dockerfile"
+          target: ${{ matrix.dockerfile }}
+          tags: ${{ steps.meta.outputs.tags }}
+          push: ${{ github.event_name != 'pull_request' }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
+          build-args: BASE_IMAGE=${{ env.BASE_IMAGE }}
+          platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/docker_trivy.yaml b/.github/workflows/docker_trivy.yaml
new file mode 100644
index 0000000000..5644a3d5e9
--- /dev/null
+++ b/.github/workflows/docker_trivy.yaml
@@ -0,0 +1,40 @@
+name: Code Scanning
+
+on:
+  push:
+    branches: [ "main"]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ "main" ]
+
+permissions:
+  contents: read
+
+jobs:
+  SAST:
+    permissions:
+      contents: read # for actions/checkout to fetch code
+      security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
+      actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
+    name: Trivy config Scan
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Run Trivy vulnerability scanner in config mode
+        uses: aquasecurity/trivy-action@master
+        with:
+          scan-type: 'config'
+          hide-progress: true
+          format: 'sarif'
+          output: 'trivy-results.sarif'
+          ignore-unfixed: true
+          severity: 'CRITICAL,HIGH'
+          limit-severities-for-sarif: true
+
+      - name: Upload Trivy scan results to GitHub Security tab
+        uses: github/codeql-action/upload-sarif@v3
+        if: always()
+        with:
+          sarif_file: 'trivy-results.sarif'
diff --git a/.github/workflows/test_images.yaml b/.github/workflows/test_images.yaml
new file mode 100644
index 0000000000..d3a6a19620
--- /dev/null
+++ b/.github/workflows/test_images.yaml
@@ -0,0 +1,77 @@
+name: Test Docker images
+
+on:
+  pull_request:
+    paths:
+      - "Dockerfile.*"
+
+      - "dask-worker/*"
+      - "jupyterhub/*"
+      - "jupyterlab/*"
+
+      - "scripts/*"
+
+      - ".github/workflows/build-push-docker.yaml"
+      - ".github/workflows/test-images.yaml"
+
+env:
+  DOCKER_ORG: nebari
+  GITHUB_SHA: ${{ github.sha }}
+  GPU_BASE_IMAGE: nvidia/cuda:12.8.1-base-ubuntu24.04
+  GPU_IMAGE_SUFFIX: gpu
+  BASE_IMAGE: ubuntu:24.04
+
+# https://docs.github.com/en/actions/using-jobs/using-concurrency
+concurrency:
+  # only cancel in-progress jobs or runs for the current workflow - matches against branch & tags
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  build-test-images:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        dockerfile:
+          - jupyterlab
+          - jupyterhub
+          - dask-worker
+        platform:
+          - gpu
+          - cpu
+        exclude:
+          # excludes JupyterHub/GPU
+          - dockerfile: jupyterhub
+            platform: gpu
+    steps:
+      - name: Checkout Repository ๐
+        uses: actions/checkout@v3
+
+      - name: Lint Dockerfiles ๐
+        uses: jbergstroem/hadolint-gh-action@v1
+        with:
+          dockerfile: Dockerfile
+          output_format: tty
+          error_level: 0
+
+      - name: "Set BASE_IMAGE and Image Suffix ๐ท"
+        if: ${{ matrix.platform == 'gpu' }}
+        run: |
+          echo "GPU Platform Matrix"
+          echo "BASE_IMAGE=$GPU_BASE_IMAGE" >> $GITHUB_ENV
+          echo "IMAGE_SUFFIX=-$GPU_IMAGE_SUFFIX" >> $GITHUB_ENV
+
+      - name: "Set up Docker Buildx ๐ ๏ธ"
+        uses: docker/setup-buildx-action@v2
+
+      - name: Build Image ๐ 
+        uses: docker/build-push-action@v3
+        with:
+          context: .
+          file: "Dockerfile"
+          target: ${{ matrix.dockerfile }}
+          push: false
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
+          build-args: BASE_IMAGE=${{ env.BASE_IMAGE }}
+          platforms: linux/amd64,linux/arm64
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3fd8676936..24b3bfaa36 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -82,3 +82,28 @@ repos:
       - id: terraform_fmt
         args:
           - --args=-write=true
+
+  # Autoformat: markdown, yaml to ensure that it doesn't need to be updated in other repos
+  - repo: https://github.com/pre-commit/mirrors-prettier
+    rev: v2.6.1
+    hooks:
+      - id: prettier
+
+    # Misc...
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.0.1
+    # ref: https://github.com/pre-commit/pre-commit-hooks#hooks-available
+    hooks:
+      # Autoformat: Makes sure files end in a newline and only a newline.
+      - id: end-of-file-fixer
+
+      # Trims trailing whitespace.
+      - id: trailing-whitespace
+        args: [--markdown-linebreak-ext=md]
+
+      # Lint: Check for files with names that would conflict on a
+      # case-insensitive filesystem like MacOS HFS+ or Windows FAT.
+      - id: check-case-conflict
+
+      # Lint: Checks that non-binary executables have a proper shebang.
+      - id: check-executables-have-shebangs
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000000..b634b69fcb
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,158 @@
+FROM ubuntu:24.04 AS builder
+LABEL MAINTAINER="Nebari development team"
+
+RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
+    --mount=type=cache,target=/var/lib/apt,sharing=locked \
+    apt-get update && apt-get install -y --no-install-recommends \
+    wget \
+    bzip2 \
+    ca-certificates \
+    curl \
+    git 
+
+COPY scripts /opt/scripts
+
+ENV MAMBAFORGE_VERSION=4.13.0-1 \
+    MAMBAFORGE_AARCH64_SHA256=69e3c90092f61916da7add745474e15317ed0dc6d48bfe4e4c90f359ba141d23 \
+    MAMBAFORGE_X86_64_SHA256=412b79330e90e49cf7e39a7b6f4752970fcdb8eb54b1a45cc91afe6777e8518c \
+    PATH=/opt/conda/bin:${PATH}:/opt/scripts
+
+
+RUN /opt/scripts/install-conda.sh
+
+
+
+# ========== dask-worker install ===========
+FROM builder AS dask-worker
+COPY dask-worker/environment.yaml /opt/dask-worker/environment.yaml
+RUN --mount=type=cache,target=/opt/conda/pkgs,sharing=locked \
+    --mount=type=cache,target=/root/.cache/pip,sharing=locked \
+    /opt/scripts/install-conda-environment.sh /opt/dask-worker/environment.yaml 'false'
+
+ENV LD_LIBRARY_PATH=/usr/local/nvidia/lib64
+ENV NVIDIA_PATH=/usr/local/nvidia/bin
+ENV PATH="$NVIDIA_PATH:$PATH"
+
+COPY dask-worker /opt/dask-worker
+RUN /opt/dask-worker/postBuild
+
+
+
+
+
+# ========== jupyterhub install ===========
+FROM builder AS jupyterhub
+COPY jupyterhub/environment.yaml /opt/jupyterhub/environment.yaml
+RUN --mount=type=cache,target=/opt/conda/pkgs,sharing=locked \
+    --mount=type=cache,target=/root/.cache/pip,sharing=locked \
+    /opt/scripts/install-conda-environment.sh /opt/jupyterhub/environment.yaml 'false'
+
+COPY jupyterhub /opt/jupyterhub
+RUN /opt/jupyterhub/postBuild
+
+WORKDIR /srv/jupyterhub
+
+# So we can actually write a db file here
+RUN fix-permissions /srv/jupyterhub
+
+CMD ["jupyterhub", "--config", "/usr/local/etc/jupyterhub/jupyterhub_config.py"]
+
+
+
+
+# ========== jupyterlab base ===========
+FROM builder AS intermediate
+ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 \
+    CONDA_DIR=/opt/conda \
+    DEFAULT_ENV=default
+RUN chmod -R a-w ~
+ENV TZ=UTC \
+    PATH=/opt/conda/envs/${DEFAULT_ENV}/bin:/opt/conda/bin:${PATH}:/opt/scripts
+# Set timezone
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
+    --mount=type=cache,target=/var/lib/apt,sharing=locked \
+    apt-get update && apt-get install -y --no-install-recommends \
+    locales \
+    libnss-wrapper \
+    htop \
+    tree \
+    zip \
+    unzip \
+    openssh-client \
+    tmux \
+    xvfb \
+    nano \
+    vim \
+    emacs
+
+
+# ========== jupyterlab install ===========
+FROM intermediate AS jupyterlab
+ENV CONDA_DIR=/opt/conda \
+    DEFAULT_ENV=default \
+    LD_LIBRARY_PATH=/usr/local/nvidia/lib64 \
+    NVIDIA_PATH=/usr/local/nvidia/bin
+
+ENV PATH="$NVIDIA_PATH:$PATH"
+
+RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
+    --mount=type=cache,target=/var/lib/apt,sharing=locked \
+    apt-get update && apt-get install -y --no-install-recommends \
+    zsh \
+    neovim \
+    libgl1 \ 
+    libglx-mesa0 \
+    libxrandr2 \
+    libxss1 \
+    libxcursor1 \
+    libxcomposite1 \
+    libasound2t64 \
+    libxi6 \
+    libxtst6 \
+    libfontconfig1 \
+    libxrender1 \
+    libosmesa6 \
+    gnupg \
+    pinentry-curses \
+    git-lfs
+
+ARG SKIP_CONDA_SOLVE=no
+COPY jupyterlab/environment.yaml /opt/jupyterlab/environment.yaml
+RUN --mount=type=cache,target=/opt/conda/pkgs,sharing=locked \
+    --mount=type=cache,target=/root/.cache/pip,sharing=locked \
+    if [ "${SKIP_CONDA_SOLVE}" != "no" ];then  \
+    ENV_FILE=/opt/jupyterlab/conda-linux-64.lock ; \
+    else  \
+    ENV_FILE=/opt/jupyterlab/environment.yaml ; \
+    fi ; \
+    /opt/scripts/install-conda-environment.sh "${ENV_FILE}" 'true'
+
+# ========== code-server install ============
+ENV PATH=/opt/conda/envs/${DEFAULT_ENV}/share/code-server/bin:${PATH}
+
+COPY jupyterlab /opt/jupyterlab
+RUN /opt/jupyterlab/postBuild
+
+
+
+
+
+# ========== nebari-workflow-controller install ============
+FROM intermediate AS workflow-controller
+
+ARG SKIP_CONDA_SOLVE=no
+COPY nebari-workflow-controller/environment.yaml /opt/nebari-workflow-controller/environment.yaml
+RUN --mount=type=cache,target=/opt/conda/pkgs,sharing=locked \
+    --mount=type=cache,target=/root/.cache/pip,sharing=locked \
+    if [ "${SKIP_CONDA_SOLVE}" != "no" ];then  \
+    ENV_FILE=/opt/nebari-workflow-controller/conda-linux-64.lock ; \
+    else  \
+    ENV_FILE=/opt/nebari-workflow-controller/environment.yaml ; \
+    fi ; \
+    /opt/scripts/install-conda-environment.sh "${ENV_FILE}" 'true'
+
+COPY nebari-workflow-controller /opt/nebari-workflow-controller
+
+CMD ["python", "-m", "nebari_workflow_controller"]
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000000..5a0553c885
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,135 @@
+
+
+  
+  
+  
+
+
+
+---
+
+# Nebari base Docker images
+
+| Information | Links                                                                                                                                                                                                                                                                                                                                                                |
+| :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Project     | [](https://opensource.org/licenses/BSD-3-Clause) [][nebari-docs] |
+| Community   | [][nebari-discussions] [][nebari-docker-issues]                     |
+| CI          |                                                                                                                                                                                                            |
+
+- [Nebari base Docker images](#nebari-base-docker-images)
+  - [Getting started โก๏ธ](#getting-started-๏ธ)
+    - [Prerequisites ๐ป](#prerequisites-)
+    - [Building the Docker images ๐ ](#building-the-docker-images-)
+    - [Pre-commit hooks ๐งน](#pre-commit-hooks-)
+  - [Reporting an issue ๐](#reporting-an-issue-)
+  - [Contributions ๐ค](#contributions-)
+  - [License ๐](#license-)
+
+This repository contains the source code for Docker (container) images used by the [Nebari platform][nebari-docs]. It also contains an automated means of building and pushing these images to public container registries through [GitHub actions][nebari-docker-actions]. Currently, these images are built and pushed to the following registries:
+
+**GitHub Container Registry (ghcr.io)**
+
+- [`nebari-jupyterlab`](https://github.com/orgs/nebari-dev/packages/container/package/nebari-jupyterlab)
+- [`nebari-jupyterlab-gpu`](https://github.com/orgs/nebari-dev/packages/container/package/nebari-jupyterlab-gpu)
+- [`nebari-jupyterhub`](https://github.com/orgs/nebari-dev/packages/container/package/nebari-jupyterhub)
+- [`nebari-dask-worker`](https://github.com/orgs/nebari-dev/packages/container/package/nebari-dask-worker)
+- [`nebari-dask-worker-gpu`](https://github.com/orgs/nebari-dev/packages/container/package/nebari-dask-worker-gpu)
+
+**Quay Container Registry (quay.io)**
+
+- [`nebari-jupyterlab`](https://quay.io/repository/nebari/nebari-jupyterlab)
+- [`nebari-jupyterlab-gpu`](https://quay.io/repository/nebari/nebari-jupyterlab-gpu)
+- [`nebari-jupyterhub`](https://quay.io/repository/nebari/nebari-jupyterhub)
+- [`nebari-dask-worker`](https://quay.io/repository/nebari/nebari-dask-worker)
+- [`nebari-dask-worker-gpu`](https://quay.io/repository/nebari/nebari-dask-worker-gpu)
+
+## Getting started โก๏ธ
+
+Whether you want to contribute to this project or whether you wish use these images, to get started, fork this repo and then clone the forked repo onto your local machine.
+
+### Prerequisites ๐ป
+
+- [`docker`](https://docs.docker.com/get-docker/), make sure to read the [Docker official documentation on how to install Docker on your machine](https://docs.docker.com/get-docker/).
+- [pre-commit](https://pre-commit.com/), which can be installed with:
+
+  ```bash
+  pip install pre-commit
+  # or using conda
+  conda install -c conda-forge pre-commit
+  ```
+
+### Building the Docker images ๐ 
+
+From the repository's root folder, you can build these images locally by running the listed commands on your terminal.
+
+- To build nebari-jupyterlab
+
+  ```shell
+  make jupyterlab
+  ```
+
+- To build nebari-jupyterhub
+
+  ```shell
+  make jupyterhub
+  ```
+
+- To build nebari-dask-worker
+
+  ```shell
+  make dask-worker
+  ```
+
+- To build nebari-workflow-controller
+
+  ```shell
+  make workflow-controller
+  ```
+
+- To build all of the images
+  
+  ```shell
+  make all
+  ```
+- To delete built images
+  
+  ```shell
+  make clean
+  ```
+
+> **NOTE**
+> It is extremely important to pin specific packages `dask-gateway` and `distributed` as they need to run the same version for the `dask-workers` to work as expected.
+
+### Pre-commit hooks ๐งน
+
+This repository uses the `prettier` pre-commit hook to standardize our YAML and markdown structure.
+To install and run it, use these commands from the repository root:
+
+```bash
+# install the pre-commit hooks
+pre-commit install
+
+# run the pre-commit hooks
+pre-commit run --all-files
+```
+
+## Reporting an issue ๐
+
+If you encounter an issue or want to make suggestions on how we can make this project better, feel free to [open an issue on this repository's issue tracker](https://github.com/nebari-dev/nebari-docker-images/issues/new/choose).
+
+## Contributions ๐ค
+
+Thinking about contributing to this repository or any other in the Nebari org? Check out our
+[Contribution Guidelines](https://nebari.dev/community).
+
+## License ๐
+
+[Nebari is BSD3 licensed](LICENSE).
+
+
+
+[nebari-docker-repo]: https://github.com/nebari-dev/nebari-docker-images
+[nebari-docker-issues]: https://github.com/nebari-dev/nebari-docker-images/issues/new/choose
+[nebari-docker-actions]: https://github.com/nebari-dev/nebari-docker-images/actions
+[nebari-discussions]: https://github.com/orgs/nebari-dev/discussions
+[nebari-docs]: https://nebari.dev
diff --git a/docker/dask-worker/environment.yaml b/docker/dask-worker/environment.yaml
new file mode 100644
index 0000000000..b41fc82d33
--- /dev/null
+++ b/docker/dask-worker/environment.yaml
@@ -0,0 +1,9 @@
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+name: base
+channels:
+  - conda-forge
+dependencies:
+  # dask
+  - nebari-dask
diff --git a/docker/dask-worker/postBuild b/docker/dask-worker/postBuild
new file mode 100644
index 0000000000..72cee969b8
--- /dev/null
+++ b/docker/dask-worker/postBuild
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+set -euo pipefail
+
+# A workaround that allows a command to run in a
+# specific conda environment
+cat </opt/conda-run-worker
+#!/bin/bash
+set -xe
+
+source activate \$CONDA_ENVIRONMENT
+dask-worker "\$@"
+EOF
+
+cat </opt/conda-run-scheduler
+#!/bin/bash
+set -xe
+
+source activate \$CONDA_ENVIRONMENT
+dask-scheduler "\$@"
+EOF
+
+chmod 755 /opt/conda-run-worker
+chmod 755 /opt/conda-run-scheduler
diff --git a/docker/jupyterhub/environment.yaml b/docker/jupyterhub/environment.yaml
new file mode 100644
index 0000000000..d313d3e3a5
--- /dev/null
+++ b/docker/jupyterhub/environment.yaml
@@ -0,0 +1,17 @@
+name: base
+channels:
+  - conda-forge
+dependencies:
+  - pip==21.1.2
+  - jupyterhub==5.3.0
+  - jupyterhub-kubespawner==6.2.0
+  - oauthenticator==16.3.0
+  - escapism==1.0.1
+  - python-kubernetes
+  - kubernetes_asyncio==29.0.0
+  - jupyterhub-idle-culler==1.2.1
+  - sqlalchemy==1.4.46
+  - pip:
+    - nebari-jupyterhub-theme==2024.7.1
+    - python-keycloak==0.26.1
+    - jhub-apps==2025.2.1
diff --git a/docker/jupyterhub/postBuild b/docker/jupyterhub/postBuild
new file mode 100644
index 0000000000..6b7e4da137
--- /dev/null
+++ b/docker/jupyterhub/postBuild
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+set -euo pipefail
diff --git a/docker/jupyterlab/environment.yaml b/docker/jupyterlab/environment.yaml
new file mode 100644
index 0000000000..ceac12c947
--- /dev/null
+++ b/docker/jupyterlab/environment.yaml
@@ -0,0 +1,69 @@
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+name: default
+channels:
+  - conda-forge
+dependencies:
+  # general
+  - pip
+
+  # jupyterhub/jupyterlab
+  - nb_conda_kernels
+  - ipython > 7
+  - jupyter-server-proxy >=4.4.0
+  - "jupyter_server>=2.13.0"
+  - jupyterlab==4.4.2
+  - jupyter_client
+  - jupyter_console
+  - jupyterhub==5.3.0
+  - nbconvert
+  - nbval
+
+  # jupyterhub extension
+
+  # jupyterlab extensions
+  - dask_labextension >= 5.3.0
+  - jupyterlab-git >=0.30.0
+  - sidecar >=0.5.0
+  - ipywidgets >= 8.0.0
+  - ipyleaflet >=0.13.5
+  - pyviz_comms >=3.0.3
+  - jupyter-resource-usage >=0.6.0
+  - nbgitpuller
+  - jupyterlab_code_formatter
+  - jupyterlab-spellchecker >= 0.7.3
+  - jupyterlab-pioneer
+  - jupyter-ai
+  - jupyterlab-favorites >=3.2.1
+  - jupyter-scheduler >=2.8.0,<3.0.0  # >=2.8 due to https://github.com/conda-forge/jupyter_scheduler-feedstock/issues/46
+
+  # viz tools
+  - param
+  - python-graphviz
+  - plotly >=5.0
+  - ipympl >=0.9.6
+  - bokeh >=3.5.2
+
+  # testing, docs, linting
+  - pytest
+  - hypothesis
+  - flake8
+  - sphinx
+  - pytest-cov
+  - black
+  - isort
+  - importnb
+  - git-lfs
+
+  - pip:
+      # vscode jupyterlab launcher
+      - git+https://github.com/betatim/vscode-binder
+      - jupyterlab_nvdashboard==0.12.0
+      - argo-jupyter-scheduler==2024.6.1
+      - jhub-apps==2025.2.1
+      - jupyterlab-nebari-mode==0.3.0
+      - jupyterlab-conda-store==2024.11.1
+      - jupyterlab-launchpad==1.0.3
+      - jupyterlab-gallery==0.6.3
+      - jupyterlab-jhub-apps==0.3.1
diff --git a/docker/jupyterlab/postBuild b/docker/jupyterlab/postBuild
new file mode 100644
index 0000000000..67fb479153
--- /dev/null
+++ b/docker/jupyterlab/postBuild
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+set -x
+
+# install code-server extension
+sh /opt/scripts/install-code-server.sh "/opt/conda/envs/${DEFAULT_ENV}/share"
+
+# if DEFAULT_ENV is unset ${DEFAULT_ENV+x} expands to nothing otherwise
+# it substitutes the string x. This allows us to check if the variable
+# is set without triggering an unbound variable error
+if [[ -z "${DEFAULT_ENV+x}" ]]; then
+    fix-permissions /opt/conda/bin
+else
+    fix-permissions "/opt/conda/envs/${DEFAULT_ENV}"
+fi
diff --git a/docker/makefile b/docker/makefile
new file mode 100644
index 0000000000..c22d0558d2
--- /dev/null
+++ b/docker/makefile
@@ -0,0 +1,27 @@
+IMAGES := jupyterhub jupyterlab dask-worker workflow-controller
+DOCKERFILE := Dockerfile
+CONTEXT := .
+
+.PHONY: all $(IMAGES) clean
+
+# Build all images
+all: $(IMAGES)
+
+# Build individual images
+jupyterhub:
+	docker build -t nebari-dev/nebari-docker-images:nebari-jupyterhub -f $(DOCKERFILE) $(CONTEXT) --target jupyterhub
+
+jupyterlab:
+	docker build -t nebari-dev/nebari-docker-images:nebari-jupyterlab -f $(DOCKERFILE) $(CONTEXT) --target jupyterlab
+
+dask-worker:
+	docker build -t nebari-dev/nebari-docker-images:nebari-dask-worker -f $(DOCKERFILE) $(CONTEXT) --target dask-worker
+
+workflow-controller:
+	docker build -t nebari-dev/nebari-docker-images:nebari-workflow-controller -f $(DOCKERFILE) $(CONTEXT) --target workflow-controller
+
+# Clean up images
+clean:
+	@for image in $(IMAGES); do \
+		docker rmi nebari-dev/nebari-docker-images:nebari-$$image; \
+	done
diff --git a/docker/nebari-workflow-controller/environment.yaml b/docker/nebari-workflow-controller/environment.yaml
new file mode 100644
index 0000000000..62eef8bb36
--- /dev/null
+++ b/docker/nebari-workflow-controller/environment.yaml
@@ -0,0 +1,8 @@
+name: default
+channels:
+  - conda-forge
+dependencies:
+  - python=3.10
+  - pip
+  - pip:
+      - nebari-workflow-controller==2023.7.1
diff --git a/docker/scripts/fix-permissions b/docker/scripts/fix-permissions
new file mode 100644
index 0000000000..8e9926c02c
--- /dev/null
+++ b/docker/scripts/fix-permissions
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+# uses find to avoid touching files that already have the right permissions
+# right permissions are:
+# world rX, we have no guarantees of uids or gids upon
+# deployment so we want files accessible to all.
+
+set -e
+for d in "$@"; do
+  find "$d" \
+    ! -perm -o+rX \
+    -exec chmod o+rX {} \;
+done
diff --git a/docker/scripts/install-code-server.sh b/docker/scripts/install-code-server.sh
new file mode 100644
index 0000000000..9c5700c65e
--- /dev/null
+++ b/docker/scripts/install-code-server.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+set -xe
+DEFAULT_PREFIX="${1}"
+shift # path to environment yaml or lock file
+CODE_SERVER_VERSION=4.23.1
+
+mkdir -p ${DEFAULT_PREFIX}/code-server
+cd ${DEFAULT_PREFIX}/code-server
+
+# Fetch the snapshot of https://code-server.dev/install.sh as of the time of writing
+wget --quiet https://raw.githubusercontent.com/coder/code-server/v4.23.1/install.sh
+expected_sum=ef0324043bc7493989764315e22bbc85c38c4e895549538b7e701948b64495e6
+
+if [[ ! $(sha256sum install.sh) == "${expected_sum}  install.sh" ]]; then
+    echo Unexpected hash from code-server install script
+    exit 1
+fi
+
+mkdir /opt/tmpdir
+sh ./install.sh --method standalone --prefix /opt/tmpdir --version ${CODE_SERVER_VERSION}
+
+mv /opt/tmpdir/lib/code-server-${CODE_SERVER_VERSION}/* ${DEFAULT_PREFIX}/code-server
+rm -rf /opt/tmpdir
diff --git a/docker/scripts/install-conda-environment.sh b/docker/scripts/install-conda-environment.sh
new file mode 100644
index 0000000000..5ad070dfcc
--- /dev/null
+++ b/docker/scripts/install-conda-environment.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+set -xe
+ENV_FILE="${1}"
+shift # path to environment yaml or lock file
+NEW_ENV="${1}"
+shift # true or false indicating whether env update should occur
+
+# Capture last optional arg or set a ENV_NAME. This can be changed but be
+# careful... setting the path for both the dockerfile and runtime container
+# can be tricky
+if [[ -z "${1+x}" ]] || [[ "${1}" == "" ]]; then
+    ENV_NAME=default
+else
+    ENV_NAME="${1}"
+    shift
+fi
+
+# Set a default value for skipping the conda solve (using a lock file).
+: ${SKIP_CONDA_SOLVE:=no}
+
+# ==== install conda dependencies ====
+
+if ! ${NEW_ENV}; then
+    if [[ $(basename "${ENV_FILE}") =~ "*lock*" ]]; then
+        echo "${ENV_FILE} should not be a lock file as this is not  supported when \
+            only updating the conda environment. Consider setting NEW_ENV to yes."
+        exit 1
+    fi
+    echo Installing into current conda environment
+    mamba env update -f "${ENV_FILE}"
+
+# Env not being updated... create one now:
+elif [[ "${SKIP_CONDA_SOLVE}" == "no" ]]; then
+    mamba env create --prefix=/opt/conda/envs/${ENV_NAME} -f "${ENV_FILE}"
+elif [[ "${SKIP_CONDA_SOLVE}" == "yes" ]]; then
+    mamba create --prefix=/opt/conda/envs/${ENV_NAME} --file "${ENV_FILE}"
+
+    # This needs to be set using the ENV directive in the docker file
+    PATH="/opt/conda/envs/${ENV_NAME}/bin:${PATH}"
+    # For now install pip section manually. We could consider using pip-tools...
+    # See https://github.com/conda-incubator/conda-lock/issues/4
+    pip install https://github.com/dirkcgrunwald/jupyter_codeserver_proxy-/archive/5596bc9c2fbd566180545fa242c659663755a427.tar.gz
+else
+    echo "SKIP_CONDA_SOLVE should be yes or no instead got: '${SKIP_CONDA_SOLVE}'"
+    exit 1
+fi
+
+# ========= list dependencies ========
+/opt/conda/bin/conda list
+
+# ========== cleanup conda ===========
+/opt/conda/bin/mamba clean -afy
+# remove unnecissary files (status, js.maps)
+find /opt/conda/ -follow -type f -name '*.a' -delete
+find /opt/conda/ -follow -type f -name '*.js.map' -delete
+
+# Fix permissions
+fix-permissions "/opt/conda/envs/${ENV_NAME}" || fix-permissions /opt/conda/bin
diff --git a/docker/scripts/install-conda.sh b/docker/scripts/install-conda.sh
new file mode 100644
index 0000000000..feda68b221
--- /dev/null
+++ b/docker/scripts/install-conda.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+set -xe
+
+# Requires environment MAMBAFORGE_SHA256, MINIFORGE_VERSION, and DEFAULT_ENV
+arch=$(uname -i)
+wget --quiet -O mambaforge.sh https://github.com/conda-forge/miniforge/releases/download/$MAMBAFORGE_VERSION/Mambaforge-Linux-$arch.sh
+
+if [[ $arch == "aarch64" ]]; then
+  echo "${MAMBAFORGE_AARCH64_SHA256} mambaforge.sh" >mambaforge.checksum
+elif [[ $arch == "x86_64" ]]; then
+  echo "${MAMBAFORGE_X86_64_SHA256} mambaforge.sh" >mambaforge.checksum
+else
+  echo "Unsupported architecture: $arch"
+  exit 1
+fi
+
+echo $(sha256sum -c mambaforge.checksum)
+
+if [ $(sha256sum -c mambaforge.checksum | awk '{print $2}') != "OK" ]; then
+  echo Error when testing checksum
+  exit 1
+fi
+
+# Install Mamba and clean-up
+if [ -d "/opt/conda" ]; then
+  sh ./mambaforge.sh -b -u -p /opt/conda
+else
+  sh ./mambaforge.sh -b -p /opt/conda
+fi
+
+rm mambaforge.sh mambaforge.checksum
+
+mamba --version
+mamba clean -afy
+
+ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh
+
+mkdir -p /etc/conda
+cat </etc/conda/condarc
+always_yes: true
+changeps1: false
+auto_update_conda: false
+aggressive_update_packages: []
+envs_dirs:
+ - /home/conda/environments
+EOF
+
+# Fix permissions in accordance with jupyter stack permissions
+# model
+fix-permissions /opt/conda /etc/conda /etc/profile.d
diff --git a/docker/scripts/install-gitlfs.sh b/docker/scripts/install-gitlfs.sh
new file mode 100644
index 0000000000..47dc9177f0
--- /dev/null
+++ b/docker/scripts/install-gitlfs.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# Copyright (c) Nebari Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+set -xe
+
+# Adding the packagecloud repository for git-lfs installation
+wget --quiet -O script.deb.sh https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh
+expected_sum=8c4d07257b8fb6d612b6085f68ad33c34567b00d0e4b29ed784b2a85380f727b
+
+if [[ ! $(sha256sum script.deb.sh) == "${expected_sum}  script.deb.sh" ]]; then
+    echo Unexpected hash from git-lfs install script
+    exit 1
+fi
+
+# Install packagecloud's repository signing key and add repository to apt
+bash ./script.deb.sh
+
+# Install git-lfs
+apt-get install -y --no-install-recommends git-lfs