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: staticfrontend — 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}:
AppMecca resolves this at build time to the backend's public HTTPS URL:
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/:
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¶
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:
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: staticservice - Each container gets its own
dockerfileandcontextpointing to a subdirectory - Use
${service:name}templates in theenvironmentsection to connect services -- AppMecca resolves them to the correct HTTPS URL per environment - One
mecca build triggerbuilds 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?¶
- Django Celery Tasks -- Multiple services from one image (the simpler pattern)
- Inter-Service Communication -- Full template syntax reference
- Multi-Container Builds -- How the build process works for multi-container apps