Skip to content

React Frontend

A React single-page application built with Vite, compiled to static files, and served by Nginx. This example demonstrates the SPA deployment pattern with AppMecca's runtime config injection.

Overview

Repository git@gitlab.com:appmecca-sample-apps/react-frontend.git
Tech Stack React / Vite / Nginx
Services web (port 80)
Database None
Cache None
URL Pattern https://react-frontend-web.{env}.appmecca.net

What This App Demonstrates

  • SPA deployment with type: spa configuration
  • Runtime config injection via __appmecca_config.js
  • Multi-stage Docker builds (Node.js build, Nginx serve)
  • The spa_root directive for config file placement
  • React deployment pattern on AppMecca

appmecca.yaml

containers:
  - name: web
    dockerfile: Dockerfile
    context: .

deploy:
  - name: web
    container: web
    type: spa
    command: 'nginx -g "daemon off;"'   # serves the built bundle from a container
    spa_root: /usr/share/nginx/html
    port: 80
    health_check:
      path: /health

Three fields set this apart from a standard static deployment:

  • type: spa -- tells AppMecca this is a single-page application that needs runtime configuration
  • spa_root -- the directory inside the container where static files are served from
  • command: -- what to run in the container

type: spa must not be ambiguous

A service with type: spa and neither a command: nor a static_site: block is a hard validation error — the platform cannot tell whether you want a container or a static deploy, so it refuses the config rather than guessing. Pick one:

  • keep command: (as above) to run it as a container; or
  • add a static_site: block to build the bundle and publish it to object storage behind the edge cache, with no container at all.

Step-by-Step Deployment

1. Register the app

mecca register git@gitlab.com:appmecca-sample-apps/react-frontend.git -e dev

2. Build the container

mecca build trigger -e dev -g git@gitlab.com:appmecca-sample-apps/react-frontend.git

The build runs a multi-stage Dockerfile: Node.js compiles the React app into static files, then Nginx serves them.

Expected output
Build started: build-xxxxxxxx
Cloning repository...
Building container: web
Step 1/12 : FROM node:20-slim AS builder
Step 7/12 : RUN npm run build
Step 9/12 : FROM nginx:alpine
Pushing to the image registry: appmecca-dev-react-frontend
Build complete. Duration: 52s

3. Deploy

mecca app deploy -e dev react-frontend
Expected output
Deploying react-frontend to dev...
Deployment queued: deploy-xxxxxxxx
Waiting for agents...
Container started: react-frontend-web
Health check passed: /health -> 200 OK
Deployment complete.

Services:
  web: https://react-frontend-web.dev.appmecca.net

4. Verify

mecca app show -e dev react-frontend

Open the app in your browser:

curl https://react-frontend-web.dev.appmecca.net

Key Concepts

SPA runtime config injection

When you set type: spa, AppMecca generates a __appmecca_config.js file at deploy time and mounts it into the container at {spa_root}/__appmecca_config.js. This file exposes environment variables to your frontend JavaScript at runtime -- without rebuilding the container.

Only prefixed variables are exposed

The generated file contains only variables whose names begin with PUBLIC_, VITE_, NEXT_PUBLIC_ or REACT_APP_. Everything else is filtered out so that injected secrets — database passwords, tokens, the mailer credential — can never reach the browser.

A variable named plain API_URL will not appear in the config file. Name it PUBLIC_API_URL (or VITE_API_URL) instead.

The generated file looks like this:

// Generated by APPMECCA at deploy time — do not edit
window.__APPMECCA_CONFIG__ = {
  "PUBLIC_API_URL": "https://myapp-backend.dev.appmecca.net"
};

To use it, add a script tag to your index.html -- before your bundle:

<script src="/__appmecca_config.js"></script>

Then read values in your React code:

const config = window.__APPMECCA_CONFIG__ || {};
const apiUrl = config.PUBLIC_API_URL || "http://localhost:8000";

Why runtime config?

SPAs are compiled to static JavaScript at build time. Without runtime config injection, you would need to rebuild the container every time an environment variable changes. The __appmecca_config.js approach lets you deploy the same image to different environments with different configuration.

How spa_root works

The spa_root field tells AppMecca where your static files live inside the container. AppMecca mounts the generated __appmecca_config.js file into this directory. For Nginx-based containers, this is typically /usr/share/nginx/html.

Web Server Typical spa_root
Nginx /usr/share/nginx/html
Apache /var/www/html
Custom Wherever your server serves static files from

Multi-stage Dockerfile

The React app uses a two-stage build:

# Stage 1: Build the React app
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Serve with Nginx
FROM nginx:alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

The final image is around 40 MB -- it contains only Nginx and the compiled static files. No Node.js runtime, no node_modules, no source code.

Connecting to a backend API

In a multi-container deployment, use the ${service:backend} template to inject the backend URL as an environment variable:

containers:
  - name: web
    dockerfile: Dockerfile
    context: .

  - name: backend
    dockerfile: backend/Dockerfile
    context: backend/

deploy:
  - name: backend
    container: backend
    port: 8000
    path: /api
    health_check:
      path: /health

  - name: web
    container: web
    type: spa
    command: 'nginx -g "daemon off;"'   # serves the built bundle from a container
    spa_root: /usr/share/nginx/html
    port: 80
    health_check:
      path: /health
    environment:
      PUBLIC_API_URL: "${service:backend}"

AppMecca resolves ${service:backend} to that service's public HTTPS URL and, because the name carries the PUBLIC_ prefix, writes it into the runtime config file at deploy time. See Inter-Service Communication for details.

The referenced service must exist

${service:backend} is checked at validation time, not at deploy time. If no service named backend is declared, the config is rejected up front with a list of the names it does know about — you will not discover the typo from a broken app later.

Memory floor for SPA services

A type: spa service is validated against a 32 MB minimum for resources.memory. Below that the nginx worker is killed silently while the master survives, so the service returns 503 with nothing obvious in the logs. Leaving resources out entirely is fine — the default is well above the floor.

SPA vs. SSR

The type: spa setting is for client-side rendered applications (React, Vue, Angular) served as static files. If your frontend uses server-side rendering (Next.js, Nuxt, SvelteKit with adapter-node), do not use type: spa -- deploy it as a regular service instead.

What's Next?