Multi-Container App¶
This guide walks through deploying a full-stack application with separate frontend and backend containers from a single repository. We use the flask-vue-fullstack sample app -- a Flask API backend with a Vue.js frontend.
When to Use Multiple Containers¶
Use multiple containers when your application has components that:
- Are built with different languages or runtimes (e.g., Python backend + JavaScript frontend)
- Have different Dockerfiles (e.g., one uses
python:3.12-slim, the other usesnginx:alpine) - Should scale independently in the future
If your services use the same language, same Dockerfile, and just run different commands, you may not need multiple containers. See Background Workers for the single-image, multiple-command pattern.
flask-vue-fullstack¶
Repository structure¶
flask-vue-fullstack/
├── appmecca.yaml
├── backend/
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
└── frontend/
├── Dockerfile
├── package.json
├── src/
└── nginx.conf
appmecca.yaml¶
containers:
- name: flask-vue-backend
dockerfile: backend/Dockerfile
context: backend/
- name: flask-vue-frontend
dockerfile: frontend/Dockerfile
context: frontend/
deploy:
- name: backend
container: flask-vue-backend
port: 5000
health_check:
path: /api/health
- name: frontend
container: flask-vue-frontend
port: 80
health_check:
path: /health
environment:
API_URL: "${service:backend}"
database:
enabled: true
There are three important things happening here:
- Two containers -- each with its own Dockerfile and build context
- Two services -- each with its own port and health check
- Inter-service URL -- the frontend gets the backend's URL via
${service:backend}
How the containers connect¶
User (browser)
|
| HTTPS
v
AppMecca router ──> frontend (nginx, port 80) → flask-vue-fullstack-frontend.dev.appmecca.net
|
| HTTPS
v
AppMecca router ──> backend (Flask, port 5000) → flask-vue-fullstack-backend.dev.appmecca.net
The frontend serves the Vue.js SPA. The JavaScript in the browser makes API calls to the backend URL, which was injected as the API_URL environment variable at deploy time.
Backend Dockerfile¶
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
Frontend Dockerfile¶
The frontend uses a multi-stage build: Node.js builds the Vue app, then nginx serves the static files.
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
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;"]
Frontend nginx.conf¶
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;
}
}
The health endpoint in nginx
Since the frontend is just static files served by nginx, the health check endpoint is defined in the nginx config rather than in application code. The location /health block returns a 200 response directly.
Inter-Service Communication¶
The ${service:backend} template in the frontend's environment resolves to the backend's public URL:
Your frontend JavaScript reads this environment variable and uses it for API calls:
// In your Vue/React/Svelte code
const API_URL =
window.__APPMECCA_CONFIG__?.VITE_API_URL || import.meta.env.VITE_API_URL;
async function fetchItems() {
const response = await fetch(`${API_URL}/api/items`);
return response.json();
}
Build-time vs. runtime
A browser cannot read a container environment variable. For an SPA served as static files by nginx, the API URL has to reach the JavaScript one of two ways:
- Baked in at build time. AppMecca resolves
${service:name}while building and passes eachdeploy[].environmententry todocker buildas a--build-arg. Name the variable the way your bundler expects (VITE_API_URLfor Vite,REACT_APP_API_URLfor Create React App) and declare a matchingARGin your Dockerfile. - Injected at runtime. Add
type: spaandspa_rootto the service. AppMecca then writes a__appmecca_config.jsfile into the container at deploy time and your page loads it with<script src="/__appmecca_config.js"></script>, reading values offwindow.__APPMECCA_CONFIG__. Only variables prefixedPUBLIC_,VITE_,NEXT_PUBLIC_orREACT_APP_are included; everything else is filtered out so secrets cannot reach the browser.
The plain API_URL name used above works for neither — rename it to a
prefixed form before relying on it in the browser.
See Inter-Service Communication for the complete template reference.
Deploy It¶
# Register
mecca register git@gitlab.com:appmecca-sample-apps/flask-vue-fullstack.git -e dev
# Build (builds BOTH containers)
mecca build trigger -e dev -g git@gitlab.com:appmecca-sample-apps/flask-vue-fullstack.git
# Deploy
mecca app deploy -e dev flask-vue-fullstack
Expected output
Deploying flask-vue-fullstack to dev...
Deployment queued: deploy-xxxxxxxx
Waiting for agents...
Container started: flask-vue-fullstack-backend
Container started: flask-vue-fullstack-frontend
Health check passed: /api/health -> 200 OK
Health check passed: /health -> 200 OK
Deployment complete.
Services:
backend: https://flask-vue-fullstack-backend.dev.appmecca.net
frontend: https://flask-vue-fullstack-frontend.dev.appmecca.net
Verify¶
Both services should show as running and healthy.
Multi-Service URL Patterns¶
With two services that have ports, the URLs follow the {app}-{service} pattern:
| Service | URL |
|---|---|
| backend | https://flask-vue-fullstack-backend.dev.appmecca.net |
| frontend | https://flask-vue-fullstack-frontend.dev.appmecca.net |
The service name is always part of the URL — there is no bare
https://flask-vue-fullstack.dev.appmecca.net form, even for an app with a
single port-bearing service.
Key Takeaways¶
- Define multiple entries in
containersto build separate Docker images from one repo - Each container gets its own
dockerfileandcontext - Use
${service:name}templates in theenvironmentsection to connect services - When you trigger a build, all containers are built together
- Each service with a port gets its own URL:
{app}-{service}.{env}.{domain} - Services without a port (workers) don't get URLs but are still deployed