Commit a0b70f3b authored by Markus Mößler's avatar Markus Mößler
Browse files

updated readme and documentation

parent 7652039f
Loading
Loading
Loading
Loading
Loading
+2 −13
Original line number Diff line number Diff line
@@ -8,10 +8,7 @@ Please check out the GitLab page: [Gitlab aidaho-tinkering-club-web-app/page/ind

Note, this is work in progress and for educational purposes and not state of the art best practice,

Potential future steps:

* Deployment procedure
* Addition of an nginx revers proxy
**Important**: The documentation of the latest step for the development and deployment of the Tornado Blog Demo (with react for the frontend) can be found [here](./doc/react_tornado_blog_dev_dep.md)

## Approach

@@ -43,19 +40,11 @@ Use, e.g., `git diff tornado-backend-stage-01:tornado-backend/blog.py react-torn
### Stage/Step 4

* (Full-stack) replication of the Tornado blog demo with react (frontend) and Tornado (backend).
* This can be found on branch `react-tornado-blog-stage-01`.
* This can be found on branch `react-tornado-blog-stage-01` to `react-tornado-blog-stage-04`
* Main changes:
  * Addition of additinal components in the frontend
  * Addition of additional routes and handlers in the backend

### Stage/Step 5

* (Full-stack) replication of the Tornado blog demo with react (frontend), Tornado (backend) and PostgreSQL (backend) served via nginx.
* Extension of branch `react-tornado-blog-stage-02`
* Included nginx service as reverse proxy between front and backend
* Added configuration for serving of static pages via nginx service
* Added persistent pg data bind

...

## Further Notes,
+390 −0
Original line number Diff line number Diff line
# React Tornado Blog Demo (Development and Deployment)

**Development and deployment of multiple isolated student apps served on the same machine** via one centralized, containerized Nginx service.

Accessible under the Tinkering Club domain:  
➡️ https://aidaho-tinkering-club.uni-hohenheim.de

Replication of the Tornado Blog demo [see GitHub Tornado Blog Dem](https://github.com/tornadoweb/tornado/blob/master/demos/blog/blog.py) using:
- Python **Tornado** (backend)
- **PostgreSQL** (database)
- **React** (frontend)

Source branch: `react-tornado-blog-stage-04`  
Repository: [aidaho-tinkering-club-web-app](https://aidaho-edu.uni-hohenheim.de/gitlab/mmoessler/aidaho-tinkering-club-web-app)

See Example deployment:

- [https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-01/](https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-01/)
- [https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-02/](https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-02/)

---

## 🧱 Development Setup

### `docker-compose.yml`

```yaml
services:
  react-frontend:
    build:
      context: ./react-frontend
      dockerfile: Dockerfile
    volumes:
      - ./react-frontend/my-react-app:/app
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true
    environment:
      - CHOKIDAR_USEPOLLING=true # Support live reloading on mounted volumes
    command: ["npm", "start"]
    depends_on:
      - tornado-backend
    networks:
      - app-network

  tornado-backend:
    build: ./tornado-backend
    links:
      - postgres
    ports:
      - "8888:8888"
    volumes:
      - ./tornado-backend/blog.py:/usr/src/app/blog.py
    command: --db_host=postgres
    networks:
      - app-network

  postgres:
    image: postgres:10.3
    environment:
      POSTGRES_USER: blog
      POSTGRES_PASSWORD: blog
      POSTGRES_DB: blog
    ports:
      - "5432:5432"
    volumes:
      - ./pg_data:/var/lib/postgresql/data:rw
    networks:
      - app-network

  adminer:
    image: adminer
    ports:
      - 8080:8080
    networks:
      - app-network

networks:
  app-network:
    driver: bridge
```

```mermaid
graph TD

  subgraph DevMachine["💻 Developer Machine"]
    subgraph DockerNetwork["Docker Network: app-network"]
      
      React["🟦 react-frontend<br>Port 3000<br><small>npm start, live reload</small>"]
      Tornado["🟧 tornado-backend<br>Port 8888<br><small>Python Tornado server</small>"]
      Postgres["🟩 postgres<br>Port 5432<br><small>Database (blog)</small>"]
      Adminer["🟪 adminer<br>Port 8080<br><small>DB UI</small>"]
      
    end
  end

  %% External user interaction
  User["🧑‍💻 Web Browser<br><small>http://localhost:3000</small>"]
  User -->|HTTP requests| React

  %% React and Tornado connection
  React -->|"API calls<br>fetch()"| Tornado
  Tornado -->|DB queries| Postgres

  %% Adminer connects to DB
  Adminer -->|Manage database| Postgres

  %% Notes
  classDef svc fill:#f9f9f9,stroke:#bbb,stroke-width:1px,rx:8,ry:8;
  class React,Tornado,Postgres,Adminer svc;

  %% Dependencies
  Tornado -.depends_on.-> Postgres
  React -.depends_on.-> Tornado
```

---

## 🚀 Deployment

### Overview

Each student app consists of:
- One **backend container** (`tornado-backend` + `postgres`)
- A **frontend React build**
- Served by one shared **Nginx reverse proxy**

Each backend joins a **shared external network**: `nginx-net`.

---

### 🧩 Example Backend Configuration

Example for **blog demo 01**:

```yaml
version: '3.8'

services:
  tornado-backend:
    build: ./tornado-backend
    command: --db_host=postgres
    depends_on:
      - postgres
    expose:
      - "8888"
    restart: unless-stopped
    networks:
      blog-demo-01-net:
      nginx-net:
        aliases:
          - blog-demo-01-backend

  postgres:
    image: postgres:10.3
    environment:
      POSTGRES_USER: blog
      POSTGRES_PASSWORD: blog
      POSTGRES_DB: blog
    volumes:
      - ./pg_data:/var/lib/postgresql/data:rw
    restart: unless-stopped
    networks:
      - blog-demo-01-net

networks:
  blog-demo-01-net:
    driver: bridge
  nginx-net:
    external: true
```

---

### 🧠 Student Deployment Workflow

1. **Clone your app repository**
   ```bash
   cd /srv/projects/
   git clone https://aidaho-edu.uni-hohenheim.de/gitlab/mmoessler/aidaho-tinkering-club-web-app my-app
   ```

2. **Create your own docker-compose.prod.yml** based on the example above.

3. **Connect your backend container** to the shared Nginx network:
   ```bash
   docker network connect nginx-net <your_backend_container_name>
   ```

4. **Request a route from the administrator**.  
   Provide:
   - Your app name (e.g., `blog-demo-03`)
   - Desired path (`/blog-demo-03/`)
   - Backend alias (`blog-demo-03-backend`)

5. **Build your React frontend**
   ```bash
   docker run -it --rm -v $(pwd)/react-frontend/my-react-app:/app -w /app node:20-alpine sh
   npm install
   npm run build
   ```

6. **Copy the built files** to the shared nginx assets directory:
   ```bash
   cp -rf ./react-frontend/my-react-app/build/* /srv/nginx/assets/static/blog-demo-03/
   ```

7. **Restart your backend container:**
   ```bash
   docker-compose -f docker-compose.prod.yml up -d --build
   ```

---

## 🧭 Frontend Configuration

In `react-frontend/my-react-app`: Here to deploy a second blog app, i.e., `blog-demo-02`

| File | Setting | Example |
|------|----------|----------|
| `package.json` | `"homepage"` | `"/blog-demo-02"` |
| `src/App.js` | `<Router basename>` | `"/blog-demo-02"` |
| `src/config.js` | `frontendBaseUrl`, `backendBaseUrl` | Frontend: `https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-02` <br> Backend: `https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-02/api` |

---

## 🌐 Central Nginx Reverse Proxy

### docker-compose (shared)

```yaml
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./assets/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./assets/nginx/proxy.conf:/etc/nginx/proxy.conf:ro
      - ./assets/static:/usr/share/nginx/html:rw
      - /srv/startpage:/srv/startpage:ro
      - ./assets/nginx/ssl:/etc/nginx/ssl:ro
    restart: always
    networks:
      - nginx-net

networks:
  nginx-net:
    external: true
```

---

### nginx configuration

```nginx
include /etc/nginx/proxy.conf;

server {
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name aidaho-tinkering-club.uni-hohenheim.de;

    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # Security Headers
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options DENY;
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Permissions-Policy "geolocation=(), microphone=()" always;

    # ---- App: blog-demo-01 ----
    location /blog-demo-01/api/ {
        proxy_pass http://blog-demo-01-backend:8888/api/;
        include /etc/nginx/proxy.conf;
    }

    location /blog-demo-01/ {
        alias /usr/share/nginx/html/blog-demo-01/;
        try_files $uri $uri/ /blog-demo-01/index.html;
    }

    # ---- App: blog-demo-02 ----
    location /blog-demo-02/api/ {
        proxy_pass http://blog-demo-02-backend:8888/api/;
        include /etc/nginx/proxy.conf;
    }

    location /blog-demo-02/ {
        alias /usr/share/nginx/html/blog-demo-02/;
        try_files $uri $uri/ /blog-demo-02/index.html;
    }

    # ---- Root Start Page ----
    location / {
        root /srv/startpage;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    # ---- Static Assets ----
    location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
        expires 30d;
        access_log off;
    }

    autoindex off;
}
```

---

## Visualization

```mermaid
graph TD

  %% Internet user
  User["🧑‍💻 User Browser<br><small>https://aidaho-tinkering-club.uni-hohenheim.de</small>"]

  %% Central reverse proxy
  Nginx["🌐 Central Nginx Reverse Proxy<br><small>Container on nginx-net<br>Ports 80/443</small>"]

  %% External connection
  User -->|"HTTPS (443)"| Nginx

  %% App 01 group
  subgraph App01["🧩 Blog Demo 01"]
    FE01["🟦 Built React App<br><small>/usr/share/nginx/html/blog-demo-01</small>"]
    BE01["🟧 Tornado Backend<br><small>blog-demo-01-backend:8888</small>"]
    DB01["🟩 Postgres DB<br><small>blog-demo-01-net</small>"]
    FE01 -->|"API calls /blog-demo-01/api/"| BE01
    BE01 -->|"SQL queries"| DB01
  end

  %% App 02 group
  subgraph App02["🧩 Blog Demo 02"]
    FE02["🟦 Built React App<br><small>/usr/share/nginx/html/blog-demo-02</small>"]
    BE02["🟧 Tornado Backend<br><small>blog-demo-02-backend:8888</small>"]
    DB02["🟩 Postgres DB<br><small>blog-demo-02-net</small>"]
    FE02 -->|"API calls /blog-demo-02/api/"| BE02
    BE02 -->|"SQL queries"| DB02
  end

  %% Nginx routing
  Nginx -->|"/blog-demo-01/ → static frontend"| FE01
  Nginx -->|"/blog-demo-01/api/ → proxy_pass"| BE01
  Nginx -->|"/blog-demo-02/ → static frontend"| FE02
  Nginx -->|"/blog-demo-02/api/ → proxy_pass"| BE02

  %% Start page
  Nginx -->|"/ → startpage (/srv/startpage)"| StartPage["📄 Static Start Page"]

  %% Style definitions
  classDef box fill:#f9f9f9,stroke:#bbb,stroke-width:1px,rx:8,ry:8;
  class Nginx,FE01,FE02,BE01,BE02,DB01,DB02,StartPage box;

  %% Networks
  Nginx --- FE01 & FE02 & BE01 & BE02:::box
  BE01 --- DB01
  BE02 --- DB02
```

## ✅ Example URLs

- [https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-01/](https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-01/)
- [https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-02/](https://aidaho-tinkering-club.uni-hohenheim.de/blog-demo-02/)

---

## 🧩 Optional Enhancements

- **Multi-stage Docker builds** for React apps (build → serve).  
- **Automated SSL** with `letsencrypt-nginx-proxy-companion`.  
- **Template repository** for new student apps.  
- Centralized **volume directories** per app (e.g., `/srv/dbs/blog-demo-01/`).

---

© 2025 University of Hohenheim — Aidaho Tinkering Club