Part 4: SPA Runtime Config¶
Replace build-time API URLs with runtime configuration so the same container image deploys to any environment without rebuilding.
What you'll learn¶
- The difference between build-time and runtime configuration
- Using AppMecca's
type: spaandspa_rootdirectives - Reading
window.__APPMECCA_CONFIG__in Vue - Managing environment variables with
mecca var
What you'll need¶
- A multi-container deployment from Part 3
The problem with build-time config¶
In Part 3, the backend URL was injected at build time via VITE_API_URL. This means:
- Promoting an image from
devtostagingrequires a full rebuild - Every environment gets a different container image
- Build-time secrets end up baked into the image layers
Build-time leaks
Anything passed as a Docker build argument is visible in the image history. Never pass secrets as build args. Runtime injection solves both the portability and security problems.
How AppMecca SPA runtime config works¶
When a deploy service has type: spa, AppMecca:
- Scans the container's environment variables for keys starting with
PUBLIC_ - Generates a
__appmecca_config.jsfile containing those values - Writes it into the
spa_rootdirectory inside the container - Your HTML loads this script, and Vue reads from
window.__APPMECCA_CONFIG__
The container image stays identical across environments. Only the injected config changes.
Update appmecca.yaml¶
Change the frontend deploy configuration to use type: spa:
database:
enabled: true
containers:
- name: backend
dockerfile: backend/Dockerfile
context: backend/
- name: frontend
dockerfile: frontend/Dockerfile
context: frontend/
deploy:
- name: backend
container: backend
port: 8080
path: /api
health_check:
path: /health
- name: frontend
container: frontend
type: spa
command: 'nginx -g "daemon off;"' # serves the built bundle from a container
spa_root: /usr/share/nginx/html
port: 3000
health_check:
path: /
environment:
PUBLIC_API_URL: "${service:backend}"
PUBLIC_APP_TITLE: "Todo App"
No more build args
The args block on the frontend container is gone. VITE_API_URL is no longer needed at build time. The PUBLIC_* environment variables are injected at deploy time.
Remove the build arg from the frontend Dockerfile¶
# frontend/Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 3000
The Dockerfile no longer accepts or uses VITE_API_URL.
Load the config script in index.html¶
Add the script tag to your frontend/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo App</title>
<script src="/__appmecca_config.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Create a config helper¶
// frontend/src/config.js
/**
* Read a runtime config value injected by AppMecca.
* Falls back to import.meta.env for local development.
* @param {string} key - Config key (e.g., 'PUBLIC_API_URL')
* @param {string} fallback - Default value if not found
* @returns {string}
*/
export function getConfig(key, fallback = '') {
const runtime = window.__APPMECCA_CONFIG__
if (runtime && runtime[key] !== undefined) {
return runtime[key]
}
return import.meta.env[`VITE_${key}`] || fallback
}
Local development
The getConfig helper falls back to import.meta.env variables. During local development with npm run dev, create a .env file with VITE_PUBLIC_API_URL=http://localhost:8080 and everything works the same.
Update App.vue¶
<!-- frontend/src/App.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import { getConfig } from './config.js'
const API_URL = getConfig('PUBLIC_API_URL')
const APP_TITLE = getConfig('PUBLIC_APP_TITLE', 'Todos')
const todos = ref([])
const newTitle = ref('')
async function fetchTodos() {
const res = await fetch(`${API_URL}/api/todos`)
todos.value = await res.json()
}
async function addTodo() {
if (!newTitle.value.trim()) return
await fetch(`${API_URL}/api/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value })
})
newTitle.value = ''
await fetchTodos()
}
async function toggleTodo(todo) {
await fetch(`${API_URL}/api/todos/${todo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...todo, done: !todo.done })
})
await fetchTodos()
}
async function deleteTodo(id) {
await fetch(`${API_URL}/api/todos/${id}`, { method: 'DELETE' })
await fetchTodos()
}
onMounted(fetchTodos)
</script>
<template>
<div class="container">
<h1>{{ APP_TITLE }}</h1>
<form @submit.prevent="addTodo">
<input v-model="newTitle" placeholder="What needs doing?" />
<button type="submit">Add</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" :checked="todo.done" @change="toggleTodo(todo)" />
<span :class="{ done: todo.done }">{{ todo.title }}</span>
<button @click="deleteTodo(todo.id)">Delete</button>
</li>
</ul>
</div>
</template>
Manage environment variables¶
Set additional runtime variables without rebuilding. The app name is the first argument, and several KEY=value pairs can go in one command:
mecca var set todo-app PUBLIC_APP_TITLE="My Todos"
mecca var set todo-app PUBLIC_APP_TITLE="My Todos" PUBLIC_SUPPORT_EMAIL="help@example.com"
var set writes to every branch by default
With no -b, the value lands on all of the app's branches. Narrow it explicitly when that is not what you want:
List current variables:
mecca var list todo-app
mecca var list todo-app -b main --dotenv > .env # .env lines; needs one branch
Remove a variable:
mecca var unset todo-app PUBLIC_SUPPORT_EMAIL
mecca var unset todo-app PUBLIC_SUPPORT_EMAIL -b staging
Redeploy after changes
Environment variable changes take effect on the next deployment, not immediately. After mecca var set, run mecca app deploy todo-app --no-build to apply them without a rebuild.
Build and deploy¶
The same image now works in dev, staging, and production -- only the PUBLIC_* values change.
Command reference¶
| Command | Purpose |
|---|---|
mecca var set <app> KEY=value [...] |
Set variables (all branches unless -b) |
mecca var list <app> |
List variables, grouped by branch |
mecca var list <app> -b <branch> --dotenv |
Emit .env lines for one branch |
mecca var unset <app> KEY |
Remove a variable |
What's next¶
In Part 5: Background Workers, you will add a Spring Task worker process backed by Redis for asynchronous job processing.