Skip to content

Multi-Container Builds

Most real-world applications have more than one piece: a frontend and a backend, or an API server and a background worker. AppMecca lets you build all of them from a single Git repository in one build run.

Why Multi-Container?

Instead of maintaining separate repositories for your frontend and backend, you can keep everything in one repo (a monorepo) and let AppMecca build all the images at once.

Benefits:

  • One build command builds everything
  • Atomic versioning — all containers share the same Git commit hash
  • Easier coordination — frontend and backend changes ship together
  • Simpler repository management — one repo, one set of CI/CD, one code review

How It Works

When you define multiple entries in the containers section of appmecca.yaml, AppMecca builds them all sequentially in a single build run. Each container gets its own repository in the private image registry and its own image tag.

containers:
  - name: frontend
    dockerfile: frontend/Dockerfile
    context: frontend/

  - name: backend
    dockerfile: backend/Dockerfile
    context: backend/

deploy:
  - name: frontend
    container: frontend
    port: 3000

  - name: backend
    container: backend
    port: 8080

When you trigger a build:

mecca build trigger -e dev -g git@github.com:yourorg/myapp.git

AppMecca will:

  1. Clone the repository
  2. Read appmecca.yaml
  3. Build frontend using frontend/Dockerfile
  4. Push frontend to the image registry as appmecca/dev/myapp/frontend:a1b2c3d
  5. Build backend using backend/Dockerfile
  6. Push backend to the image registry as appmecca/dev/myapp/backend:a1b2c3d

Both images get the same commit hash tag because they were built from the same commit.

Expected output
[Build] Cloning repository...
[Build] Reading appmecca.yaml...
[Build] Building container 1/2: frontend
[Build] Pushing image: appmecca/dev/myapp/frontend:a1b2c3d
[Build] Building container 2/2: backend
[Build] Pushing image: appmecca/dev/myapp/backend:a1b2c3d
Build completed successfully in 6m 15s.

Image Repository Naming

Each container gets its own repository in the private image registry, following the pattern:

appmecca/{environment}/{app}/{container-name}
Container name App Environment Image repository
frontend myapp dev appmecca/dev/myapp/frontend
backend myapp dev appmecca/dev/myapp/backend
worker myapp staging appmecca/staging/myapp/worker
api myapp prod appmecca/prod/myapp/api

Container names become platform resource names

Keep container names short and lowercase. They are used in image repository names, routing resource names, and other platform resources that have character length limits. Use hyphens, not underscores.

Complete Example: Full-Stack App

Here is a full appmecca.yaml for a typical frontend + backend application:

# Build configuration
containers:
  - name: frontend
    dockerfile: frontend/Dockerfile
    context: frontend/
    args:
      NODE_ENV: production

  - name: backend
    dockerfile: backend/Dockerfile
    context: backend/

# Deployment configuration
deploy:
  - name: frontend
    container: frontend
    port: 3000
    health_check:
      path: /
    environment:
      API_URL: "${service:backend:internal}"

  - name: backend
    container: backend
    port: 8080
    health_check:
      path: /health

# Enable PostgreSQL for the backend
database:
  enabled: true

This gives you:

  • https://myapp-frontend-yourgroup.dev.yourdomain.io — serves the frontend
  • https://myapp-backend-yourgroup.dev.yourdomain.io — serves the API
  • The frontend's API_URL environment variable automatically points to the backend's URL

Every service gets its own URL

A service hostname always contains the service name and your group slug — there is no bare myapp.dev.… URL, even for a single-service app. Run mecca app show myapp to see the real URLs for your app.

The container field links build to deploy

Notice how deploy[].container references containers[].name. This is how AppMecca knows which image to use for each service. The names must match exactly.

Repository Layouts

AppMecca is flexible about how you organize your repository. Here are the most common patterns.

Each service has its own top-level directory with its own Dockerfile:

myapp/
├── appmecca.yaml
├── frontend/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
│       └── ...
└── backend/
    ├── Dockerfile
    ├── requirements.txt
    └── src/
        └── ...
containers:
  - name: frontend
    dockerfile: frontend/Dockerfile
    context: frontend/

  - name: backend
    dockerfile: backend/Dockerfile
    context: backend/

deploy:
  - name: frontend
    container: frontend
    port: 3000

  - name: backend
    container: backend
    port: 8080

Each Dockerfile only sees files from its own directory. This keeps build contexts small and builds fast.

Pattern 2: Services Directory

For projects with many services, a services/ directory keeps things organized:

myapp/
├── appmecca.yaml
└── services/
    ├── web/
    │   ├── Dockerfile
    │   └── ...
    ├── api/
    │   ├── Dockerfile
    │   └── ...
    └── worker/
        ├── Dockerfile
        └── ...
containers:
  - name: web
    dockerfile: services/web/Dockerfile
    context: services/web/

  - name: api
    dockerfile: services/api/Dockerfile
    context: services/api/

  - name: worker
    dockerfile: services/worker/Dockerfile
    context: services/worker/

deploy:
  - name: web
    container: web
    port: 3000

  - name: api
    container: api
    port: 8080

  - name: worker
    container: worker
    # No port = internal service, no routing, no URL

Pattern 3: Shared Code (Root Context)

When your services share code (like a common library or shared types), set the build context to the repository root:

myapp/
├── appmecca.yaml
├── shared/
│   └── models.py
├── frontend/
│   ├── Dockerfile
│   └── ...
└── backend/
    ├── Dockerfile
    └── ...
containers:
  - name: frontend
    dockerfile: frontend/Dockerfile
    context: .

  - name: backend
    dockerfile: backend/Dockerfile
    context: .

deploy:
  - name: frontend
    container: frontend
    port: 3000

  - name: backend
    container: backend
    port: 8080

With context: ., both Dockerfiles can COPY from anywhere in the repository, including the shared/ directory:

# backend/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY shared/ ./shared/
COPY backend/ ./backend/
RUN pip install -r backend/requirements.txt
CMD ["python", "-m", "backend.main"]

Root context means larger builds

When you set context: ., Docker sends the entire repository to the build engine. This is fine for small repos, but can slow down builds for large ones. If your services don't actually share code, use separate contexts (Pattern 1) for faster builds.

One Image, Multiple Services

You don't always need multiple containers. If your frontend and backend use the same language and runtime, you can build one image and run it with different commands:

containers:
  - name: myapp
    dockerfile: Dockerfile
    context: .

deploy:
  - name: web
    container: myapp
    port: 8000
    command: "gunicorn app:application -w 4 -b 0.0.0.0:8000"

  - name: worker
    container: myapp
    command: "celery -A app worker -l info"

  - name: scheduler
    container: myapp
    command: "celery -A app beat -l info"

This builds one image and runs three services from it. The command field overrides the Dockerfile CMD for each service. Only the web service gets a URL (because it has a port); worker and scheduler run in the background.

When to use one image vs multiple

  • One image, multiple services: Great for Python apps where the web server, worker, and scheduler all share the same codebase (Django + Celery is the classic example)
  • Multiple images: Necessary when services use different languages or runtimes (e.g., a React frontend and a Python backend)

Build Order

When you have multiple containers, AppMecca builds them in the order they appear in appmecca.yaml. All containers are built in sequence within a single build run.

containers:
  - name: frontend    # Built first
    dockerfile: frontend/Dockerfile
    context: frontend/

  - name: backend     # Built second
    dockerfile: backend/Dockerfile
    context: backend/

deploy:
  - name: frontend
    container: frontend
    port: 3000

  - name: backend
    container: backend
    port: 8080

If any container fails to build, the entire build is marked as failed and no images are pushed. This ensures you never end up with a new frontend but an old backend.

All or nothing

You cannot build just one container from a multi-container appmecca.yaml. When you trigger a build, all containers are built. This ensures all images in a deployment are from the same commit.

Passing Build Arguments Between Containers

Sometimes your frontend needs to know the backend's URL at build time (for example, to embed it in a static JavaScript bundle). You can use inter-service URL templates as build arguments:

containers:
  - name: frontend
    dockerfile: frontend/Dockerfile
    context: frontend/
    args:
      PUBLIC_API_URL: "${service:backend}"

  - name: backend
    dockerfile: backend/Dockerfile
    context: backend/

deploy:
  - name: frontend
    container: frontend
    port: 3000

  - name: backend
    container: backend
    port: 8080

In your frontend Dockerfile:

FROM node:20-slim AS builder
WORKDIR /app
ARG PUBLIC_API_URL
ENV VITE_API_URL=$PUBLIC_API_URL
COPY . .
RUN npm ci && npm run build

FROM nginx:alpine
COPY --from=builder /app/dist/ /usr/share/nginx/html/

The ${service:backend} template resolves to the backend's external HTTPS URL (e.g., https://myapp-backend.dev.yourdomain.io) at build time.

nginx and dropped Linux capabilities

A stock nginx image starts as root, chowns its cache directories, then drops to an unprivileged user. Containers run with all Linux capabilities dropped, so that sequence only works when a limited set is restored — on container port 80/443, or on any declared port when the image runs as root and declares an EXPOSE.

If your container exits with nginx: [emerg] chown(...) failed (1: Operation not permitted), that is the cause. The most robust fix is to make the image not need the capability at all:

FROM nginx:alpine
COPY --from=builder /app/dist/ /usr/share/nginx/html/
# Pre-create and chown the cache dirs at BUILD time, then drop privileges,
# so nothing needs CHOWN at runtime.
RUN mkdir -p /var/cache/nginx/client_temp \
    && chown -R nginx:nginx /var/cache/nginx /var/run
USER nginx

See Services → Linux capabilities.

Troubleshooting Multi-Container Builds

Build fails on the second container

Read the logs to see which container failed and why:

mecca build logs <build-id> -e dev

The logs show which container is being built (Building container 1/2: frontend, Building container 2/2: backend), so you can find the failing step.

"context" files not found

If your Dockerfile tries to COPY a file and gets a "not found" error, the file is probably outside the build context.

  • If the file is in a sibling directory, change context to . (repo root)
  • Make sure paths in COPY are relative to the context, not the Dockerfile location

Container name conflicts

Container names must be unique within your appmecca.yaml. If two containers have the same name, the build will fail.

Next Steps