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¶
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:
- The
builderstage installs Node.js dependencies and runsnpm run buildto compile the React app into static HTML/CSS/JS files in thedist/directory - 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: staticwhen 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.htmlin 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)