Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.dev
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
REACT_APP_API_ENDPOINT=http://dashboard-be.devv:5000
REACT_APP_API_ENDPOINT=http://dashboard-be.devv/api
REACT_APP_DASHBOARD_NAME=MQP Local
2 changes: 1 addition & 1 deletion .env.production
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
REACT_APP_API_ENDPOINT=https://portal.quantum.lrz.de:5000
REACT_APP_API_ENDPOINT=https://portal.quantum.lrz.de/api
REACT_APP_DASHBOARD_NAME=Munich Quantum Portal
2 changes: 1 addition & 1 deletion .env.stage
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
REACT_APP_API_ENDPOINT=https://portal-stg.quantum.lrz.de:5000
REACT_APP_API_ENDPOINT=https://portal-stg.quantum.lrz.de/api
REACT_APP_DASHBOARD_NAME=MQP Stage
2 changes: 1 addition & 1 deletion .env.test
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
REACT_APP_API_ENDPOINT=https://portal-test.quantum.lrz.de:5000
REACT_APP_API_ENDPOINT=https://portal-test.quantum.lrz.de/api
REACT_APP_DASHBOARD_NAME=MQP Test
39 changes: 39 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Documentation

on:
push:
branches:
- develop
paths:
- 'docs/**'
- 'mkdocs.yml'
pull_request:
paths:
- 'docs/**'
- 'mkdocs.yml'

jobs:
build-docs:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Install dependencies
run: uv pip install --system -r docs/requirements.txt

- name: Build docs
run: mkdocs build --strict

- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
run: mkdocs gh-deploy --force
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
package-lock.json


# dependencies
/node_modules
/.pnp
Expand All @@ -27,7 +23,6 @@ build/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json

# assets
*.mp4
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ── Stage 1: build the React app ──────────────────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .

ARG BUILD_ENV=production
RUN npx env-cmd -f .env.${BUILD_ENV} npm run build:${BUILD_ENV}

# ── Stage 2: serve with nginx ──────────────────────────────────────────────
FROM nginx:alpine

COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx/default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ENV ?= production

build:
BUILD_ENV=$(ENV) docker compose build

up:
docker compose up -d

deploy: build up

down:
docker compose down

logs:
docker compose logs -f react-app
# Usage:
# make deploy ENV=staging
# make deploy ENV=production
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
mqp-app:
build:
context: .
dockerfile: Dockerfile
args:
BUILD_ENV: production # passed to Dockerfile at build time
container_name: mqp-app
restart: unless-stopped
networks:
- mqp-web

networks:
mqp-web:
external: true
167 changes: 167 additions & 0 deletions docs/user_guide/deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Deployment

This document describes how to build and deploy the frontend application using Docker and Docker Compose. It also covers how the app connects to the backend infrastructure at runtime.

## Overview

The frontend is a React (craco) application served by an nginx container. The build process is fully containerised — no local Node.js installation is required to deploy.

At runtime, the app container sits on a shared Docker network alongside the backend services. It is not exposed directly to the internet; an infrastructure-level nginx reverse proxy handles all inbound traffic and routes requests to the appropriate container by hostname.

```
Internet
nginx (infrastructure) ← binds :80 / :443, TLS termination
├── app.example.com → react-app container (this repo)
└── grafana.example.com → grafana container (backend repo)

All containers share a Docker bridge network: web
```

```
Infrastructure repo App repo
────────────────────────────── ────────────────────────────────
nginx container react-app container
- binds :80, :443 - inner nginx serves SPA
- routes by hostname - listens on :80 internally
- TLS termination - NO published ports
- proxies → react-app:80 - only on shared network
- proxies → grafana:3000
│ │
└──────── shared Docker network ─────────┘
```

## Prerequisites

- Docker and Docker Compose installed on the target machine
- The shared Docker network `mqp-web` must already exist (created by the infrastructure stack)
- A clone of this repository on the target machine

## Repository structure

```
MQP-Dashboard-Frontend/
├── ...
├── src/
├── public/
├── nginx/
│ └── default.conf # inner nginx config — serves the SPA
├── Dockerfile # multi-stage: build → serve
├── docker-compose.yml
├── Makefile
├── .env.test
├── .env.staging
└── .env.production
```

## Environment configuration

Build-time environment variables are managed via per-environment `.env.*` files and injected during the React build step using `env-cmd`. The target environment is selected by passing `BUILD_ENV` at build time.

| File | Environment |
|---|---|
| `.env.test` | Test |
| `.env.staging` | Staging |
| `.env.production` | Production |

!!! warning
Never commit secrets or API keys to these files. Use environment-specific secret management for sensitive values.

## Building and deploying

Deployment is managed via `make` targets. All targets accept an optional `ENV` variable (defaults to `production`).

```bash
# Deploy to production (default)
make deploy

# Deploy to staging
make deploy ENV=staging

# Deploy to test
make deploy ENV=test
```

### Available Makefile targets

| Target | Description |
|---|---|
| `make build` | Build the Docker image for the target environment |
| `make up` | Start the container (image must already be built) |
| `make deploy` | Build and start in one step |
| `make down` | Stop and remove the container |
| `make logs` | Tail container logs |

### First-time setup

On a fresh machine, ensure the shared network exists before bringing up the app:

```bash
docker network create mqp-web
```

This network is owned by the infrastructure stack. If the infrastructure stack is already running, the network will already exist.

## How the Docker image is built

The `Dockerfile` uses a multi-stage build:

1. **Stage 1 — build**: installs Node dependencies and runs `env-cmd` with the target `.env.*` file to produce a static build in `/app/build`
2. **Stage 2 — serve**: copies the static build into an nginx Alpine image

The result is a small, self-contained image with no Node.js runtime.

```dockerfile
# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install && npm ci
COPY . .
ARG BUILD_ENV=production
RUN npx env-cmd -f .env.${BUILD_ENV} npm run build:${BUILD_ENV}

# Stage 2: serve
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```

## Inner nginx configuration

The `nginx/default.conf` in this repository is the **inner** nginx config — it runs inside the container and is responsible only for serving the SPA correctly. It is not the infrastructure-level reverse proxy.

Its sole responsibilities are:

- Serving static files from `/usr/share/nginx/html`
- Redirecting all unknown paths to `index.html` so client-side routing works
- Exposing a `/health` endpoint for container health checks

It has no knowledge of domain names, TLS, or upstream services. Those concerns belong to the infrastructure layer.

## Connection to the backend

The app container joins the shared `mqp-web` Docker network at startup. It does not publish any ports to the host directly.

The infrastructure nginx reverse proxy — maintained in the backend repository — routes `mqp.example.com` traffic to this container by its Docker container name (`mqp-app`). No changes to this repository are required when the infrastructure routing configuration changes.

### Grafana embedding

Certain pages embed Grafana dashboard panels via `<iframe>`. Grafana is deployed as part of the backend stack and is accessible at `grafana.example.com`. The infrastructure nginx exposes only the paths required for panel embedding (`/d-solo/`, `/public/`) and blocks all other Grafana routes.

The `Content-Security-Policy: frame-ancestors *.example.com` header set by the infrastructure nginx ensures panels can only be embedded from the shared superdomain.

No Grafana configuration lives in this repository.

## Updating a deployment

To deploy a new version of the app:

```bash
git pull
make deploy ENV=production
```

This rebuilds the image from the latest source and restarts the container. The infrastructure nginx reconnects automatically — no changes to the infrastructure stack are needed.
16 changes: 16 additions & 0 deletions nginx/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;

# SPA fallback — all routes serve index.html
location / {
try_files $uri $uri/ /index.html;
}

# Health check endpoint
location /health {
return 200 "ok";
add_header Content-Type text/plain;
}
}
Loading
Loading