Web Service
Astro-based frontend that surfaces backend status and metadata for the GitOps platform. It renders static content plus runtime checks against the API service.
Purpose
- Provide a human-facing status surface for the platform
- Display health and metadata from the API service
- Keep UI simple, deterministic, and lightweight
Runtime
- Framework: Astro
- Adapter:
@astrojs/node(standalone server output) - Styling: Tailwind CSS (via
@tailwindcss/vite)
Configuration
The frontend requires the API base URL for server-side rendering:
| Setting | Required | Description |
|---|---|---|
PUBLIC_API_URL |
Yes | Base URL for API requests (e.g. http://api:8000 in Compose/K8s) |
Keep API URL accurate
If PUBLIC_API_URL points to the wrong service, the entire UI will render stale or fail silently. Update this value whenever the API host/port changes and redeploy the web service.
Pages
| Route | Description | API Dependency |
|---|---|---|
/ |
Overview and navigation | None |
/status |
Backend health status | GET /health |
/info |
Backend metadata display | GET /info |
API Integration
API calls use native fetch() directly in each page's frontmatter following Astro's recommended pattern for SSR. Each page wraps fetch in try-catch and returns a normalized { ok, data, error } result object to avoid throwing exceptions during rendering.
Local Development
From the repository root:
pnpm --dir services/web install
PUBLIC_API_URL=http://localhost:8000 pnpm --dir services/web dev --host 0.0.0.0
Build and preview:
pnpm --dir services/web build
pnpm --dir services/web preview
Container Build
The web service uses a multi-stage Dockerfile to separate the build environment from the runtime environment, reducing the final image size and excluding development dependencies from production. See services/web/Dockerfile for the complete specification.
Build Stage (node:20-alpine)
The first stage compiles Astro source code into production-ready static and server assets.
Key steps:
- Accept
PUBLIC_API_URLas a build argument (defaults tohttp://localhost:8000) - Copy
package.jsonandpnpm-lock.yamlfirst for layer caching - Install dependencies with
pnpm install --frozen-lockfile(ensures reproducible builds) - Copy all source files
- Set
PUBLIC_API_URLas environment variable for the build process - Run
pnpm buildto compile assets intodist/
Build-time vs Runtime: PUBLIC_API_URL
Astro (via Vite) bakes PUBLIC_* environment variables into the JavaScript bundle at build time. Changing PUBLIC_API_URL at runtime has no effect. The value must be provided as a build argument when building the image.
This is why Docker Compose passes PUBLIC_API_URL: http://api:8000 as a build arg-it ensures the compiled JavaScript makes API calls to the correct service name within the Docker network.
Runtime Stage (node:20-alpine)
The second stage creates the final image containing only production artifacts.
What's copied:
dist/directory (compiled Astro app with SSR server)package.json(required for Node.js module resolution)
What's excluded:
- Source files (
src/,astro.config.mjs) - Development dependencies (
node_modules/) - Build tools and caches
This reduces the final image size from ~500MB to ~150MB.
Layer Caching Strategy
The build separates dependency installation from source code copying:
- Copy
package.jsonandpnpm-lock.yaml - Run
pnpm install(cached if lockfile unchanged) - Copy source files (invalidates cache only for subsequent layers)
This optimization means changing a single Astro component doesn't require reinstalling all npm packages.
Reproducibility
The --frozen-lockfile flag forces pnpm to use exact versions from pnpm-lock.yaml. If the lockfile is out of sync with package.json, the build fails rather than silently installing different versions. This ensures every build produces byte-identical output given the same inputs.