Skip to content

Workspace template examples

A lab's workspace templates describe the environments students can request. Each template is one workspace flavor: an image, an optional git repo to clone, resources, and anything extra the workshop needs (a database, a Docker daemon, some VS Code extensions).

This page is a cookbook of complete, copy-pasteable examples. For the field-by-field reference and the wizard form, see Configure workspaces.

Using these examples

  1. Go to New Lab and walk through to the Templates step — or open Add Template on an existing lab's detail page, which is the same editor.
  2. Choose Paste YAML.
  3. Paste an example below (or save it as a .yaml file and use Upload file).
  4. Click Validate — it checks the document without creating anything.
  5. Finish the wizard and click Create Lab (or Add Template in the drawer).

YAML wins while it is selected

While Paste YAML is selected, the form fields are ignored. Switching back to Build with a form does not carry your YAML edits over.

Only name is required, and template names must be unique within a lab. Unknown keys are rejected rather than ignored, so a typo like imagee: fails validation instead of silently shipping a lab without its image.

Key reference

Key Type Notes
name string Required. Shown in the student template selector; unique per lab.
ide string Legacy — omit it. Workspaces always run code-server. Older labs carrying openvscode still load; the value is rewritten to code-server.
image string Container image override. Defaults to codercom/code-server:latest.
git_repo string Cloned into the workspace on first start, into an empty workspace only.
git_branch string Clones a single branch. Default branch when unset.
git_folder string Subfolder of the repo the IDE opens. Repo root when unset.
git_shallow bool true clones only the latest commit (--depth 1): faster first start on a large repo, but no git history. See Faster first start.
cpu string Resource request, e.g. 500m. Quote plain numbers: "2".
memory string Resource request, e.g. 4Gi.
cpu_limit string Resource limit override. Empty means it matches cpu (the request).
memory_limit string Resource limit override. Empty means it matches memory (the request).
disk_size string Volume size, e.g. 10Gi. Defaults to 5Gi — it sizes the volume, it does not decide whether there is one.
ephemeral bool true drops the persistent volume: student work is lost whenever the pod is rescheduled. See Persistence.
storage_class string StorageClass for the volume. Empty uses the cluster default, which is correct on OVHcloud and Azure.
startup_script string Shell commands run before the IDE starts. Best-effort.
dotfiles_repo string Cloned to ~/.dotfiles; its install.sh / setup.sh / bootstrap.sh runs if present.
extensions list VS Code extension IDs installed on start.
vscode_settings map VS Code settings written to the student's user settings.json on first start. See VS Code settings.
env map Environment variables for the workspace container.
sidecars list Extra containers in the pod: name, image, ports, env, privileged, capabilities.
mounts list Existing ConfigMaps/Secrets: type (configmap | secret), name, path. Mounted read-only.
image_pull_secrets list Names of registry credentials used to pull this template's images. See Private registries and repositories.
git_auth_secret string Name of a git credential used to clone a private git_repo. http(s) only. See Private registries and repositories.
devcontainer map Builds the image from the repo's devcontainer.json instead of using image. See Devcontainer workshops.

Minimal

The smallest valid document. Students get a code-server workspace on the default image with a 5Gi persistent volume.

workspace_templates:
  - name: default

Persistence

Every workspace gets a persistent volume unless the template opts out. The volume is mounted over the student's whole home directory, so their files, their IDE settings and the extensions they install all come back when the pod is rescheduled onto another node — which happens routinely, on cluster upgrades, node failures and autoscaler consolidation.

A devcontainer workspace is the exception: its volume holds the project folder and the IDE's settings and extensions, but not the rest of the home directory — see the caveats in that section.

disk_size sizes that volume (5Gi by default). storage_class places it; left empty the cluster's default StorageClass is used, which is what you want on an EasyLab-provisioned OVHcloud or Azure cluster.

ephemeral: true throws the student's work away

Setting ephemeral: true replaces the volume with an EmptyDir tied to the node. Anything the student writes — including their cloned repo, which is silently re-cloned from scratch — is lost whenever the pod moves. It exists for two cases: throwaway demo labs, and very large cohorts on Azure, where hundreds of simultaneous disk attachments can exceed the cluster's attach limits and leave pods stuck (see Azure). It cannot be combined with disk_size or storage_class; that is rejected when the lab is saved.

Bring-your-own clusters

A workspace volume is only as durable as the StorageClass behind it. If your cluster's default provisioner is node-local (local-path, hostPath), the volume is pinned to one node and the data is lost on a reschedule regardless of disk_size. Set storage_class to a network-attached class in that case.

A git-backed workshop

The most common setup: clone the exercises repo, open a subfolder, install the language extension.

workspace_templates:
  - name: go-workshop
    git_repo: https://gitlab.com/yodamad/go-workshop.git
    git_branch: main
    git_folder: exercises
    extensions:
      - golang.go

The repo is cloned only when the volume is empty — a restarting workspace keeps the student's own commits and edits rather than being reset.

Faster first start

Most of a student's wait happens while the workspace pod starts, not in EasyLab. Two things help:

  • Images are pre-pulled for you. Every lab runs a small DaemonSet (easylab-prepull-<lab id>, in the workspace namespace) that pulls each template's images onto every node ahead of time. That covers the IDE image, sidecars, the git-clone helper, and the baked or envbuilder image for devcontainer templates. So the first student on a node, or on a node the autoscaler just added, does not wait on a multi-hundred-megabyte pull. This needs no configuration; see Image pre-pull.
  • git_shallow: true for large repos. It clones only the latest commit instead of the full history. The student can still commit, push and pull, but git log, git blame and checking out older commits only see that one commit. Use it when the history is large and the exercises don't need it. It also applies to a devcontainer template that is not baked (ENVBUILDER_GIT_CLONE_DEPTH=1). A baked repo snapshot is always a full clone, because it is taken once and not per student.
workspace_templates:
  - name: big-repo
    git_repo: https://gitlab.com/org/monorepo.git
    git_shallow: true

If students don't need their work to survive a reschedule, ephemeral: true also skips attaching a persistent volume, which on OVHcloud adds tens of seconds to a start. Read the warning under Persistence first.

Sizing a workspace

workspace_templates:
  - name: heavy
    cpu: "2"
    memory: 4Gi
    disk_size: 20Gi

Quote bare numbers

cpu: 2 fails validation — these fields are strings. Write cpu: "2". Values with a unit suffix (500m, 4Gi) are already strings and need no quotes.

Several templates in one lab

Students request one workspace per template, so a lab with three templates lets each student run up to three workspaces side by side. They pick from a dropdown on the student portal.

workspace_templates:
  - name: go
    git_repo: https://gitlab.com/yodamad/go-workshop.git
    extensions:
      - golang.go

  - name: node
    git_repo: https://gitlab.com/yodamad/node-workshop.git
    extensions:
      - dbaeumer.vscode-eslint

  - name: scratch
    disk_size: 5Gi

Installing system packages

Startup scripts run as the workspace user, which has passwordless sudo, so packages can be installed at start:

workspace_templates:
  - name: tooling
    startup_script: |
      sudo apt-get update
      sudo apt-get install -y jq make httpie

Startup scripts are best-effort: a failing command is visible in kubectl logs but never blocks the workspace from opening.

Every student pays this cost on every workspace start, so for anything slow or large bake the tools into an image instead and point the template at it:

FROM codercom/code-server:latest
USER root
RUN apt-get update && apt-get install -y golang nodejs && rm -rf /var/lib/apt/lists/*
USER coder
workspace_templates:
  - name: custom
    image: registry.example.com/easylab/go-workshop:1.0

Push it to a registry the lab cluster can pull from. Prebuilt images also start faster than installing the same packages in every student's workspace. If the registry is private, add image_pull_secrets — see Private registries and repositories.

A database sidecar

Sidecars are extra containers in the same pod, reachable from the IDE at localhost:<port>.

workspace_templates:
  - name: api-workshop
    git_repo: https://gitlab.com/yodamad/api-workshop.git
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/app
    sidecars:
      - name: db
        image: postgres:16
        ports: [5432]
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: app

Each student gets their own database — sidecars live in the student's pod, not in a shared service.

workspace is reserved

The IDE container is named workspace. A sidecar with that name (or a name that isn't a valid DNS label) is dropped from the pod.

Docker inside a workspace

Managed clusters (OVHcloud, AKS) run containerd, not Docker, so there is no host Docker socket to reuse. Run a Docker-in-Docker sidecar and point the CLI at it:

workspace_templates:
  - name: docker-workshop
    env:
      DOCKER_HOST: tcp://localhost:2375
    startup_script: |
      sudo apt-get update && sudo apt-get install -y docker.io
      until docker info >/dev/null 2>&1; do sleep 2; done
    sidecars:
      - name: docker
        image: docker:dind
        ports: [2375]
        privileged: true
        env:
          DOCKER_TLS_CERTDIR: ""

DOCKER_TLS_CERTDIR: "" disables TLS on the daemon, which is what makes the plain tcp://localhost:2375 connection work. The startup script waits for the daemon so students don't hit "cannot connect" in their first minute.

privileged: true escalates to the node

Docker-in-Docker requires a privileged container, and a privileged container can take over the node it runs on. Only enable this on a trusted workshop cluster you control. For an unprivileged alternative, use docker:dind-rootless with the right capabilities, or install a Sysbox/Kata RuntimeClass on the cluster.

Mounting config and secrets

Mount a ConfigMap or Secret that already exists in the workspace namespace (workshops by default — set it on the Workspace step):

workspace_templates:
  - name: preconfigured
    mounts:
      - type: configmap
        name: workshop-config
        path: /etc/workshop
      - type: secret
        name: workshop-registry
        path: /home/coder/.docker

Create them before students request workspaces:

kubectl -n workshops create configmap workshop-config --from-file=./config/
kubectl -n workshops create secret generic workshop-registry --from-file=.dockerconfigjson=./config.json

The object must exist first

A mount referencing a missing ConfigMap or Secret leaves the pod stuck and the workspace never opens. Mounts are read-only, so students can't edit them.

Personal dotfiles

workspace_templates:
  - name: dotfiles
    dotfiles_repo: https://github.com/yodamad/dotfiles

The repo is cloned to ~/.dotfiles and the first executable of install.sh, setup.sh or bootstrap.sh runs. Like startup scripts, this is best-effort.

VS Code settings

workspace_templates:
  - name: focused
    vscode_settings:
      chat.disableAIFeatures: true
      security.workspace.trust.enabled: false
      editor.fontSize: 14
      files.exclude:
        "**/node_modules": true

The settings are written to code-server's user settings file (~/.local/share/code-server/User/settings.json) before the IDE starts. All setting scopes work there, including application-scoped ones such as security.workspace.trust.enabled.

  • The file is only written when it does not exist yet. On a persistent workspace that means the first start: a student who changes a setting keeps the change across restarts, and a later edit to vscode_settings does not reach workspaces that already exist.
  • The value must be a map. It is validated when the YAML is saved; a key with no name, or a value that is not a mapping, is rejected in the editor.
  • Importing a devcontainer fills vscode_settings from its customizations.vscode.settings — see Importing a devcontainer.

Everything together

A full-featured template using most of the schema:

workspace_templates:
  - name: full-stack
    image: codercom/enterprise-base:ubuntu
    git_repo: https://gitlab.com/yodamad/workshop.git
    git_branch: main
    git_folder: exercises
    cpu: "2"
    memory: 4Gi
    disk_size: 10Gi
    startup_script: |
      sudo apt-get update && sudo apt-get install -y jq
      make bootstrap
    dotfiles_repo: https://github.com/yodamad/dotfiles
    extensions:
      - golang.go
      - dbaeumer.vscode-eslint
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/app
    sidecars:
      - name: db
        image: postgres:16
        ports: [5432]
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: app
    mounts:
      - type: configmap
        name: workshop-config
        path: /etc/workshop

Private registries and repositories

A private image or a private workshop repository needs a credential. Credentials are never written in the template: the template names a Kubernetes Secret, and the Secret lives in the lab's cluster. That is what keeps tokens out of the lab's job file and out of the templates export, so a template is always safe to share.

workspace_templates:
  - name: private-workshop
    image: registry.example.com/workshops/base:1
    image_pull_secrets:
      - regcred
    git_repo: https://gitlab.com/org/private-workshop.git
    git_auth_secret: gitcred
    disk_size: 10Gi

Adding a credential

There are two moments to add one, and they behave the same way — the token is written to the lab's cluster and EasyLab keeps no copy:

  • In the creation wizard. The Workspace Templates step has a Credentials section. Add a row for each credential your templates reference. Because the cluster does not exist yet, the tokens are held in memory and written the moment provisioning finishes — before the lab reports itself ready.
  • In the lab detail page's Workspaces & Templates section, once the lab is up. Expand Credentials, add a Container registry or Git repository credential, and paste the token.

Saving over an existing name rotates it. Workspaces already running keep the old token — the kubelet resolved it when the pod started — so a rotation reaches students only as their workspaces are recreated.

Credentials live in the cluster, so destroying a lab destroys them. Recreating a lab prompts you to re-enter the tokens (the names and types are carried over). A retried lab keeps them.

If a template references a credential the cluster does not have — a token lost to a server restart mid-provisioning, a name typo, or one not yet added — the Credentials panel says so, and the workspaces that need it fail until it is added.

If you have kubectl access you can create the same Secrets by hand; the panel lists those too, since what matters is the name your template references:

# Registry — for image_pull_secrets
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=<user> \
  --docker-password=<token> \
  --namespace=workshops

# Git — for git_auth_secret
kubectl create secret generic gitcred \
  --type=kubernetes.io/basic-auth \
  --from-literal=username=oauth2 \
  --from-literal=password=<token> \
  --namespace=workshops

image_pull_secrets does not cover the devcontainer build

Two different things pull images, and they do not share credentials.

Image Pulled by Credential
image, sidecars, the init containers The kubelet image_pull_secrets
The devcontainer's base image, fallback_image, cache_repo The build, inside the pod devcontainer.registry_auth_secret

A devcontainer template pulling a private base image needs registry_auth_secret; image_pull_secrets will not help it. Both may be set, and both may name the same Secret.

git_auth_secret is http(s) only

A username and password is not how ssh:// authenticates, so the secret would be ignored and the clone would fail on the host key instead. Validation rejects the combination rather than letting you find out at the first workspace.

For GitLab use oauth2 as the username with a personal access token; GitHub accepts any non-empty username. The Credentials panel fills in oauth2 when you leave it blank.

Tokens in a repo URL are not private

git_repo: https://oauth2:glpat-xxx@gitlab.com/org/repo.git works, and EasyLab accepts it with a warning rather than rejecting it. But the URL is stored in the lab's job file, returned by the jobs API, and included in the templates export — so the token travels anywhere the template does. git_auth_secret exists to avoid exactly that.

What students can and cannot do

The clone is authenticated; the student's IDE is not. The token is given to the clone step alone and never to the workspace container, because students have a shell there and the token is yours, not theirs.

So a student's checkout has an ordinary remote with no stored credentials: reading the code works, but an interactive git pull or git push against the private repo will prompt and fail. If a workshop needs students to push, have them authenticate as themselves from inside the workspace.

Devcontainer workshops

When a workshop repository already ships a .devcontainer/devcontainer.json, a template can build from it instead of naming an image. Each student's workspace builds the devcontainer on first start, so a workshop's own Dockerfile and features work as written.

workspace_templates:
  - name: go-workshop
    git_repo: https://gitlab.com/org/workshop.git
    git_branch: main
    cpu: "2"
    memory: 4Gi
    disk_size: 20Gi
    extensions:
      - golang.go
    devcontainer:
      enabled: true
      dir: .devcontainer
      cache_repo: registry.example.com/easylab/cache
      registry_auth_secret: regcred
Key Type Notes
enabled bool Turns the mode on. Requires git_repo, and conflicts with image.
dir string Folder holding devcontainer.json. Defaults to .devcontainer.
cache_repo string Registry the built layers are cached in — see below. Required unless use_in_cluster_cache is set.
use_in_cluster_cache bool Provisions the build cache inside the lab's cluster instead of requiring cache_repo to name an external registry. Ignored if cache_repo is also set — an explicit cache_repo always wins. See below.
registry_auth_secret string Registry credential for everything the build pulls or pushes: the base image, fallback_image, and cache_repo. Omit when all of them are public. An in-cluster cache needs none for itself, but a private base image still does — the two combine. See Private registries and repositories.
fallback_image string Used when the devcontainer names neither an image nor a Dockerfile.
insecure bool Skip TLS verification when cloning and pulling.
config_repo string Reads devcontainer.json from a separate repo instead of git_repo — see Devcontainer config from a separate repository. dir then resolves inside this repo.
config_branch string Branch to clone from config_repo. Default branch when unset. Ignored when config_repo is unset.
config_auth_secret string Git credential for a private config_repo. http(s) only. Ignored when config_repo is unset.

The template's own ide key applies here too — see the constraints below.

The image the devcontainer builds contains no IDE of its own, so code-server is copied onto a volume the build is told to leave alone, and started from there. That makes the IDE independent of the devcontainer's own base image — but it does impose three constraints:

  • The base image must be glibc-based. The bundle ships a dynamically linked Node, which cannot run on Alpine/musl. The failure is misleading: the workspace container exits with no such file or directory even though the binary is plainly there.
  • The devcontainer's user needs a writable $HOME. code-server stores its user data and extensions under the home directory of the user the devcontainer runs as. When that is unwritable, extension installs are skipped quietly and the IDE then fails to start — so an unexplained "my extensions are missing" is worth checking here first.
  • Do not set ephemeral. The volume holding the student's files is also what makes the workspace folder writable by the usual uid-1000 devcontainer users (vscode, node, coder). Opting out of it can leave the folder owned by root, with the student unable to save. Use disk_size to size it instead.
  • $HOME is not persisted in devcontainer mode, but the IDE's state is. A plain workspace mounts its volume over the whole home directory; a devcontainer one cannot, because envbuilder replaces the entire root filesystem during the build and a devcontainer image's user home is often not /home/coder at all. Its volume instead holds two things: the project folder, and code-server's data directory (~/.local/share/code-server, linked onto the volume at /ide-state). So a student's files, their IDE settings and the extensions they install survive a restart, while anything else written to the home directory — shell history, dotfiles, tools installed under ~ — does not.
    • This needs the devcontainer's user to be root or in group 1000, as the usual vscode, node and coder users are. For any other user the IDE falls back to keeping its data in the unpersisted home directory.
    • It applies to workspaces created from now on. A devcontainer workspace that already exists keeps the project-only volume it was created with until it is deleted and recreated.

Authentication is the same as outside devcontainer mode: code-server presents a login page taking the workspace password.

A private devcontainer repo needs git_auth_secret

The devcontainer is built inside the pod, and the builder clones the repo itself — so a private git_repo needs git_auth_secret, exactly like any other private repo. It is separate from registry_auth_secret, which only covers the images the build pulls and pushes. Miss it and the clone falls back to no authentication: the build then reports the devcontainer.json as missing (no such file or directory) because nothing was ever fetched.

workspace_templates:
  - name: go-workshop
    git_repo: https://innersource.example.com/org/workshop.git
    git_branch: master
    git_auth_secret: gitcred
    disk_size: 20Gi
    devcontainer:
      enabled: true
      dir: .devcontainer
      cache_repo: registry.example.com/easylab/cache
      registry_auth_secret: regcred

Create gitcred the same way as for any private repo — see Private registries and repositories.

Devcontainer config from a separate repository

By default devcontainer.json is read from git_repo — the same repo the student's workspace clones. Set devcontainer.config_repo to read it from a different repo instead — for a devcontainer config shared or standardized across several workshops, kept apart from the workshop content it builds. git_repo is still required and is still what the student's workspace clones; config_repo is only cloned to build the devcontainer, into a separate location in the pod. dir (default .devcontainer) then resolves inside config_repo rather than git_repo.

workspace_templates:
  - name: go-workshop
    git_repo: https://gitlab.com/org/go-workshop-exercises.git
    git_branch: main
    disk_size: 20Gi
    devcontainer:
      enabled: true
      config_repo: https://gitlab.com/org/devcontainer-config.git
      config_branch: main
      dir: .devcontainer
      cache_repo: registry.example.com/easylab/cache
      registry_auth_secret: regcred

A private config_repo needs its own config_auth_secret — it is a separate credential from git_auth_secret, because the two repos may live in different projects or organizations. Create it the same way as any other git credential — see Private registries and repositories.

Importing a devcontainer

Rather than writing the block by hand, choose From a devcontainer in the Templates step — or in the Add Template drawer on an existing lab. It reads a devcontainer.json — by cloning a repo, or from a devcontainer.json / repository .zip you upload — fills the template in, and reports anything in the devcontainer that will not take effect. The result is ordinary YAML: review and edit it before creating the lab.

By default the import clones the workshop repository field to read the devcontainer. Choose Different repository under Devcontainer config to read it from a separate config repo instead, while the workshop repository field stays the content repo the generated template's git_repo is set to — this produces the config_repo block above without hand-editing the YAML.

For a private repository, supply an access token in the import dialog. It authenticates that one clone and is then discarded — it is not saved anywhere and does not become the lab's git_auth_secret. It cannot: at import time the lab's cluster does not exist yet, so there is nowhere to put a Secret. Giving students access to the repo is the separate step described in Private registries and repositories.

What is honoured, and what is not

The build understands the parts that describe the image; EasyLab covers several keys the build ignores; and some keys are honoured by neither.

devcontainer.json Handled by
image, build.dockerfile, build.args, build.context The build
features The build
containerEnv, remoteEnv The build
postCreateCommand, onCreateCommand, updateContentCommand The build
customizations.vscode.extensions EasyLab — copied to extensions on import; -publisher.ext removal entries are dropped, along with the extension they remove
customizations.vscode.settings EasyLab — copied to vscode_settings on import
hostRequirements.cpus / memory / storage EasyLab — copied to cpu / memory / disk_size on import
workspaceFolder EasyLab — copied to git_folder when relative
dockerComposeFile, service, runServices Nobody — rejected at import
forwardPorts, mounts, workspaceMount Nobody — only the IDE port is routed; use the template's mounts
privileged, capAdd, init Nobody — use a template sidecar instead
postStartCommand Nobody — move it to postCreateCommand or the template's startup_script

docker-compose devcontainers are not supported

A devcontainer built on dockerComposeFile cannot be used: the workspace is built as a single image, not a set of orchestrated services. The import rejects it outright. The nearest equivalent is a hand-written sidecars: block — see A database sidecar.

The cache registry is not optional

cache_repo (or use_in_cluster_cache) is required for a reason. Image layers are cached there and pushed after the first build, so the first workspace pays for the build and every later one starts from the cache — the difference is minutes versus seconds, per student. Without either one, all thirty students in a workshop would each rebuild the whole devcontainer from scratch.

The Secret named by registry_auth_secret must already exist in the workspace namespace — add it from the lab's Credentials panel, or with kubectl. See Private registries and repositories.

Hosting the cache registry in the cluster

Set use_in_cluster_cache: true instead of cache_repo to have EasyLab provision the registry itself, in the lab's own cluster:

devcontainer:
  enabled: true
  dir: .devcontainer
  use_in_cluster_cache: true

No registry_auth_secret is needed for the cache itself. The in-cluster registry does require authentication (once a template is pre-baked it is also exposed through the lab's domain, and baked images carry the workshop repo), but EasyLab generates its credentials itself — stored in the easylab-registry-cache-auth Secret of the workspace namespace — and supplies them wherever the registry is used: envbuilder's pushes and pulls over the cluster network, a student pod's pull of a baked image, and EasyLab's own checks. It is shared by every devcontainer template in the lab that opts in, and disappears when the lab is destroyed. An external cache_repo remains the right choice when a cache needs to survive across labs (e.g. a shared base image reused by several workshops) or be inspected outside the cluster.

A devcontainer built from a private base image still needs registry_auth_secret for that image — EasyLab merges it with the in-cluster registry's credentials, for live builds and bakes alike:

devcontainer:
  enabled: true
  dir: .devcontainer
  use_in_cluster_cache: true
  registry_auth_secret: regcred   # for the private base image

First start is slow, and it uses node disk

Even with a warm cache the first workspace of a lab builds the devcontainer before the IDE appears, which can take several minutes. The build writes to the node's disk rather than the workspace volume, so a large image across many students is worth sizing the node pool for.

A warm cache skips the build, not the extraction — size CPU/memory for concurrent starts

A cache hit only skips re-running the devcontainer's build steps; every workspace still has to extract the resulting image's layers onto its own pod from scratch (Extracting layer N/M... in the pod's logs) — that part happens every time, cache hit or not, and it is CPU- and disk-I/O-heavy. Leave cpu, memory, cpu_limit and memory_limit all unset and EasyLab now applies a default (500m CPU request / 2 CPU limit, 2Gi memory, 5Gi ephemeral storage) instead of leaving the pod with no reservation at all — the low CPU request keeps the scheduler/cluster-autoscaler from over-provisioning nodes for a burst of many workspaces, while the higher limit still lets extraction burst into whatever a node actually has spare. A workshop with many concurrent students on a large base image should still size cpu/memory (the request) and cpu_limit/memory_limit (the limit) explicitly for the image and class size — the default is a floor, not a recommendation for every workload.

Pre-baking: skipping the build entirely

Everything above describes making the build fast. A lab admin can instead skip it entirely: in the lab detail page's Workspaces & Templates section, a devcontainer template's card has a Bake image button. Baking builds the devcontainer once, pushes the result as an ordinary image, and records it on the lab — every student workspace for that template afterward does a plain image pull instead of running envbuilder at all, so neither the build nor the per-pod layer extraction above happens per student.

This is an action on a lab, not a template YAML key — there is nothing to set on the template itself. An admin triggers it with the button, or lets EasyLab do it with the Bake devcontainer images automatically option of the create-lab wizard and of the Add Template drawer, which bakes a devcontainer template as soon as the lab is ready or the template is added. See Pre-baking a devcontainer template in the admin guide for how to trigger it, what the status badges mean, and the domain requirement when baking to the in-cluster registry (an external cache_repo has no such requirement).

A bake also snapshots the workshop repository (git_repo, at git_branch) into the image, at /opt/easylab/repo. A student's workspace copies it onto their volume on first start instead of cloning, so starting a workspace makes no request to the git host at all — no clone time, and no rate limit to hit when a whole class starts at once. The result is the same checkout a clone would have produced, .git included, owned by the IDE user; a returning student's files are never overwritten. Private repositories are baked too — the clone at bake time uses the template's git_auth_secret, and the token is never written into the image. The in-cluster registry requires authentication for exactly this reason; an external cache_repo should be private if the repository is.

The tradeoff: a bake is a snapshot. It does not track the workshop repository, so a new commit — or a devcontainer.json change — after baking needs an explicit Rebuild. Until then, students keep getting the previously baked content rather than the live one.

Bakes made before repositories were baked

A template baked by an earlier EasyLab version still clones the repository at workspace start, as before — its image has no snapshot to copy from. Click Rebuild once to include the repository.

A Rebuild always reaches students. Baked images are pushed to a fixed :latest tag, but EasyLab records the image by its digest (…/baked/<lab>/<template>@sha256:…) rather than by that tag, and hands the digest to student pods. That matters because workspace pods pull with imagePullPolicy: IfNotPresent — deliberately, so a burst of students starting at once reuses a node's cached layers instead of all hitting the registry. Under a plain :latest a node that had already cached the pre-rebuild image would have gone on serving it; a digest changes whenever the content does, so the kubelet fetches the new image while an unchanged one still hits the node cache. (If the registry does not report a digest, EasyLab falls back to the tag and logs it — the bake still works, without that guarantee.)

Reusing a lab's templates

Use Export Templates YAML on the labs list to download an existing lab's workspace-templates-<stack>.yaml, then load it into a new lab — or into a running one through Add Template — with Upload file. Only the templates are exported — credentials are never included.

This makes template documents easy to keep in a git repository and reuse from one workshop edition to the next.

Troubleshooting

Symptom Likely cause
cannot unmarshal number ... into ... of type string A bare number where a string is expected — quote it (cpu: "2").
unknown field "imagee" A typo'd key. Unknown keys are rejected on purpose.
duplicate template name "go" Two templates share a name; names must be unique within a lab.
Student work disappears after a restart ephemeral: true on the template, or a bring-your-own cluster whose default StorageClass is node-local — see Persistence.
The repo isn't cloned git_repo clones only into an empty volume; an existing workspace keeps its contents.
Workspace never opens Often a mounts entry pointing at a ConfigMap/Secret that doesn't exist in the namespace.
apt-get fails in the startup script Prefix it with sudo; the workspace user has passwordless sudo but is not root.
A sidecar is missing from the pod It was named workspace (reserved) or has no image.
devcontainer.cache_repo is required unless devcontainer.use_in_cluster_cache is set Devcontainer mode needs a layer cache registry — either an external cache_repo, or use_in_cluster_cache: true to have EasyLab host one; see Devcontainer workshops.
devcontainer.enabled conflicts with image The image is built from the repo's devcontainer.json — remove image.
devcontainer.enabled requires git_repo The devcontainer.json is read from the workshop repo, so there must be one.
git_auth_secret requires git_repo The credential has nothing to authenticate to — remove it, or add the repo.
git_auth_secret needs an http(s) git_repo A username and password is not how ssh:// authenticates. Use an https:// remote.
failed to read git auth secret "x" The template names a credential the lab's cluster does not have. Add it in Credentials in the lab detail page's Workspaces & Templates section.
Workspace pods stuck in ImagePullBackOff A private image with no image_pull_secrets, or a name that does not match a credential.
The devcontainer build — or a bake — fails pulling its base image (DENIED: access forbidden, UNAUTHORIZED) Devcontainer images are pulled by the build, not the kubelet — they need devcontainer.registry_auth_secret, not image_pull_secrets. This holds with use_in_cluster_cache too: EasyLab only supplies the in-cluster registry's own credentials, not the base image's.
Devcontainer build fails with devcontainer.json: no such file or directory on a private repo The clone ran with no credentials (Using no authentication! in the pod logs), so nothing was fetched. Add git_auth_secret — see Devcontainer workshops. Recreate the workspace so the clone runs again on a clean volume.
Students can read the repo but cannot git push Expected. The token authenticates the clone only and is never given to the IDE.
A devcontainer import fails on dockerComposeFile Compose-based devcontainers are not supported — use sidecars instead.
Every student's workspace takes minutes to start The cache registry is unreachable or unwritable, so each build starts cold. Check registry_auth_secret. If the cache is warm and it's still slow at scale, it's likely layer extraction contending for CPU/disk across many concurrent pods — see A warm cache skips the build, not the extraction and size cpu/memory explicitly.
"Bake image" is rejected for a template using the in-cluster registry Baking to the in-cluster registry needs the lab to have a domain configured, so a student's pull can trust the registry over HTTPS — see Pre-baking: skipping the build entirely. Configure a domain for the lab, or set an external cache_repo on the template instead.
A bake fails with "image was built, but never became pullable" The image built and pushed fine, but nothing could fetch it back over a trusted connection. For the in-cluster registry this almost always means its TLS certificate never finished issuing. Check kubectl get certificate,certificaterequest -n <workspace namespace> in the lab's cluster; a stuck CertificateRequest usually means the lab's ClusterIssuer name doesn't match one that actually exists on that cluster (kubectl get clusterissuer). For an external cache_repo, verification authenticates with the same registry_auth_secret the push used, so a persistent (not just transient) failure there means those credentials cannot read the pushed tag back — check they have pull, not just push, permission on the registry. Click Rebuild once fixed — EasyLab only records a bake as ready after confirming it.
A workspace pod's pull error names a *.traefik.default (or similar auto-generated) certificate, not the registry's own hostname Traefik never received a real certificate for that host and fell back to serving its own internal default one — the ClusterIssuer/CertificateRequest never actually completed. This is the same failure as the row above, just observed from the workspace pod's own pull error instead of the bake status; the fix is the same (fix the certificate, then Rebuild — a stale pre-fix bake record is cleared automatically once a rebuild confirms the pull path is still broken). Note that the registry's Ingress no longer needs to be recreated to pick up corrected HTTPS settings: it is reconciled on the next bake, as workspace Ingresses are — see Certificates repair themselves.
A pull from the in-cluster registry fails with 401 Unauthorized / authentication required The registry requires authentication, and EasyLab adds its credentials to every workspace it creates — but a workspace created before the registry was secured has none. It keeps running while its node has the image cached; if its pod lands on a fresh node, the pull fails. Delete and recreate that workspace. If new workspaces fail too, check the easylab-registry-cache-auth and easylab-registry-cache-htpasswd Secrets exist in the workspace namespace: EasyLab recreates either one on the next bake or workspace request, and restarts the registry to pick up a new password.
A bake fails and the bake-repo container's log shows a crane append error The devcontainer built and pushed fine, but appending the repository snapshot to it did not. kubectl logs -n <workspace namespace> -l easylab.io/bake-template=<template> -c bake-repo has the cause — typically credentials that can push the build but not the extra layer, or an external cache_repo served over plain HTTP without insecure: true.