Chapter 30Lesson 02~245 minutes

Codespaces, Dev Containers, GitHub Pages, Documentation, and Cloud Developer Environments: Guided Hands-On Workflow and Core Operations

Create a disposable documentation repository, validate a minimal dev container, optionally inspect a Codespace, publish with Pages, and prove the exact hosted source commit.

Disposable labdevcontainer.jsonPages publishingCodespaces CLIVerification

Learning objectives

  • Create a disposable public documentation repository and record repository/commit identity before changing hosted settings.
  • Add a minimal devcontainer.json, validate its JSON locally, and preview the same static documentation without Codespaces.
  • Optionally create and inspect a Codespace only after confirming included quota/billing ownership and requested permissions.
  • Enable GitHub Pages from main:/docs, query the Pages resource/build, and prove the hosted build commit equals the intended Git commit.
  • Compare local, Dev Container, and Codespace execution boundaries and perform explicit cleanup.

1. Scenario, availability, and shell conventions

You are creating github-cloud-dev-lab, a disposable public project containing one static documentation page and one development-container configuration. The mandatory path needs GitHub Free, GitHub CLI, Git, and Python 3. GitHub Pages is available for public repositories on GitHub Free. A Codespace is optional: create one only if your personal account still has included usage or you have deliberately configured billing/spending.

Bash/Git Bash blocks: Commands using export, heredocs, test, or command substitution are written for Bash/Git Bash. On Windows PowerShell either run them in Git Bash or translate environment variables/heredocs; the GitHub/UI concepts are shell-independent.
Required role: Use a repository you own or administer. Configuring the Pages publishing source requires repository admin or maintainer authority in the current GitHub model; Codespaces creation also remains subject to repository access and account/organization policy. The personal disposable repository created here gives its owner the required repository administration authority.
Do not reuse a valuable repository: Pages settings, Codespace creation/deletion, and later failure drills belong only in this disposable lab.

2. Preflight: prove account, target name, and existing cloud inventory

gh auth status
OWNER="$(gh api -H 'X-GitHub-Api-Version: 2026-03-10' user --jq .login)"
REPO="github-cloud-dev-lab"
FULL="$OWNER/$REPO"
printf 'owner=%s repo=%s
' "$OWNER" "$FULL"

gh repo view "$FULL" --json nameWithOwner,visibility,url 2>/dev/null || echo "repository not present"
gh codespace list --json name,repository,state,machineName,lastUsedAt

# Inspect Codespaces secret scope metadata only; values are never returned.
gh secret list -R "$FULL" --app codespaces --json visibility,numSelectedRepos   --jq '{repository_codespaces_secret_count:length}' 2>/dev/null || true
gh secret list --user --app codespaces --json visibility,numSelectedRepos   --jq 'map({visibility,numSelectedRepos})' 2>/dev/null || true

Prediction 1: creating the repository will create a hosted repository object and default branch only after the first push. It will not create a Codespace or Pages site. Prediction 2: adding .devcontainer/devcontainer.json changes repository content only; it does not allocate a VM until an environment is created. The secret-list commands show only scope/count metadata; they do not reveal encrypted secret values. If the repository does not exist yet, its repository-secret query can legitimately fail and is ignored during this preflight.

3. Create the repository, documentation, and minimal development-container metadata

gh repo create "$FULL" --public --clone --description "Disposable Chapter 30 cloud development and Pages lab"
cd "$REPO"
mkdir -p docs .devcontainer

cat > docs/index.html <<'HTML'
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Cloud Dev Lab</title></head>
<body>
  <main>
    <h1>Cloud Dev Lab</h1>
    <p id="evidence">This page is published from the repository's docs directory.</p>
  </main>
</body>
</html>
HTML

cat > .devcontainer/devcontainer.json <<'JSON'
{
  "name": "chapter-30-docs",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "forwardPorts": [8000],
  "portsAttributes": {
    "8000": {
      "label": "documentation preview",
      "onAutoForward": "notify"
    }
  },
  "postCreateCommand": "printf 'chapter-30 environment ready\n'"
}
JSON

python -m json.tool .devcontainer/devcontainer.json >/dev/null
git add docs .devcontainer
git commit -m "Add reproducible docs environment"
git push -u origin HEAD
BASE_SHA="$(git rev-parse HEAD)"
printf 'baseline commit=%s
' "$BASE_SHA"

The first independent verification is local Git state: git show --stat HEAD must contain both the documentation and the dev-container config. The config does not contain credentials and does not request access to any other repository.

4. Verify the static site locally before involving GitHub Pages

Use the same checked-out bytes that will become the branch-source Pages input. This tests content, not GitHub hosting:

python -m http.server 8000 --directory docs >local-pages.log 2>&1 &
SERVER_PID=$!
sleep 1
curl --fail --silent http://127.0.0.1:8000/ | grep -F "Cloud Dev Lab"
kill "$SERVER_PID"
rm -f local-pages.log

Expected observation: curl exits zero and prints the heading line. If it fails here, fix the repository content before debugging Pages.

If the open-source Dev Container CLI (or VS Code Dev Containers extension) and a local container runtime are already available, exercise the same configuration locally before spending Codespaces quota:

# Optional local portability proof; requires a local container runtime and Dev Container CLI.
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . sh -lc 'test -f docs/index.html && printf "container parity ok\n"'

VS Code users can instead choose Dev Containers: Reopen in Container. The key evidence is that the repository-defined configuration can create a local development container without depending on Codespaces-specific billing, secrets, or VM policy.

5. Optional live Codespace: check quota first, then create with bounded lifecycle

Personal accounts include a monthly quota, but the remaining amount is account-specific. Confirm billing/usage in GitHub settings before creating anything. If you proceed, use a small available machine, short idle timeout, and short retention period. Do not add a secret for this lab.

# Inspect Codespaces secret metadata/scope without revealing any values.
gh secret list -R "$FULL" --app codespaces --json visibility,numSelectedRepos   --jq 'map({visibility,numSelectedRepos})'
gh secret list --user --app codespaces --json visibility,numSelectedRepos   --jq 'map({visibility,numSelectedRepos})'

# Then inspect available machines/default attributes.
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$FULL/codespaces/machines" --jq '.machines[] | {name,display_name,storage_in_bytes,memory_in_bytes,cpus}'

# Let GitHub choose an available machine; bound inactivity and stopped-storage lifetime.
gh codespace create -R "$FULL" -b main --idle-timeout 30m --retention-period 1d --status

gh codespace list -R "$FULL" --json name,displayName,state,machineName,lastUsedAt
CS="$(gh codespace list -R "$FULL" --json name --jq '.[0].name')"
gh codespace view -c "$CS" --json name,repository,state,machineName,idleTimeoutMinutes,retentionPeriodDays,prebuild

gh codespace ports -c "$CS" --json sourcePort,visibility,label,browseUrl

gh codespace ssh -c "$CS" -- 'printf "repo=%s\n" "$GITHUB_REPOSITORY"; git rev-parse HEAD; printf "CODESPACES=%s\n" "$CODESPACES"'

The SSH command deliberately prints only non-secret identity/state. Never run env, set, or a recursive context dump in a course lab because a future Codespaces secret could appear. If port 8000 is forwarded, its default visibility is private; do not make it public for this exercise.

If gh codespace create is blocked by quota or policy, record that as an availability result and continue. The mandatory learning outcome—the repository-defined environment plus Pages deployment—does not depend on allocating a Codespace.

6. Enable Pages from main:/docs and verify the hosted source

For this static site, branch publishing is the smallest safe model. The Pages resource is a hosted configuration; enabling it changes no Git ref.

cat > /tmp/pages-source.json <<'JSON'
{"source":{"branch":"main","path":"/docs"}}
JSON

gh api --method POST   -H "X-GitHub-Api-Version: 2026-03-10"   "repos/$FULL/pages"   --input /tmp/pages-source.json

rm -f /tmp/pages-source.json

gh api -H "X-GitHub-Api-Version: 2026-03-10"   "repos/$FULL/pages"   --jq '{status,html_url,build_type,source,https_enforced}'

Expected state: the Pages object reports the configured source branch main and path /docs. The build may be queued or building immediately after creation. Do not treat the site URL becoming reachable as enough evidence; correlate the build record with the commit.

# Poll manually a few times rather than hammering the API.
gh api -H "X-GitHub-Api-Version: 2026-03-10"   "repos/$FULL/pages/builds/latest"   --jq '{status,commit,created_at,updated_at,error}'

HOSTED_SHA="$(gh api -H 'X-GitHub-Api-Version: 2026-03-10' "repos/$FULL/pages/builds/latest" --jq .commit)"
printf 'local=%s
hosted=%s
' "$BASE_SHA" "$HOSTED_SHA"
test "$BASE_SHA" = "$HOSTED_SHA"

PAGE_URL="$(gh api -H 'X-GitHub-Api-Version: 2026-03-10' "repos/$FULL/pages" --jq .html_url)"
curl --fail --silent --location "$PAGE_URL" | grep -F "Cloud Dev Lab"

Those two checks prove two different things: the API proves the build was associated with the intended commit, and the HTTP request proves the public endpoint serves the expected page. If the build record has not completed yet, wait manually and query again; do not create a tight loop that consumes API requests.

7. Compare portability gaps explicitly

Question Conventional local Local Dev Container Codespace
Repository bytes Local clone/ref Local clone/ref mounted into container GitHub-created environment from repository/ref
Container config Optional Primary environment description Primary container description inside hosted VM
Identity to GitHub Local credential manager/SSH Usually host/injected tooling-specific auth GitHub-managed Codespaces repository token plus approved extra access
Secrets Local OS/tooling Local injection strategy Codespaces user/repo/org secret stores
Ports Local OS firewall Container-to-host forwarding Codespaces forwarding with private/org/public visibility policy
Billing Local hardware Local hardware/container runtime Hosted compute + storage / included quota or billing owner
Offline use Yes Yes after images/deps exist locally No; requires service/network access

Write one portability gap into docs/environment-notes.md. For example, “Codespaces supplies hosted repository authentication; local Dev Containers rely on workstation authentication.” Commit that statement rather than hiding it behind an assumption of perfect parity.

8. Challenge: choose the control, not the memorized command

For each request, choose the surface before looking at syntax:

  1. A developer wants an API key only in their Codespaces for this repository → user Codespaces secret scoped to that repository.
  2. The project wants every environment to expose port 8000 privately → repository devcontainer.json port metadata plus organization restrictions if applicable.
  3. The docs require a custom static-site build before publishing → Pages Actions deployment rather than branch-source publishing.
  4. A reviewer wants proof of the live documentation version → Pages build/deployment commit plus the corresponding Git commit.
  5. Finance wants cloud development cost capped → Codespaces machine/timeout/retention and account/org spending policy, not a Git setting.

9. Verification and cleanup

git status --short
git rev-parse HEAD
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$FULL/pages" --jq '{html_url,source,status}'

# Optional Codespace cleanup, if one was created.
if [ -n "${CS:-}" ]; then
  gh codespace stop -c "$CS" || true
  gh codespace delete -c "$CS"   # destructive to unpushed Codespace-only work; confirm the lab is clean
fi

# Optional: unpublish this disposable Pages site before archiving.
# SECURITY-SENSITIVE/DESTRUCTIVE hosted-setting change; only for this lab.
gh api --method DELETE -H "X-GitHub-Api-Version: 2026-03-10" "repos/$FULL/pages"

gh repo archive "$FULL" --yes

Before deleting a Codespace, git status inside it should be clean and all wanted changes must be pushed. Deleting a Codespace removes its unpushed work. Archiving the disposable repository prevents accidental future modification; delete the repository only if you intentionally want full removal and understand that deletion is a separate destructive operation.

Knowledge check

Why did the lab enable Pages only after the local HTTP check passed?

Which two observations prove the intended Pages version is live?

Why is creating a Codespace optional even on a public repository?

Why does the lab avoid printing all environment variables inside the Codespace?

If a forwarded port is private, may you assume it can never become public?

Summary

You built one versioned source tree that works as local documentation, carries portable dev-container metadata, can optionally become a Codespace, and publishes through a separately inspectable Pages resource. The important evidence is the relationship among repository/ref, environment configuration, hosted runtime state, and Pages build commit—not the success of a button click. Lesson 3 turns those mechanics into design policy.

Next lesson

Codespaces, Dev Containers, GitHub Pages, Documentation, and Cloud Developer Environments: Configuration, Design Choices, and Tradeoffs

Further reading — current primary sources

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.