Skip to content

Flask + Vue Fullstack

A full-stack application with a Flask API backend and a Vue.js frontend, built as separate containers from a single repository. This is the most complex deployment pattern -- two Docker images, two services, two URLs, and inter-service communication via AppMecca's template syntax.

Source: git@gitlab.com:appmecca-sample-apps/flask-vue-fullstack.git

What This App Demonstrates

  • One container plus one static service built from the same repository
  • Multi-service URL pattern -- each service gets its own subdomain
  • Inter-service communication via ${service:backend} template resolution
  • Database access for the backend only
  • A type: static frontend — built and uploaded to object storage, served from the AppMecca edge cache, with no container, Dockerfile or nginx

appmecca.yaml

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

deploy:
  - name: backend
    container: backend
    port: 5000
    path: /api
    health_check:
      path: /api/health

  - name: frontend
    primary: true
    type: static
    static_site:
      build_command: "cd frontend && npm install --no-audit --no-fund && npm run build"
      output_dir: frontend/dist
      spa_fallback: true
      index_document: index.html
    environment:
      VITE_API_URL: "${service:backend}"

database:
  enabled: true

There are three things happening here that differ from simpler examples:

1. One container, one static service, separate build contexts

The container has its own dockerfile and context, and the static service builds from its own subdirectory:

flask-vue-fullstack/
├── appmecca.yaml
├── backend/
│   ├── Dockerfile
│   ├── app.py
│   └── requirements.txt
└── frontend/
    ├── package.json
    └── src/

When you trigger a build, AppMecca builds one Docker image (the backend) and pushes it to your private image registry. The frontend never becomes an image at all — AppMecca runs its build_command and uploads output_dir to object storage:

Service Build artefact Build context
backend Docker image in the private image registry backend/
frontend Static bundle in object storage frontend/ (via static_site.output_dir)

2. Multi-service URLs

Each service gets its own subdomain:

Service URL
backend https://flask-vue-fullstack-backend.dev.appmecca.net
frontend https://flask-vue-fullstack-frontend.dev.appmecca.net

This is the {app}-{service}.{env}.{domain} pattern. The service name is always part of the hostname — there is no bare {app}.{env}.{domain} URL, even for an app with a single service.

3. Inter-service URL template

The frontend's VITE_API_URL environment variable uses ${service:backend}:

environment:
  VITE_API_URL: "${service:backend}"

AppMecca resolves this at build time to the backend's public HTTPS URL:

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

This resolution is environment-aware -- in staging, it would resolve to https://flask-vue-fullstack-backend.staging.appmecca.net. You never hardcode URLs.

Build-time vs. deploy-time resolution

External templates (${service:name}) are resolved at both build time and deploy time. At build time, the resolved URL is passed as a Docker --build-arg and exported to the static build, so your frontend can bake it into the JavaScript bundle. At deploy time, it is injected as a container environment variable. A type: static service has no container at runtime, so only the build-time resolution reaches it — bake the value into the bundle. See Inter-Service Communication for details.

Backend

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 . .

RUN chmod +x entrypoint.sh

EXPOSE 5000

ENTRYPOINT ["./entrypoint.sh"]

The backend follows the standard database app pattern: an entrypoint script waits for the database, runs migrations, then starts gunicorn. Only the backend has database: enabled: true access -- the frontend serves static files and does not need database credentials.

Backend health check

The health check is at /api/health (not /health) because the backend prefixes all routes with /api/:

@app.route("/api/health")
def health():
    return jsonify({"status": "healthy"})

Frontend

The Vue app is a type: static service: there is no Dockerfile and no nginx config. AppMecca runs the build command, uploads output_dir to object storage, and serves it through the AppMecca edge cache.

deploy:
  # ... backend service omitted for brevity
  - name: frontend
    primary: true
    type: static
    static_site:
      build_command: "cd frontend && npm install --no-audit --no-fund && npm run build"
      output_dir: frontend/dist
      spa_fallback: true
      index_document: index.html
    environment:
      VITE_API_URL: "${service:backend}"

spa_fallback: true makes the edge cache serve index.html (status 200) for any path that is not a real file, so Vue Router handles deep links like /items/42. That is what the nginx try_files $uri $uri/ /index.html; directive used to do — the edge does it now, with no container to run or config file to maintain. When spa_fallback is true, error_document is ignored, so there is no point setting it.

A type: static service must not carry container fields

port, command, health_check, replicas above 1, and drives are all rejected at validation time for a type: static service — they are meaningless without a container. No Dockerfile, no containers: entry, no nginx.conf, no /health endpoint and no host RAM. If you are writing a web-server config for a frontend that only serves files, something has gone wrong — see Service types.

How the Services Connect

User (browser)
  |
  | HTTPS
  v
AppMecca edge cache ──> static bundle  --> flask-vue-fullstack-frontend.dev.appmecca.net
  |
  | HTTPS (API calls from browser JS)
  v
AppMecca HTTPS router ──> backend (Flask, port 5000)
                          --> flask-vue-fullstack-backend.dev.appmecca.net

The user's browser loads the Vue.js SPA from the frontend URL — served straight from the edge cache, with no container involved. The JavaScript in the browser then makes API calls to the backend URL (baked into the bundle at build time via VITE_API_URL). The backend request goes through the AppMecca HTTPS router. Both are served over HTTPS with a certificate AppMecca issues and renews for you.

Deploy It

# Register (runs a pre-flight check on the repo; does NOT start a build)
mecca register git@gitlab.com:appmecca-sample-apps/flask-vue-fullstack.git -e dev

# Build the backend image + the frontend bundle, then auto-deploy
mecca build trigger -e dev -g git@gitlab.com:appmecca-sample-apps/flask-vue-fullstack.git

# Deploy again later without rebuilding
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
Health check passed: /api/health -> 200 OK
Static bundle published: flask-vue-fullstack-frontend
Deployment complete.

Services:
  backend:  https://flask-vue-fullstack-backend.dev.appmecca.net
  frontend: https://flask-vue-fullstack-frontend.dev.appmecca.net

Verify both services

# Check service status
mecca app show -e dev flask-vue-fullstack

# Backend health
curl https://flask-vue-fullstack-backend.dev.appmecca.net/api/health

# Frontend — a static service has no /health endpoint; fetch the page itself
curl -I https://flask-vue-fullstack-frontend.dev.appmecca.net/

View backend logs

mecca logs flask-vue-fullstack --type container --service backend -e dev

There are no container logs for frontend — a type: static service never runs a container. To debug a frontend problem, look at the build output instead:

mecca logs flask-vue-fullstack --type build -e dev

When to Split Into Multiple Containers

Use multiple containers[] entries when your services:

  • Are built with different languages or runtimes (e.g. a Python backend plus a Go worker)
  • Have different Dockerfiles
  • Should scale independently in the future

If your services use the same language, same Dockerfile, and just run different commands, use the single-image, multiple-command pattern instead. And if a "service" only serves built files, do not give it a container at all — make it type: static, as the frontend here does.

Key Takeaways

  • A single repo can mix a containerised service and a type: static service
  • Each container gets its own dockerfile and context pointing to a subdirectory
  • Use ${service:name} templates in the environment section to connect services -- AppMecca resolves them to the correct HTTPS URL per environment
  • One mecca build trigger builds every container and every static bundle
  • Every service gets its own URL: {app}-{service}.{env}.{domain} — the service name is always present
  • Only the backend needs database access -- the frontend is just files at the edge

What's Next?