Skip to content

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 uses nginx: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:

  1. Two containers -- each with its own Dockerfile and build context
  2. Two services -- each with its own port and health check
  3. 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:

API_URL = https://flask-vue-fullstack-backend.dev.appmecca.net

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 each deploy[].environment entry to docker build as a --build-arg. Name the variable the way your bundler expects (VITE_API_URL for Vite, REACT_APP_API_URL for Create React App) and declare a matching ARG in your Dockerfile.
  • Injected at runtime. Add type: spa and spa_root to the service. AppMecca then writes a __appmecca_config.js file into the container at deploy time and your page loads it with <script src="/__appmecca_config.js"></script>, reading values off window.__APPMECCA_CONFIG__. Only variables prefixed PUBLIC_, VITE_, NEXT_PUBLIC_ or REACT_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

mecca app show -e dev flask-vue-fullstack

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 containers to build separate Docker images from one repo
  • Each container gets its own dockerfile and context
  • Use ${service:name} templates in the environment section 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