# Quickstart

This quickstart uses one scenario: creating a container image that prints a required message. You will package that scenario either as a scored SkillCred item or as a guided lab.

What you will build

A sandbox around one container-image task:

| Path | Outcome |
| --- | --- |
| SkillCred | Score whether a candidate can create `linux-foundation-education/hello:1.0`. |
| Lab | Guide a learner through creating `linux-foundation-education/hello:1.0`. |

The image must print:

```
hello tux
```

Before you begin, [set up your Codespace](https://skills.staging.lf-cert.cloud/docs/setting-up/index.md).

## Follow your content repository workflow

**SkillCred**

**Step 1: Scaffold the environment**

Run the initializer from the `items/` directory:

**@codespace-author ➜ .../quickstart (main)**

```
cd items
sandbox init
```

When prompted: select `Exam task` → name it `hello-image` → accept all other defaults → `Create` → validate.

Enter the item directory (named after what you entered above):

**@codespace-author ➜ .../items (main)**

```
cd hello-image
```

You now have a bootable sandbox with one Debian-based virtual machine. Drop into the VM to see what the candidate will see:

**@codespace-author ➜ .../items/hello-image (main)**

```
sandbox shell
```

You can now exit the virtual machine by using `Ctrl`+`D`

You have successfully used `sandbox init` to scaffold out a sandbox environment, started the virtual machine, and stopped it.

You may want to take some time to review the file structure created in step 1.

Generated file structure

```
hello-image/
├── .gitignore
├── metadata.yaml
├── sandbox.yaml
├── task.en.md
└── host1/
    ├── assets/
    │   └── .gitkeep
    └── scripts/
        ├── build.sh
        ├── setup.sh
        ├── answer.sh
        └── score.sh
```

Checkpoint

You have a scaffolded SkillCred item in `items/hello-image`.

**Step 2: Write the candidate task**

`task.en.md` is what the candidate reads. The sandbox renders it as a MkDocs site in the [instructions tool](https://skills.staging.lf-cert.cloud/docs/sandbox/tools/#instructions), visible by default in the left panel when the instructions tool is defined in `sandbox.yaml`.

You may preview the instructions as they will be rendered by the instructions tool in your development environment using the `sandbox` CLI. It's helpful to keep this running in a Codespace terminal while you author because the preview auto-updates.

**@codespace-author ➜ .../items/hello-image (main)**

```
sandbox instructions
```

Preview server started

The port may differ in your environment. Open the localhost URL shown in your terminal.

**terminal output**

```
ghcr.io/lf-certification/p3-sandbox-instructions-generator:latest
Building instructions...
Starting preview server at http://127.0.0.1:<port>
```

The preview tab shows the instructions exactly as the platform will render them to the end user of your environment.

Keep preview running

Leave `sandbox instructions` running while you edit. The preview reloads automatically when `task.en.md` changes.

Open `task.en.md` in your editor and save the contents to the following:

Copy into `task.en.md`

**task.en.md**

````
# Task

A starter `Dockerfile` is available at:

```text
/home/tux/container-image/Dockerfile
```

Modify it, then create a container image tagged:

```text
linux-foundation-education/hello:1.0
```

When the image is run, it must print exactly:

```text
hello tux
```

You may use `docker build`.
````

Checkpoint

The preview tab now shows the candidate-facing task.

You may now `Ctrl`+`C` in the terminal where sandbox instructions is running and close the preview tab.

**Step 3: Implement the sandbox lifecycle scripts**

Open `sandbox.yaml` and configure the [tools](https://skills.staging.lf-cert.cloud/docs/sandbox/tools/index.md). Each tool `name` becomes a tab the candidate sees. Then implement the [lifecycle scripts](https://skills.staging.lf-cert.cloud/docs/sandbox/lifecycle-scripts/index.md) in `host1/scripts/`.

Lifecycle map for this task

| Script | When it runs | Purpose in this task |
| --- | --- | --- |
| `build.sh` | Once, when the VM image is built | Install Docker, enable and start it with `--now`, and copy a starter asset into the candidate workspace. |
| `setup.sh` | Every time a sandbox instance starts | No-op; Docker enabled with `--now` in `build.sh` starts reliably with the VM. |
| `answer.sh` | On demand during local testing | Apply the reference solution by creating the `Dockerfile` and building the expected image. |
| `score.sh` | On demand during grading | Run independent checks for image existence and expected container output. |

**3.1 Build the VM image**

First, add a starter `Dockerfile` asset that candidates can modify:

Copy into `host1/assets/Dockerfile.starter`

**host1/assets/Dockerfile.starter**

```
FROM docker.io/library/alpine:3.20

# TODO: make this image print the required message.
CMD ["echo", "replace me"]
```

Then update `host1/scripts/build.sh` to install Docker, enable it at boot, and copy the starter asset into the candidate workspace:

Copy into `host1/scripts/build.sh`

**host1/scripts/build.sh**

```
#!/usr/bin/env bashp

install::docker
systemctl enable --now docker

mkdir -p /home/tux/container-image
chown -R tux:tux /home/tux/container-image
file::copy assets/Dockerfile.starter /home/tux/container-image/Dockerfile
usermod -aG docker tux
```

**3.2 Leave setup as a no-op**

In `host1/scripts/setup.sh`, leave it as a no-op:

No wait needed for Docker

Docker was enabled with `systemctl enable --now` in `build.sh`, so it starts reliably with the VM. Only add waits in `setup.sh` when startup delay is significant enough to outlast SSH availability — Kubernetes cluster readiness is the canonical case.

Copy into `host1/scripts/setup.sh`

**host1/scripts/setup.sh**

```
#!/usr/bin/env bashp
```

**3.3 Add the reference answer**

In `host1/scripts/answer.sh`, write the reference solution:

Copy into `host1/scripts/answer.sh`

**host1/scripts/answer.sh**

```
#!/usr/bin/env bashp

mkdir -p /home/tux/container-image
chown -R tux:tux /home/tux/container-image

cat > /home/tux/container-image/Dockerfile <<'EOF'
FROM docker.io/library/alpine:3.20
CMD ["echo", "hello tux"]
EOF

chown tux:tux /home/tux/container-image/Dockerfile
sudo -iu tux docker build -t linux-foundation-education/hello:1.0 /home/tux/container-image
```

**3.4 Score the candidate work**

In `host1/scripts/score.sh`, check that the image exists and prints the expected message:

Copy into `host1/scripts/score.sh`

**host1/scripts/score.sh**

```
#!/usr/bin/env bashp

IMAGE_TAG="linux-foundation-education/hello:1.0"
EXPECTED_OUTPUT="hello tux"

imageExists() {
    sudo -iu tux docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1
}

imagePrintsExpectedMessage() {
    output="$(sudo -iu tux docker run --rm "${IMAGE_TAG}")"
    test "${output}" = "${EXPECTED_OUTPUT}"
}

scoring::check imageExists
scoring::check imagePrintsExpectedMessage
scoring::report
```

Checkpoint

The sandbox now has setup, reference answer, and scoring logic for the container image task.

**Step 4: Add item metadata**

Open `metadata.yaml`, change the generated `settings.nickname`, and add the required item fields:

Update `metadata.yaml`

**metadata.yaml**

```
---
settings:
  nickname: [petname]
competency: quickstart
difficulty: easy
title: "Build a greeting container image"
total_check_count: 2
revision: 1
```

Tip

`nickname` should be a unique item name, such as a generated pet name. `competency` must match an id in `blueprint.yaml`. `total_check_count` must match the number of checks in `score.sh`. See [Blueprint reference](https://skills.staging.lf-cert.cloud/docs/skillcred/blueprint-reference/index.md) for the full list of metadata constraints.

Checkpoint

The metadata now matches the two `scoring::check` calls in `score.sh`.

**Step 5: Validate**

Rebuild the VM image, then run the lifecycle test:

**@codespace-author ➜ .../items/hello-image (main)**

```
sandbox build && sandbox test
```

Why build first?

You changed `build.sh` after first starting the sandbox. `sandbox test` runs against the existing sandbox build; it does not rebuild a pre-existing VM image for you.

`sandbox test` succeeds only when both scoring paths are valid:

- **No candidate work**

  `setup.sh` -> `score.sh`

  Expected score: **0 of 2 checks pass**

  Confirms the task does not award points before the candidate does any work.
- **Reference answer**

  `setup.sh` -> `answer.sh` -> `score.sh`

  Expected score: **2 of 2 checks pass**

  Confirms the reference answer satisfies every scoring check.

Successful test result

The final report includes `checks_executed: 2`, `checks_passed: 2`, and the two successful checks: `imageExists` and `imagePrintsExpectedMessage`.

```
Test successful!
```

Exact final host report

The CLI prints the JSON after `host1 |` on one line. It is formatted here for readability.

```
{
  "output_version": 1,
  "checks_executed": 2,
  "checks_passed": 2,
  "checks_successful": [
    "imageExists",
    "imagePrintsExpectedMessage"
  ],
  "checks_failed": []
}
```

Preview the candidate instructions:

**@codespace-author ➜ .../items/hello-image (main)**

```
sandbox instructions
```

Validation runs at commit time

A `skills validate item` git hook re-checks the items you touched on every `git commit`; you do not need to run validation manually.

You are done when

- `sandbox build && sandbox test` ends with `Test successful!`.
- The final score report has `checks_executed: 2` and `checks_passed: 2`.
- The instructions preview renders the candidate task correctly.

**Lab**

**Step 1: Scaffold the lab environment**

Run the lab initializer from the `items/` directory:

**@codespace-author ➜ .../quickstart (main)**

```
cd items
sandbox init
```

When prompted: select `Lab exercise` → name it `hello-image` → accept all other defaults → `Create` → validate.

Enter the lab directory:

**@codespace-author ➜ .../items (main)**

```
cd hello-image
```

Drop into the VM to see the learner environment:

**@codespace-author ➜ .../items/hello-image (main)**

```
sandbox shell
```

You can now exit the virtual machine by using `Ctrl`+`D`

Generated file structure

```
hello-image/
├── .gitignore
├── instructions.md
├── sandbox.yaml
└── host1/
    ├── assets/
    │   └── .gitkeep
    └── scripts/
        ├── build.sh
        ├── setup.sh
        ├── answer.sh
        └── score.sh
```

Checkpoint

You have a scaffolded lab sandbox in `items/hello-image`.

**Step 2: Register the lab**

Describe the lab in `metadata.yaml` inside the lab directory:

Copy into `items/hello-image/metadata.yaml`

**items/hello-image/metadata.yaml**

```
---
competency: quickstart
difficulty: easy
title: "Create a greeting container image"
total_check_count: 0
revision: 1
```

Scoring comes later

`total_check_count: 0` is allowed while the lab has no scoring checks yet. Once `score.sh` calls `scoring::check`, set it to the exact check count.

Then select the lab in `manifest.yaml` at the content repository root:

Copy into `manifest.yaml`

**manifest.yaml**

```
assemblies:
  primary:
    selected_items:
      - hello-image
```

Checkpoint

The lab content repository now selects `items/hello-image`.

**Step 3: Implement the sandbox lifecycle scripts**

Open `sandbox.yaml` and review the [tools](https://skills.staging.lf-cert.cloud/docs/sandbox/tools/index.md) configuration. Each tool `name` becomes a tab the learner sees, then implement the [lifecycle scripts](https://skills.staging.lf-cert.cloud/docs/sandbox/lifecycle-scripts/index.md) in `host1/scripts/`.

Scoring is still useful in labs. It validates that the instructions being taught are achievable in the authored environment produced by the lifecycle scripts.

Lifecycle map for this lab

| Script | When it runs | Purpose in this lab |
| --- | --- | --- |
| `build.sh` | Once, when the VM image is built | Copy a starter asset into the learner workspace. |
| `setup.sh` | Every time a sandbox instance starts | No-op, no services to check for readiness in this lab. |
| `answer.sh` | On demand during local testing | Apply the reference solution by running the same Docker setup commands taught to the learner, then building the expected image. |
| `score.sh` | On demand during validation | Check whether the learner-visible instructions produce a usable Docker runtime, the expected image, and the expected output. |

**3.1 Prepare the learner workspace**

First, add a starter `Dockerfile` asset that learners can modify:

Copy into `host1/assets/Dockerfile.starter`

**host1/assets/Dockerfile.starter**

```
FROM docker.io/library/alpine:3.20

# TODO: make this image print the required message.
CMD ["echo", "replace me"]
```

Then update `host1/scripts/build.sh` to copy the starter asset into the learner workspace. In this lab, Docker setup is part of what the learner practices, so do not bake it into the VM image.

Copy into `host1/scripts/build.sh`

**host1/scripts/build.sh**

```
#!/usr/bin/env bashp

mkdir -p /home/tux/container-image
chown -R tux:tux /home/tux/container-image
file::copy assets/Dockerfile.starter /home/tux/container-image/Dockerfile
```

**3.2 Leave setup empty**

In `host1/scripts/setup.sh`, keep the script as a no-op:

Use `setup.sh` only for runtime readiness

`setup.sh` runs every time a sandbox instance starts, after the VM boots from the image produced by `build.sh`. Put work here only when it depends on runtime state or verifies readiness. In this lab, Docker is intentionally not ready yet because installing it is part of the learner workflow.

Copy into `host1/scripts/setup.sh`

**host1/scripts/setup.sh**

```
#!/usr/bin/env bashp
```

**3.3 Add the reference answer**

In `host1/scripts/answer.sh`, automate the same outcome the lab teaches:

Copy into `host1/scripts/answer.sh`

**host1/scripts/answer.sh**

```
#!/usr/bin/env bashp

apt-get update
apt-get install -y docker.io
systemctl enable --now docker
usermod -aG docker tux
sudo -iu tux docker ps >/dev/null

mkdir -p /home/tux/container-image
chown -R tux:tux /home/tux/container-image

cat > /home/tux/container-image/Dockerfile <<'EOF'
FROM docker.io/library/alpine:3.20
CMD ["echo", "hello tux"]
EOF

chown tux:tux /home/tux/container-image/Dockerfile
sudo -iu tux docker build -t linux-foundation-education/hello:1.0 /home/tux/container-image
```

**3.4 Validate the lab result**

In `host1/scripts/score.sh`, check that Docker is usable, the image exists, and the image prints the expected message:

Copy into `host1/scripts/score.sh`

**host1/scripts/score.sh**

```
#!/usr/bin/env bashp

IMAGE_TAG="linux-foundation-education/hello:1.0"
EXPECTED_OUTPUT="hello tux"

dockerIsUsable() {
    sudo -iu tux docker ps >/dev/null
}

imageExists() {
    sudo -iu tux docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1
}

imagePrintsExpectedMessage() {
    output="$(sudo -iu tux docker run --rm "${IMAGE_TAG}")"
    test "${output}" = "${EXPECTED_OUTPUT}"
}

scoring::check dockerIsUsable
scoring::check imageExists
scoring::check imagePrintsExpectedMessage
scoring::report
```

Then set `total_check_count` in `metadata.yaml` to match the three `scoring::check` calls:

Update `metadata.yaml`

**metadata.yaml**

```
---
competency: quickstart
difficulty: easy
title: "Create a greeting container image"
total_check_count: 3
revision: 1
```

Checkpoint

The lab sandbox now has a prepared workspace, a reference answer, and validation logic for the guided Docker exercise.

**Step 4: Write the lab instructions**

`instructions.md` is what the learner reads. The sandbox renders it as a MkDocs site in the [instructions tool](https://skills.staging.lf-cert.cloud/docs/sandbox/tools/#instructions), always visible in the left panel.

When you write labs for an existing catalog, the [Lab instruction style](https://skills.staging.lf-cert.cloud/docs/labs/instruction-style/index.md) reference describes the voice, heading shape, prompt titles, and copy-target conventions the catalog labs follow. The quickstart example below is a minimal starting point; production labs use more of those conventions.

Open `instructions.md` and write the guided steps:

Copy into `instructions.md`

**instructions.md**

````
# Lab

In this lab, you will install Docker, configure your user to run Docker commands, and create a container image that prints a greeting.

Install Docker and enable it:

```bash title="tux@host1:~$"
sudo apt-get update
sudo apt-get install -y docker.io
sudo systemctl enable --now docker
```

Add your user to the Docker group, then start a shell with the updated group membership:

```bash title="tux@host1:~$"
sudo usermod -aG docker tux
newgrp docker
```

Confirm Docker works without `sudo`:

```bash title="tux@host1:~$"
docker ps
```

A starter `Dockerfile` is available at:

```text
/home/tux/container-image/Dockerfile
```

Open the file and replace its contents with:

```dockerfile
FROM docker.io/library/alpine:3.20
CMD ["echo", "hello tux"]
```

Build the image with this tag:

```text
linux-foundation-education/hello:1.0
```

```bash title="tux@host1:~$"
cd /home/tux/container-image
docker build -t linux-foundation-education/hello:1.0 .
```

Run the image:

```bash title="tux@host1:~$"
docker run --rm linux-foundation-education/hello:1.0
```

!!! success "Expected Output"
    ```{.text .no-copy}
    hello tux
    ```
````

Preview it at any time:

**@codespace-author ➜ .../items/hello-image (main)**

```
sandbox instructions
```

Keep preview running

Leave `sandbox instructions` running while you edit. The preview reloads automatically when `instructions.md` changes.

Checkpoint

The learner instructions now walk through the shared container image scenario.

**Step 5: Validate**

Rebuild the VM image, then run the lifecycle test:

**@codespace-author ➜ .../items/hello-image (main)**

```
sandbox build && sandbox test
```

Why build first?

You changed `build.sh` after first starting the sandbox. `sandbox test` runs against the existing sandbox build; it does not rebuild a pre-existing VM image for you.

`sandbox test` succeeds only when both validation paths are valid:

- **Before the lab steps**

  `setup.sh` -> `score.sh`

  Expected score: **0 of 3 checks pass**

  Confirms the checks do not pass before the learner follows the instructions.
- **Reference answer**

  `setup.sh` -> `answer.sh` -> `score.sh`

  Expected score: **3 of 3 checks pass**

  Confirms the authored environment can complete the same outcome taught in the lab.

Successful test result

The final report includes `checks_executed: 3`, `checks_passed: 3`, and the three successful checks: `dockerIsUsable`, `imageExists`, and `imagePrintsExpectedMessage`.

```
Test successful!
```

Exact final host report

The CLI prints the JSON after `host1 |` on one line. It is formatted here for readability.

```
{
  "output_version": 1,
  "checks_executed": 3,
  "checks_passed": 3,
  "checks_successful": [
    "dockerIsUsable",
    "imageExists",
    "imagePrintsExpectedMessage"
  ],
  "checks_failed": []
}
```

Validation runs at commit time

A `skills validate .` git hook re-checks the lab content repository on every `git commit`; you do not need to run validation manually.

You are done when

- The instructions preview renders the guided lab correctly.
- `sandbox build && sandbox test` ends with `Test successful!`.
- The final score report has `checks_executed: 3` and `checks_passed: 3`.

## Key Resources

- **[sandbox.yaml](https://skills.staging.lf-cert.cloud/docs/sandbox/sandbox-yaml/index.md)**

Configure virtual machines, tools, base images, resources, and multi-VM layouts.

- **[Lifecycle Scripts](https://skills.staging.lf-cert.cloud/docs/sandbox/lifecycle-scripts/index.md)**

Learn when `build.sh`, `setup.sh`, `answer.sh`, and `score.sh` run and how to write scoring checks.

- **[Sandbox](https://skills.staging.lf-cert.cloud/docs/sandbox/index.md)**

Understand the sandbox model, configuration flow, and authoring building blocks.

- **[Setting Up](https://skills.staging.lf-cert.cloud/docs/setting-up/index.md)**

Prepare your Codespace and local authoring tools before building content repositories.
