Small pnpm monorepo for a junior DevOps exercise. The application is already written. Container images, Compose, and GitHub Actions are intentionally missing. The assignment is TASKS.md.
apps/web is a static page with one button. The page asks the web server for POST /api/clicks, and that server proxies the call to apps/api.
The API does three things, in order:
- Increments a row in Postgres (
counters.id = 'clicks'). - Writes that count into Valkey with a TTL.
- Reads the count back from Valkey and returns it.
{ "count": 1, "source": "valkey", "persisted": true }GET /health checks Postgres and Valkey. It does not increment the counter. Use it as a readiness probe. A probe against POST /api/clicks would count as a real click.
The schema lives in apps/api/sql/001_counters.sql. The API applies it on startup.
apps/api Node.js HTTP API
apps/web static files plus a small server that proxies /api/clicks
TASKS.md the work to do
- Node.js 24.21.0 (see
.nvmrc). The app is written for the Node 24 LTS line. - pnpm 11.20.0. From the repo root,
corepack enablethencorepack installwill use the version inpackageManager. - PostgreSQL 18 and Valkey 9, only if you want to press the button before the stack is containerized.
Create an empty database named devops_task and a Valkey instance on port 6379. Then, in one shell:
pnpm install
export DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/devops_task
export VALKEY_URL=redis://127.0.0.1:6379
pnpm dev:apiIn a second shell:
export PORT=3000
export API_UPSTREAM=http://127.0.0.1:3001
pnpm dev:webOpen http://127.0.0.1:3000 and press Record click.
Do not source .env.example into both shells. Both processes read PORT, and they need different values. .env.example is documentation. Nothing in this repo loads dotenv.
The local passwords above are for a laptop only. TASKS.md does not allow those values in Compose.
Docker Compose wires the API, web app, Postgres, and Valkey together. Export two passwords in the shell (do not commit real values):
export POSTGRES_PASSWORD='choose-a-strong-postgres-password'
export VALKEY_PASSWORD='choose-a-strong-valkey-password'
docker compose up --buildOpen http://127.0.0.1:3000 and press Record click. The counter should increase on each click.
Stop the stack:
docker compose downPostgres data is kept in a named volume, so docker compose down followed by docker compose up -d preserves the counter. Only the web port is published on the loopback interface.
pnpm check
pnpm testThe tests do not need Postgres or Valkey.
| Variable | Process | Purpose |
|---|---|---|
PORT |
both | API defaults to 3001, web defaults to 3000 |
HOST |
both | Bind address, default 0.0.0.0 |
DATABASE_URL |
API | Postgres connection string |
VALKEY_URL |
API | Valkey connection string. redis:// URLs are correct. A password looks like redis://:<password>@host:6379 |
CACHE_TTL_SECONDS |
API | Cache TTL, default 30, max 86400 |
CORS_ORIGIN |
API | Allowed browser origin for direct API calls. Default http://127.0.0.1:3000 |
API_UPSTREAM |
web | API origin, with no path, query, or credentials. Default http://127.0.0.1:3001 |
Inside Compose, the web service should keep proxying, and the API port should not be published on the host. The button calls the same origin.