Skip to content

Static Sites

This guide covers deploying static websites and single-page applications (SPAs) on AppMecca as containers. We walk through three patterns: plain HTML, a React SPA with a build step, and a SvelteKit app with server-side rendering.

The first two run nginx inside the container; the SvelteKit example runs a Node.js process instead.

There is a simpler option for anything that is just files

If your site has no server-side code, you usually do not want a container at all. A type: static service skips the container entirely: AppMecca runs your build command, uploads the output to object storage and serves it from the edge cache — no Dockerfile, no nginx config, no health check, no memory slot on a host.

See Static Site for that pattern. Use the container patterns below when you need something nginx-specific (custom rewrites, headers, proxying) or when the app runs a real server process.

static-site (Plain HTML + nginx)

The simplest possible static deployment -- just HTML and CSS served by nginx.

Repository structure

static-site/
├── appmecca.yaml
├── Dockerfile
├── nginx.conf
├── index.html
└── style.css

appmecca.yaml

containers:
  - name: static-site
    dockerfile: Dockerfile
    context: .

deploy:
  - name: web
    container: static-site
    port: 80
    health_check:
      path: /health

Dockerfile

FROM nginx:alpine

# Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf

# Copy our nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Copy static files
COPY index.html /usr/share/nginx/html/
COPY style.css /usr/share/nginx/html/

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

nginx.conf

server {
    listen 80;
    server_name _;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 '{"status": "healthy"}';
        add_header Content-Type application/json;
    }
}

Health check in nginx

Static sites don't have application code that can respond to health checks. Instead, define a /health location in your nginx config that returns a 200 response. This is a simple, reliable approach.

Deploy it

mecca register git@gitlab.com:appmecca-sample-apps/static-site.git -e dev
mecca build trigger -e dev -g git@gitlab.com:appmecca-sample-apps/static-site.git
mecca app deploy -e dev static-site

Live at https://static-site-web.dev.appmecca.net — the service name is always part of the hostname.

react-frontend (React SPA + nginx)

A React app built with Vite, compiled to static files, and served by nginx. This is the standard SPA deployment pattern.

Repository structure

react-frontend/
├── appmecca.yaml
├── Dockerfile
├── nginx.conf
├── package.json
├── vite.config.js
├── index.html
├── public/
└── src/
    ├── App.jsx
    └── main.jsx

appmecca.yaml

containers:
  - name: react-frontend
    dockerfile: Dockerfile
    context: .

deploy:
  - name: web
    container: react-frontend
    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: "https://api.example.com"

Setting type: spa enables runtime config injection. AppMecca generates a __appmecca_config.js file at deploy time and mounts it into the container at {spa_root}/__appmecca_config.js. Add <script src="/__appmecca_config.js"></script> to your index.html and read values from window.__APPMECCA_CONFIG__.

Prefix anything the browser must see

Only variables named with a PUBLIC_, VITE_, NEXT_PUBLIC_ or REACT_APP_ prefix are written into the config file — everything else is filtered out so injected secrets cannot reach the browser. A variable named plain API_URL simply will not appear, which is why the example above uses PUBLIC_API_URL.

type: spa must not be ambiguous

The command: line above is doing real work. A type: spa service with 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 and refuses to guess.

Dockerfile (multi-stage)

# Build stage
FROM node:20-slim AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY index.html vite.config.js ./
COPY src/ src/
COPY public/ public/

RUN npm run build

# Runtime stage - serve static files 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;"]

This is a multi-stage build:

  1. The builder stage installs Node.js dependencies and runs npm run build to compile the React app into static HTML/CSS/JS files in the dist/ directory
  2. The runtime stage copies those static files into an nginx container

The final image is tiny (around 40 MB) because it only contains nginx and the built static files -- no Node.js runtime, no node_modules.

nginx.conf

The nginx config is the same as the static site example. The try_files directive with /index.html fallback is essential for SPAs:

server {
    listen 80;
    server_name _;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /health {
        access_log off;
        return 200 '{"status": "healthy"}';
        add_header Content-Type application/json;
    }
}

Why try_files with /index.html?

SPAs handle routing on the client side. When a user visits /about, nginx won't find a file at that path. The try_files directive tells nginx to serve index.html for any path that doesn't match a real file. The JavaScript router in your app then handles the /about route.

Deploy it

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

Live at https://react-frontend-web.dev.appmecca.net.

svelte-app (SvelteKit with SSR)

SvelteKit with adapter-node runs as a Node.js server, enabling server-side rendering. This is different from the static patterns above -- the container runs an actual Node.js process.

appmecca.yaml

containers:
  - name: svelte-app
    dockerfile: Dockerfile
    context: .

deploy:
  - name: web
    container: svelte-app
    port: 3000
    health_check:
      path: /health

Dockerfile

FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/build/ ./build/
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "build"]

SSR vs. static

The SvelteKit example uses adapter-node and runs a Node.js server. If you want a purely static SvelteKit site, use adapter-static and serve the output with nginx (same pattern as the React example).

Deploy it

mecca register git@gitlab.com:appmecca-sample-apps/svelte-app.git -e dev
mecca build trigger -e dev -g git@gitlab.com:appmecca-sample-apps/svelte-app.git
mecca app deploy -e dev svelte-app

Live at https://svelte-app-web.dev.appmecca.net.

Choosing a Pattern

Pattern When to use Runtime Image size
type: static (no container) Anything that is just files — marketing pages, docs, most SPAs None No image at all
Static HTML + nginx You need custom nginx behaviour (rewrites, headers, proxying) None (nginx only) ~40 MB
SPA + nginx Client-side-routed apps that also need custom nginx behaviour None (nginx only) ~40 MB
SvelteKit + Node.js Apps that need server-side rendering Node.js ~200 MB

Reach for type: static first — it needs no image, no health check and no memory on a host, and it is served from the edge cache. See Static Site.

Drop to the nginx container patterns when you genuinely need nginx config. Use SSR (SvelteKit/Next.js with Node.js) only when you need server-side rendering for SEO or dynamic content.

Key Takeaways

  • Prefer type: static when the site is just files — no container, no image, no health check
  • When you do use a container, static sites and SPAs use nginx as the web server
  • Define the health check endpoint in nginx.conf (not application code)
  • Use multi-stage builds to keep the final image small -- build with Node.js, serve with nginx
  • SPA routing needs try_files $uri $uri/ /index.html in nginx
  • Values the browser must read need a PUBLIC_ / VITE_ / NEXT_PUBLIC_ / REACT_APP_ prefix
  • SSR apps (SvelteKit adapter-node) run a Node.js server and use a different port (3000 vs 80)