Self-hosted service
learntocloud/learn-to-cloud-app avatar
learntocloud/learn-to-cloud-app

Learn to Cloud app: a FastAPI and HTMX tracker for the cloud engineering course

The best open source courseware to learn cloud engineering

1,044 stars28 forksPythonMIT

At a glance

What is it?
The Learn to Cloud App is the open-source web application behind the Learn to Cloud guide, tracking a learner's progress through all eight curriculum phases with GitHub OAuth, hands-on project submissions and background verification run inside the API container. It is a course companion built as a production service, with Terraform on Azure Container Apps and a deploy pipeline that refuses to roll back through an old workflow run.
Who is it for?
Learn to Cloud App suits a self-taught engineer following the Learn to Cloud curriculum who wants their project submissions checked and their progress recorded in one place, because the verification loop runs inside the API and the whole course content ships as YAML in the repository.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A course guide with a companion application

The Learn to Cloud App is a web application for tracking your progress through the Learn to Cloud guide, and the guide itself lives at learntocloud.guide.

The feature list is short and tells you what the application does. All eight phases of the Learn to Cloud curriculum. Progress tracking with steps, questions and hands-on projects. Authentication via GitHub OAuth. A dashboard with progress visualisation. GitHub integration for project submissions. And background verification inside the API container.

The design decision worth noticing is the last one. A course companion that checks the learner's work has to run some kind of job somewhere, and most applications of this shape would add a worker, a queue and a scheduler. This one does not. The README states that the API starts verification automatically, with one sequential background loop per API process that atomically claims attempts from PostgreSQL. No separate host, external queue or verification job is required.

That is a real trade-off. A sequential loop in the API process means verification throughput is tied to the number of API replicas and the loop is not horizontally scalable in the way a dedicated worker is. In exchange, there is no queue to operate, no separate deployment to reason about, and no way for the two halves to disagree about what a job is. For a course with a cohort of learners rather than a service with a request queue, that is a sensible place to draw the line.

The stack, and why server-rendered HTML is enough here

The tech stack table in the README is a full application stack, and each row has a reason.

The backend is Python 3.13 or newer, FastAPI, SQLAlchemy in async mode, and PostgreSQL. Verification is a sequential API background worker. The frontend is HTMX with Jinja2 templates, Alpine.js and Tailwind CSS v4. Auth is GitHub OAuth through Authlib. Infrastructure is Azure Container Apps, Azure PostgreSQL and Terraform, and CI/CD is GitHub Actions.

HTMX with server-rendered templates is the interesting choice for an application that has to run on cheap infrastructure. There is no SPA build, no client-side router and no JSON API consumed by a separate front end, which means the deployment is a Python process serving HTML. For a progress tracker, where the read paths are simple and the write paths are form submissions, that removes a category of work without removing anything the user needs.

The Jinja2 templates live in src/learn_to_cloud/templates/ and the static assets in src/learn_to_cloud/static/. The Dockerfile has a dedicated Tailwind stage that runs the Tailwind CLI against a single input CSS file and the templates, so the CSS build happens at image build time rather than at request time.

The other side of the stack is worth noticing too. The pyproject.toml carries the OpenTelemetry API, SDK, OTLP exporter and instrumentation for httpx, logging and SQLAlchemy, all pinned to the 1.44 line because azure-monitor-opentelemetry 1.8.10 requires it. Telemetry is not an afterthought in this codebase.

Local development assumes four services are already running

The quick start is unusually explicit about what it will not do for you, and the notes section says so directly: the API does not start local dependencies for you, and you should run the compose command first.

The first step brings up Postgres and the .NET Aspire dashboard.

bash
docker compose up -d db aspire-dashboard

The compose file shows exactly what that means. The db service is postgres:16-alpine published on 127.0.0.1:55432 rather than the default port, with POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB set to postgres, postgres and learn_to_cloud, a named volume for the data directory, and a healthcheck that runs pg_isready every five seconds with five retries. The aspire-dashboard service is the Microsoft Aspire dashboard, published on 18888 with the OTLP ports 4317 and 4318 mapped alongside, and it runs with ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS set to true.

Everything is bound to 127.0.0.1 rather than 0.0.0.0, which is the correct default for a development stack and a detail worth copying.

The Python side then installs with uv, which also supplies the required Python 3.13 runtime, and the environment file is copied from the example.

bash
uv sync --locked
cp .env.example .env  # Create environment config (edit if needed)

Migrations run through Alembic, and the API starts with uvicorn against the learn_to_cloud.main:app application object.

bash
uv run alembic upgrade head
bash
uv run python -m uvicorn learn_to_cloud.main:app --reload --port 8000

The result is a service on port 8000, interactive API docs on port 8000 at /docs, which are enabled in development or with WEB_SECURITY__ENABLE_DOCS=true, Postgres on 55432, and the Aspire dashboard on 18888.

Verification claims attempts from Postgres, not from a queue

The verification engine deserves a closer look because it is where the design is most opinionated.

Attempts have an execution timeout and overdue cleanup, with no workflow retries or checkpoints.

That last clause is the important one, and it is a deliberate refusal of a common pattern. A verification pipeline with retries and checkpoints is the right shape for long-running, multi-step work where a transient failure should not lose progress. Here, a verification attempt is presumably a bounded check against a submitted repository, and the design choice is that a failed attempt is a failed attempt rather than something to be resumed. The consequence for a learner is that a flaky check is not silently retried, and the consequence for an operator is that there is no checkpoint state to corrupt.

The claiming mechanism is described as atomic, one attempt at a time per API process, and the source is PostgreSQL rather than an external broker. The pyproject.toml includes asyncpg, and SQLAlchemy is used in async mode, so the claim is a database-level operation rather than an application-level lock. That is the right primitive, and it means the loop works correctly with several API replicas against one database.

The verification code sits in src/learn_to_cloud/verification/, described as verification checks and engine. The curriculum content is in src/learn_to_cloud/content/ as curriculum YAML and compiled JSON, and the build configuration excludes the authored YAML and schemas from the wheel, so only the compiled curriculum ships. The content is therefore authored in version control and compiled at build time, not edited in production.

Sessions are revocable and backed by PostgreSQL

The authentication section is a single sentence in the README, and it points at a document that does the real work.

GitHub login establishes revocable, PostgreSQL-backed sessions. See the Authentication and sessions document for route dependencies, login redirects, expiry and logout guarantees.

The word revocable is the load-bearing one. A session that lives in a signed cookie cannot be withdrawn before it expires, which matters when a learner's GitHub account is deactivated or when an administrator needs to cut access. Storing sessions in PostgreSQL means the server can delete the row.

The implementation is visible in the dependencies. Authlib handles the OAuth flow, itsdangerous provides signed value serialisation, and starlette is required at 1.3.1 or newer with FastAPI at 0.141.1 and below 1. There is also a.mcp.json file at the repository root and agent-framework-core, agent-framework-foundry and agent-framework-openai in the dependency list, which points to the project having an agent-facing surface as well as a user-facing one.

Telemetry is configured through the same core/ package, alongside config, auth and database. The OpenTelemetry instrumentation spans httpx for outbound calls, logging and SQLAlchemy for the database, with the OTLP gRPC exporter sending it out.

A deploy pipeline that will not roll back for you

The deployment section describes a pipeline with opinions, and the opinions are about not making things worse.

CI runs on pull requests. Pushes to main that change infrastructure, application runtime files or the deploy workflow run deploy.yml, one ordered pipeline that runs a single deploy at a time. It plans Terraform and applies it when there are changes. It releases the API when application files differ from the commit production currently runs, building and pushing the API and migration images, running migrations and updating the API. Then it verifies that the latest revision runs the expected image, is healthy, and returns 200 from /ready.

Two rules stand out. Each run releases the latest main, not just the commit that triggered it, so a run that waited behind another deploy cannot roll production back. And tests and documentation do not deploy.

The second point closes a loop that many pipelines leave open. If a run picks up whatever is on main at the time it executes, then a deploy that was queued for hours cannot reintroduce an older commit. The ready check on /ready is a smoke test on the new revision before traffic is considered good.

The recovery instructions are equally specific. After a failed deployment, fix and rerun it before shipping another release, and do not use an old workflow run as an image-only rollback. That last instruction is a direct consequence of the rule above: an old run is not a rollback mechanism, because it would release whatever main was at the time.

There is also a manual Deploy run that redeploys the current main, with a force_rebuild option to rebuild the API without cache and a plan_only option to plan Terraform for the selected branch without applying or deploying.

Editorial conclusion

Learn to Cloud App suits a self-taught engineer following the Learn to Cloud curriculum who wants their project submissions checked and their progress recorded in one place, because the verification loop runs inside the API and the whole course content ships as YAML in the repository. It is the wrong tool as a generic learning management system, since the curriculum, the phases and the checks are all specific to this one course, and it is the wrong choice if you cannot run Docker, since the database and the dashboard are expected to come from compose. Before you deploy it yourself, read docs/authentication.md for the session guarantees, and note the Python constraint in pyproject.toml, which pins requires-python to 3.13 and below 3.14, and check that the Tailwind build stage in the Dockerfile still finds your templates.

Frequently asked questions

How do I run the Learn to Cloud App locally?

Start the dependencies with docker compose up -d db aspire-dashboard, install the Python environment with uv sync --locked and copy .env.example to .env, run uv run alembic upgrade head, then start the API with uv run python -m uvicorn learn_to_cloud.main:app --reload --port 8000.

Does the Learn to Cloud App need a separate worker for verification?

No. The README states that the API starts verification automatically, with one sequential background loop per API process that atomically claims attempts from PostgreSQL, so no separate host, external queue or verification job is required.

How are sessions handled in the Learn to Cloud App?

GitHub login establishes revocable, PostgreSQL-backed sessions. The README points to the authentication and sessions document for route dependencies, login redirects, expiry and logout guarantees.

What is the stack behind the Learn to Cloud App?

Python 3.13+ with FastAPI, SQLAlchemy async and PostgreSQL on the backend, HTMX with Jinja2 templates, Alpine.js and Tailwind CSS v4 on the frontend, GitHub OAuth through Authlib, and Azure Container Apps with Terraform and GitHub Actions for infrastructure.

How does the Learn to Cloud deploy pipeline avoid rolling production back?

Each run releases the latest main rather than the commit that triggered it, so a run that waited behind another deploy cannot roll production back. After a failed deployment the README says to fix and rerun it, and not to use an old workflow run as an image-only rollback.

Official sources

  1. Issues
  2. learntocloud/learn-to-cloud-app on GitHub
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/learntocloud-learn-to-cloud-app.svg)](https://hysenlabs.com/projects/learntocloud-learn-to-cloud-app)

Was this useful?