Skip to content

AppMecca AI Agent Guide

A single-file, self-contained reference for AI coding agents configuring an application to run on AppMecca.

This guide is written for any AI assistant — it assumes nothing about which model or tool is reading it. Copy it into your project (e.g. as APPMECCA.md, or append it to whatever instruction file your assistant reads: CLAUDE.md, AGENTS.md, .cursorrules, .github/copilot-instructions.md, GEMINI.md, or equivalent) so your agent can configure appmecca.yaml and the Dockerfile correctly without reading the platform's source. If you are an AI agent reading this: §21 at the end is your step-by-step operating procedure — start there, and use §1–§20 as the reference it points into.

Everything below is verified against the platform's live validation code. Where the platform accepts something silently but behaves surprisingly, that is called out as a trap — those are the cases that waste the most time.


0. What AppMecca is, in one page

AppMecca takes a Git repository and runs it. You give it:

  1. A appmecca.yaml in the repository root — declares what to build and how to run it.
  2. A Dockerfile (or several) — unless your app is a pure static site, which needs none.

AppMecca then builds your images, provisions whatever backing services you declared (database, cache, storage), deploys your containers across a pool of hosts, puts HTTPS in front of them, and gives you a URL.

You never provision infrastructure, write networking config, or manage credentials. Backing services are declared in one line each and their connection details arrive as environment variables inside your container.

The CLI

Everything you do runs through the mecca command: register, build, deploy, logs, variables, storage, database, domains. Every example in this guide uses it.

Glossary — every term and every feature

Structure

Term YAML key Meaning
App — One Git repository + branch, registered with AppMecca. The unit you deploy.
Service deploy[] One runnable unit inside an app — a web server, a worker, a static frontend. An app has one or more.
Container containers[] One built image. Several services can run from the same image with different commands.
Container service deploy[] with container: A service that runs as a long-lived process in a container.
Static service deploy[].type: static A service with no container — built once and served from the edge cache.
Worker deploy[] with no port An internal service with no URL and no health check (queue consumer, scheduler).
Replica deploy[].replicas One additional copy of a service, placed on a different host. 1–3.
Branch — Each Git branch deploys independently: its own URL, database, cache namespace and storage.
Group — Your team/tenant. Your app's URL ends with your group's slug.
Slug — Your app's short name, derived from its display name at registration. Part of its URL. A small set of names is reserved — see below.

Data and state

Term YAML key Meaning
Database database.enabled A managed PostgreSQL database, provisioned on first deploy. Credentials arrive as DATABASE_URL. One per app per branch. §5
Application cache (a.k.a. internal cache) cache.enabled A managed Redis key-value store your code talks to directly, for sessions, queues, computed values. Arrives as REDIS_URL. Default off. §6
Drive storage.drives[] A shared POSIX filesystem volume mounted into your container at a path you choose. Ordinary file I/O. Capped at 100 GB. §7.1
Bucket storage.buckets[] Private object storage for your app, reached over the S3 API with an injected credential. Effectively unlimited. §7.2
Backup bundle — A point-in-time snapshot of your database and drives (not buckets), taken by mecca app backup. §16

Delivery and routing

Term YAML key Meaning
Edge cache (a.k.a. external cache, web cache, content cache, CDN) deploy[].cache AppMecca's HTTP response cache, sitting in front of a service at the network edge. Caches whole responses by URL path, close to your users. On by default whenever the block is present. §12
Front door deploy[].primary The service whose URL represents the app on the dashboard. Auto-detected unless you set it. §2.6
Visibility deploy[].visibility public (anyone with the URL) or private (only signed-in members of your group). App-wide. §11
Custom domain — (CLI only) Your own hostname in front of a service. Managed with mecca domain add — not in YAML. §13
Health check deploy[].health_check The HTTP probe that decides whether your service is alive and whether a deploy succeeded. §2.4

cache means two completely different things

This is the single most confusing thing in appmecca.yaml, because both use the key cache:.

Application cache (internal) Edge cache (external / web)
Where Top level: cache: Inside a service: deploy[].cache:
What it is Redis key-value store HTTP response cache at the edge
Who talks to it Your code, via REDIS_URL The network, transparently
What it stores Whatever you put in it Copies of your HTTP responses
Default Off — you must set enabled: true On as soon as the block exists

They are independent. You can use either, both, or neither.

Operations

Term Meaning
Pre-flight The repository check that runs at registration: can AppMecca reach the repo, is appmecca.yaml present and valid. An app cannot build or deploy until it passes. §15
Build Turning your repository into images (and static bundles).
Deploy Starting the new version and switching traffic to it.
Action / queue Every build and deploy is a queued action with an id you can watch with mecca queue status. §15
Environment variable Configuration delivered into your container — from YAML, from mecca var set, or injected by the platform. §8
Cron job A scheduled HTTP request AppMecca makes to your own endpoint. Not a command in a container. §10
Built-in email (a.k.a. the email relay) AppMecca's zero-configuration email service, enabled with email.enabled: true. For notifying yourself — the developer, site owner or team — of things happening in the app: contact-form submissions, threshold alerts, error digests, system events. It is not a bulk/marketing/customer-mailing service. No mail server, no credentials, no third-party account: the app POSTs one JSON request. §9

Reserved slugs — you are never refused, you are suffixed

An app's slug becomes a DNS label and reaches the path positions the platform's own API answers on, so two families of name are reserved:

  • the platform's hostnames — api, dashboard, docs, admin-docs, dev-docs, api-docs, tutorial, monitor, gateway, screenshot, mail
  • the literal segments of its API routes — apex-check, checks, claim, config-schema, group, history, hosts, id, incidents, ns, pool, prioritize, providers, queue, refresh, report, snooze, stable, summary, suppressions, uptime, version, versions

⚠ Do not hardcode that list. It is DERIVED from the live route tree and the platform's hostname map, and a guard test fails the build when the two drift apart — so it changes when the platform changes. Ask for the name you want.

Registration never fails because of this. A reserved name — or one another app in your group already holds — is registered on the next free numbered variant, and the substitution is reported back to you:

'claim' is reserved by the platform — registered as 'claim-1' instead.

Suffixes run -1, -2, -3. Apps registered before the rule existed keep their slug; nothing is renamed retroactively.

1. Decision tree — pick your app shape first

Everything else follows from this. Read it before writing any YAML.

Does your app need a server process at runtime?
│
├─ NO — it's HTML/CSS/JS that a browser can run directly
│        (React, Vue, Svelte, Angular SPA; Hugo, MkDocs, Jekyll, Astro static export)
│  │
│  └─► STATIC APP.  type: static + static_site:
│      No Dockerfile. No containers: block. No port. No health check.
│      Served from AppMecca's edge cache. §4.2
│
├─ YES, and it's the only thing
│  │
│  └─► CONTAINER APP.  containers: + deploy: with a port
│      One Dockerfile, one service. §4.1
│
└─ YES for the backend, NO for the frontend (an API + a separate SPA)
   │
   └─► MIXED APP.  One container service + one type: static service
       The most common full-stack shape. §4.3

Do not put a React build behind an nginx container just to serve files. Use type: static — it is cheaper, faster, globally cached, and needs no Dockerfile.


2. appmecca.yaml — complete reference

The file must be named exactly appmecca.yaml and live in the repository root. (A different path is possible via mecca register --config-path sub/appmecca.yaml.)

2.0 THE #1 TRAP: unknown keys are silently ignored

The config parser drops any key it does not recognise, with no error and no warning.

deploy:
  - name: web
    container: web
    replicaz: 3          # ← typo. Silently ignored. You get replicas: 1.
    helth_check:         # ← typo. Silently ignored. No health check at all.
      path: /health

There is no spell-check and no strict mode. Every key you write must match this reference exactly. When something you configured "does nothing", suspect a typo first.

2.1 Top-level keys

Key Required Purpose Section
app_name No Display-name override. Defaults to the repository name. —
containers Conditional Images to build. Omit only when every service is type: static. §2.2
deploy Yes The services that make up your app. At least one. §2.3
database No Managed PostgreSQL database. §5
cache No Managed Redis key-value cache. §6
storage No Drives and buckets. §7
static_site Conditional Build settings for static services. Required if any service is type: static (unless every such service carries its own). §4.2
static No Collect-static asset upload alongside a container service. Rarely needed. §2.7
email No Built-in email — the app notifies its owner (forms, alerts). §9
cron No Scheduled HTTP requests to your own endpoints. §10

cache and static each name two different things

  • Top-level cache: = managed Redis. Default off.
  • Per-service deploy[].cache: = HTTP edge caching. Default on whenever the block is present — writing cache: {} under a service enables edge caching.
  • Top-level static: = collect-static upload beside a container.
  • Top-level/per-service static_site: = the static deploy target instead of a container.

Confusing either pair is a common and confusing failure. See §6, §2.7 and §4.2.

2.2 containers[] — what to build

containers:
  - name: web              # required — referenced by deploy[].container
    dockerfile: Dockerfile # required (see trap below)
    context: .             # default "."
    args:                  # optional build args
      NODE_ENV: production
    target: runtime        # optional multi-stage build target
Field Required Default Notes
name Yes — Lowercase, hyphens. Must be unique. Referenced by deploy[].container.
dockerfile Yes Dockerfile The schema defaults it, but pre-flight validation rejects the key being absent. Always write it.
context No . Build context, relative to repo root.
args No {} Passed as --build-arg.
build_args_from_vars No [] Stored variables (mecca var set) to pass as build args as well as secrets. Only for values that are not secret.
target No — Multi-stage build target stage.

2.3 deploy[] — how to run it

deploy:
  - name: web              # required — appears in the URL
    container: web         # required for container services; must match containers[].name
    port: 8000             # the port your process listens on INSIDE the container
    replicas: 1            # 1-3
    command: "gunicorn app:app --bind 0.0.0.0:8000"   # optional; overrides Dockerfile CMD
    health_check:
      path: /health
    environment:
      LOG_LEVEL: info
    resources:
      memory: 256m
Field Type Default Bounds Notes
name string — required, unique Appears in your URL.
container string — required unless static Must match a containers[].name.
port int none 1–65535 Omit ⇒ internal-only service (worker) with no URL.
replicas int 1 1–3 Each replica lands on a different host.
command string Dockerfile CMD — Run via sh -c.
health_check block none — Omit for workers. See §2.4.
environment map {} — Non-secret config. See §8.
resources.memory string see §3 — Must carry a unit suffix.
resources.cpu float — — Accepted but does nothing. See §3 trap.
drain_delay_seconds int 30 0–300 How long in-flight requests survive a swap. Raise for websockets/streaming.
type string none — static, spa. See §4.
spa_root string /app/build — Only for containerised SPA services.
singleton bool false — Run on one host only (e.g. a scheduler).
visibility string public public|private See §11.
primary bool auto — Front-door flag. See §2.6.
drives list [] — Drive mounts. See §7.1.
cache block none — Edge caching. See §12.
static_site block none — Per-service static build. See §4.2.
domain / domains — — — Ignored. Use mecca domain add. See §13.

2.4 health_check

health_check:
  path: /health          # default /health
  interval: 30           # 5-300  seconds between checks
  timeout: 5             # 2-120  seconds before a check times out
  healthy_threshold: 2   # 2-10   consecutive passes to be marked healthy
  unhealthy_threshold: 3 # 2-10   consecutive failures to be marked unhealthy
  startup_timeout: 120   # 30-600 seconds to pass its FIRST check during a deploy
  • healthy_threshold and unhealthy_threshold both have a minimum of 2. Writing 1 is a hard validation error. This surprises almost everyone.
  • startup_timeout is the one that matters on deploy. A slow-booting app (JVM, large migrations) needs it raised, or the deploy fails while the app is still starting.
  • Omitting health_check entirely is correct for workers — a service with no port and no health check is never probed.
  • Your endpoint must return HTTP 200. It should be cheap and must not require authentication.

2.5 A service with no port is a worker

deploy:
  - name: worker
    container: app
    command: "celery -A myapp worker -l info"
    # no port, no health_check → internal only, no URL, never probed

2.6 primary — which service is the front door

Controls which URL the dashboard shows and screenshots. Leave it unset and AppMecca auto-detects: every static/spa service, else the first port-bearing service whose name is not worker*, beat*, cron-*, cron_*, _internal*.

Two traps

  • Setting primary: false on every service yields zero front doors — no URL on the dashboard. Leave it unset instead.
  • A legitimate public service named e.g. workerboard or beatmaker is silently excluded by the auto-detect prefix match. Set primary: true explicitly on it.

primary: true requires the service to be reachable (a port, or static). Otherwise it is a validation error.

2.7 static: — REMOVED, and now refused

The static: block no longer exists at any level. FEATURES #347 deleted the top-level one because it did nothing: collect_command and bucket_path were read by no code at all, so an app that declared it collected no assets and got no error either.

⚠ BUGS #854 closed the per-service spelling (deploy[].static:), which #347 left accepted-and-silently-dropped. That gap was not hypothetical: two sample apps declared it and one's README documented it as working, so the platform was teaching a falsehood about itself in the repos newcomers copy from. Both spellings are refused now.

It is now a hard refusal rather than a silent no-op, so a config carrying it will not register:

'static' at the config root was removed — it had no effect. Run collectstatic
from your Dockerfile or build command instead. `static_site:` is a different
block and is unaffected. FEATURES #347.

Collect your assets in the image instead — a RUN python manage.py collectstatic --noinput line in the Dockerfile, or in the service's build_command. That runs at build time, is visible in the build log, and fails loudly when it fails.

static_site: is a different key and is unaffected

deploy[].static_site: configures a service published to S3 + CloudFront. It is unrelated to the removed static: block and remains fully supported.


3. Resources and memory

Defaults when you write nothing

AppMecca inspects your image at deploy time and picks a default:

Detected runtime Default memory
JVM (Java, Kotlin, Spring) 512m
Python (gunicorn/uvicorn/celery) 256m
Node.js 128m
Static file server (nginx) 64m
Anything else / undetectable 128m

An explicit resources.memory always wins.

Traps

memory without a unit suffix is parsed as BYTES

memory: 512 means 512 bytes → a 0 MB limit. Always write 512m or 1g. Accepted suffixes are exactly k, m, g. 512mb is not parsed — it is treated as unparseable and silently skips the safety checks.

resources.cpu does nothing

It is accepted by the schema and validated as > 0, but no code path applies it. Your container is not CPU-limited. Do not rely on it.

JVM apps

If your service declares a command: containing java, .jar, mvn or gradle and memory below 512m, validation emits a warning (not an error) and the deploy proceeds — then the JVM is killed during startup with exit code 137. If your JVM starts from the Dockerfile ENTRYPOINT with no command: in YAML, you get no warning at all. Set memory: 512m explicitly for any JVM service.

Containerised SPA services have a 32 MB floor

type: spa with resources.memory below 32m is a hard validation error — below that the web server's worker process is killed silently while the container appears healthy.

Replicas

replicas is 1–3. Each replica must land on a different host — this is a hard constraint, not a preference. replicas: 3 on a three-host pool requires all three to have capacity simultaneously, or the whole deploy fails. Start at 1.


4. The three app shapes, in full

4.1 Container app

The default. One or more Dockerfiles; every service runs as a container.

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

deploy:
  - name: web
    container: web
    port: 8000
    health_check:
      path: /health
    resources:
      memory: 256m

4.2 Static app — no container at all

For anything a browser can run directly. AppMecca runs your build command, uploads the output, and serves it from its edge cache. No Dockerfile, no containers: block.

static_site:
  build_command: "npm ci && npm run build"
  output_dir: "dist"
  spa_fallback: true          # true for SPAs, false for true static sites
  index_document: "index.html"
  error_document: "index.html"

deploy:
  - name: web
    type: static
    primary: true
Field Default Notes
build_command npm run build Runs from the repo root. Must exit 0 and populate output_dir.
output_dir dist Relative to repo root. Must not be absolute or contain ...
spa_fallback false true for client-routed SPAs (React Router, Vue Router) so deep links work. false for Hugo/MkDocs/Jekyll where 404s should stay 404s.
index_document index.html Served at the site root.
error_document 404.html Served on 404 when spa_fallback is false; ignored when it is true.
flat_html_urls false true when the build emits FLAT pages (about.html) rather than about/index.html — SvelteKit adapter-static, Astro, Jekyll with pretty_urls: false. Writes the directory key too, so /about resolves. Mutually exclusive with spa_fallback (refused at validation).

Common output_dir values: dist (Vite, SvelteKit static), build (Create React App), out (Next.js export), public (Hugo, Jekyll), site (MkDocs).

type: static forbids container-shaped fields

A type: static service must not declare port, replicas other than 1, drives, command, or health_check. Any of them is a hard validation error. Converting a container service to static means deleting those keys, not just flipping type.

SvelteKit and other adapters that emit 200.html

If your build emits 200.html instead of index.html, set index_document: 200.html. AppMecca probes for the declared entry document and will fail the deploy if neither is found.

4.3 Mixed app — container backend + static frontend

The most common full-stack shape.

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

deploy:
  # API — runs as a container
  - name: backend
    container: backend
    port: 8000
    path: /api
    health_check:
      path: /api/health
    resources:
      memory: 256m

  # SPA — built and served from the edge, no container
  - name: frontend
    primary: true
    type: static
    static_site:
      build_command: "cd frontend && npm ci && npm run build"
      output_dir: "frontend/dist"
      spa_fallback: true
      index_document: "index.html"
      error_document: "index.html"
    environment:
      VITE_API_URL: "${service:backend}"

database:
  enabled: true

The frontend's environment block is exported during its build, so ${service:backend} resolves to the backend's public URL before the bundler runs and gets baked into the JavaScript. This is how a static frontend learns its API address. See §8.4.

A mixed deploy produces two queue actions

Container and static halves are tracked separately but share one deployment. mecca queue list shows both; watch both before declaring success.

4.4 type: spa — avoid it

type: spa is a legacy shape kept for containerised SPAs with a custom web-server config. Writing type: spa with neither command: nor a static_site: block is a hard validation error — the platform cannot tell whether you want a static deploy or a container.

Unless you specifically need a custom web-server configuration, use type: static (§4.2) or drop type: altogether.

type: is not validated against a list of allowed values

type: statik, type: Static, type: static-site are all accepted and silently treated as ordinary container services. ssr and api are accepted and do nothing. Only the exact lowercase strings static and spa have any effect.


5. Database

One line provisions a managed PostgreSQL database on first deploy.

database:
  enabled: true
  max_connections: 5     # 1-50, default 5

Your container receives:

Variable Contents
DATABASE_URL Full connection URL, including ?sslmode=require
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD The same details, separately

Rules

  • Every branch gets its own database. main and dev never share data.
  • Never hardcode the database or user name. They are opaque hash-derived identifiers (e.g. am_a1b2c3d4e5f6_db), not myapp_db. Always read DATABASE_URL.
  • max_connections is a declaration the platform enforces at deploy admission. Set it to match your framework's real pool size (Django ≈ 1–5, SQLAlchemy pool_size, Hikari maximumPoolSize). Over-declaring starves other apps; under-declaring exhausts connections at runtime.
  • Migrations are your job. Run them from your container's entrypoint. There is no migration hook.
  • Using PostgreSQL row-level security? Set runtime_role: true. Never FORCE ROW LEVEL SECURITY — the platform's pre-deploy backup runs pg_dump as the table owner and fails under FORCE, which fails every deploy after the first. With runtime_role: true, DATABASE_URL/DB_USER/DB_PASSWORD are a non-owner role that RLS binds, and DATABASE_OWNER_URL/DB_OWNER_USER/DB_OWNER_PASSWORD are the owner — run migrations with the owner URL. See Database → A Separate Runtime Role.
# Dockerfile — run migrations before starting the server
CMD ["sh", "-c", "python manage.py migrate --noinput && gunicorn app.wsgi --bind 0.0.0.0:8000"]

Node.js SELF_SIGNED_CERT_IN_CHAIN

Recent pg versions treat sslmode=require in a URL as full certificate verification. Strip the query parameter and configure TLS in code:

new Pool({ connectionString: process.env.DATABASE_URL.split('?')[0],
           ssl: { rejectUnauthorized: false } })

Spring Boot connection exhaustion

Hikari defaults to 10 connections per replica. Set spring.datasource.hikari.maximum-pool-size=3 and declare max_connections: 3.


6. Cache (managed Redis)

redis:
  enabled: true

Your container receives REDIS_URL and CELERY_BROKER_URL (the same string), plus CACHE_PREFIX.

Rules

  • Always prefix your keys with CACHE_PREFIX. The cache is shared infrastructure; the prefix is your namespace. It is injected as {app}:{branch}:.
  • REDIS_URL and CELERY_BROKER_URL are identical. If Celery needs a distinct result backend, point result_backend at your database rather than Redis.
  • CACHE_PREFIX is injected even when cache.enabled is false — do not use its presence to detect whether caching is on.

This is the internal key-value cache

For HTTP/CDN edge caching of your responses, see §12 — a completely different feature configured under deploy[].cache.


7. Storage — drives and buckets

Two kinds. Picking the wrong one is the most expensive configuration mistake on the platform.

The decision, in one table

Drive Bucket
Looks like A directory in your container An S3-API endpoint
Access Ordinary file I/O (open(), fs.readFile) An S3 client library
Size ceiling 100 GB, hard Effectively unlimited
Free allowance 5 GB per app No allowance needed
Above the allowance Needs an operator grant Self-service
Relative cost ~13× more expensive Cheap
Shared across replicas Yes Yes
Shared across branches No No
In mecca app backup bundles Yes No

Choose a drive when

  • A framework demands a real filesystem path it will not let you override.
  • Small, shared working state between replicas.
  • You need it captured in platform backups.

Choose a bucket when

  • User uploads, media libraries, generated images, exports, documents.
  • Anything that can grow.
  • Anything over a few gigabytes.

The two most costly storage mistakes

  1. Bulk media on a drive. You pay roughly 13× the bucket rate, hit a hard 100 GB wall, and need an operator to raise it. Put media in a bucket.
  2. Logs on a drive. Write to stdout/stderr instead — AppMecca collects container output automatically. A log directory on a drive is unbounded growth at the most expensive rate available.

7.1 Drives

Declare at app level, mount per service:

storage:
  drives:
    - name: media          # kebab-case, starts with a letter
      size_gb: 5           # 1-100, default 5

deploy:
  - name: web
    container: web
    port: 8000
    drives:
      - name: media        # must match a declared drive
        path: /data/media  # absolute path inside the container
        readonly: false
  • path must be absolute and unique per service. /, /etc, /usr, /var, /bin, /sbin, /boot and /dev are rejected.
  • One drive can be mounted by several services, at different paths, with different readonly settings.
  • Every branch gets its own drive. Files uploaded on main are invisible on dev.

Entitlement: an app's declared drive total must fit inside the free allowance (5 GB) unless the group holds an operator grant. The ceiling is 100 GB for everyone. The group-wide total counts every app and every branch — a second branch of a 5 GB app adds another 5 GB to the group's total. Exceeding it is refused at deploy time with HTTP 409, not at commit time.

size_gb is enforced on uploads, not on your container's writes

The platform refuses an over-cap mecca drive upload, but your container writing directly to the mount is not blocked at the filesystem level. Do not treat size_gb as a hard quota against your own code.

7.2 Buckets

storage:
  buckets:
    - name: uploads        # kebab-case, starts with a letter
    - name: exports

On deploy, AppMecca provisions each bucket and injects one scoped credential:

Variable Contents
APPMECCA_BUCKET_UPLOADS_NAME Physical name of the uploads bucket
APPMECCA_BUCKET_EXPORTS_NAME Physical name of the exports bucket
APPMECCA_BUCKET_REGION Region the buckets live in
APPMECCA_BUCKET_ACCESS_KEY_ID / APPMECCA_BUCKET_SECRET_ACCESS_KEY The credential
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION Convenience copies, so standard S3 client libraries work with no configuration

The logical name is upper-cased with hyphens becoming underscores: user-uploads → APPMECCA_BUCKET_USER_UPLOADS_NAME.

import boto3, os
s3 = boto3.client("s3")                       # credentials already in the environment
s3.upload_file("photo.jpg", os.environ["APPMECCA_BUCKET_UPLOADS_NAME"], "photo.jpg")
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({});                   // credentials already in the environment
await s3.send(new PutObjectCommand({
  Bucket: process.env.APPMECCA_BUCKET_UPLOADS_NAME,
  Key: "photo.jpg", Body: data,
}));

Never hardcode a bucket name

The physical name is generated and differs per branch. Always read APPMECCA_BUCKET_{NAME}_NAME.

Do not set AWS_ACCESS_KEY_ID yourself

The three convenience variables are only set if you have not set them. If you define any of them in appmecca.yaml or via mecca var set, your value wins and every bucket call fails with an authentication error that looks nothing like a config mistake. Read the APPMECCA_BUCKET_* variables explicitly if you need another credential in the same container.

Buckets are not in backup bundles

mecca app backup captures the database and drives. Bucket objects are not included and mecca app restore will not bring them back.

7.3 Getting media in

Into a drive — from your machine, with the CLI:

mecca drive ls      myapp media                    # list a directory
mecca drive tree    myapp media                    # walk the whole drive
mecca drive du      myapp media                    # usage against size_gb
mecca drive upload  myapp media ./photo.jpg        # ONE file — a directory is refused
mecca drive download myapp media path/to/file.jpg ./local.jpg
mecca drive mkdir   myapp media new/folder
mecca drive mv      myapp media old/path new/path
mecca drive rm      myapp media path/to/file       # destructive — confirms
mecca drive archive myapp media -o ./media.tar.gz  # the whole drive as one file

Directory upload is recursive and resumable: files already present with a matching size are skipped, so re-running after an interruption is cheap. --overwrite re-sends everything. Symlinks are never followed; .DS_Store, Thumbs.db, .git, .svn, .hg and __pycache__ are skipped. Add -b <branch> to target a non-production branch.

Into a bucket — the same verbs, where S3 semantics allow them:

mecca bucket list        myapp            # which buckets exist + their physical names
mecca bucket ls          myapp media      # one prefix level (-r for everything below)
mecca bucket stat        myapp media k.jpg
mecca bucket upload      myapp media ./photo.jpg photos/photo.jpg
mecca bucket download    myapp media photos/photo.jpg -o ./p.jpg
mecca bucket mkdir       myapp media photos/2027
mecca bucket rm          myapp media photos/old.jpg    # destructive — type the bucket name
mecca bucket purge       myapp media                   # empty it; bucket survives
mecca bucket archive     myapp media -o ./media.tar.gz
mecca bucket reveal      myapp            # show the credential once
mecca bucket rotate-key  myapp            # issue a new credential and redeploy

reveal is show-once — a second call is refused. Use rotate-key if you need it again.

⚠ There is no mecca bucket mv/cp. S3 has no server-side rename, so moving a "directory" is an unbounded copy-then-delete loop with no atomicity. Use aws s3 mv with the app's own revealed credential if you need it. Buckets also carry no quota — unlike a drive's size_gb, nothing bounds their growth.

⚠ Bucket uploads go straight to S3 — no transit hop and no commit step, so an object is live the moment S3 accepts it. (A drive upload stages through transit storage because a filesystem cannot be presigned.)

Serving media back to users:

  • Drive — your application reads the file from the mount path and serves it.
  • Bucket — your application generates a pre-signed URL (recommended: no traffic through your container) or streams the object itself.

8. Environment variables

8.1 Where to put what

Put it in When Visibility
appmecca.yaml → deploy[].environment Non-secret config that belongs with the code In Git, and printed in full in build logs
mecca var set Secrets, per-environment values, anything you would not commit Stored server-side; reaches the build as a secret, not stored in the image; only names appear in build logs

Never put a credential in appmecca.yaml

Values in deploy[].environment are committed to Git and echoed verbatim into the build log. Values set with mecca var set are never printed; only their names are.

8.2 The CLI

mecca var set   myapp SECRET_KEY=abc123                # ALL branches (the default)
mecca var set   myapp SECRET_KEY=abc123 -b dev         # one branch
mecca var set   myapp DB_HOST=db DB_PORT=5432 -b dev,qa   # several branches
mecca var list  myapp                                  # per-branch listing
mecca var list  myapp -b dev --dotenv > .env           # export
mecca var load  myapp .env -b dev                      # bulk import from a file
mecca var unset myapp SECRET_KEY -b dev
  • mecca var set applies to every branch unless you narrow it with -b. The selector accepts a name (-b dev), a comma list (-b dev,qa), or a glob (-b 'feature/*').
  • -b must come after the subcommand — unlike -e, it is not accepted before it.
  • Setting is a merge, not a replace. Existing keys you do not mention are kept. To remove one, use mecca var unset (one key per call).
  • mecca var list prints values in cleartext — treat its output as sensitive.
  • --dotenv requires the selector to resolve to exactly one branch.

Changes need a redeploy

Variables are read only when a container starts. After changing one:

mecca app redeploy myapp

8.3 What AppMecca injects for you

Always present:

Variable Contents
APPMECCA_ENV Environment name
APPMECCA_APP Canonical app name (group-prefixed)
APPMECCA_SERVICE This service's name
APPMECCA_BRANCH Branch name, sanitised (feature/Dark-Theme → feature-dark-theme)
APPMECCA_REGION Deployment region
CACHE_PREFIX {app}:{branch}: — your cache namespace

Conditionally present: database variables (§5), cache variables (§6), bucket variables (§7.2), mailer variables (§9).

8.4 Precedence — who wins

Built in this order, later writes overwriting earlier ones:

  1. appmecca.yaml → deploy[].environment
  2. ${...} template resolution
  3. Platform-injected values (DATABASE_URL, REDIS_URL, APPMECCA_*, bucket vars)
  4. mecca var set values
  5. Mailer variables (§9)

Consequences:

  • YAML loses to the platform. Writing DATABASE_URL: in deploy[].environment is silently overwritten whenever the app has a database.
  • mecca var set beats the platform. You can override DATABASE_URL or REDIS_URL with a stored variable — useful for pointing at an external database, and a foot-gun if left behind by accident. If your app is mysteriously talking to the wrong database, check mecca var list first.
  • Mailer variables cannot be overridden.

8.5 Cross-service references

environment:
  API_URL: "${service:backend}"   # → https://<backend's public URL>
  • Resolves to the referenced service's public HTTPS URL.
  • Only services that declare a port are addressable. ${service:worker} on a port-less worker fails the deploy.
  • A typo referencing an undeclared service is caught by validation.
  • At build time (for static frontends) the template must be the entire value. "${service:backend}/v1" is left literal in a build arg; append the path in your code instead.

path: — mount several services beneath ONE URL

Your app answers on one hostname, with its services on paths beneath it. Give a service a path: and it is served there:

deploy:
  - name: web
    container: web
    port: 3000
    primary: true        # the front door — serves `/`, declares NO path
  - name: api
    container: api
    port: 8080
    path: /api           # served at https://<your-app-url>/api

A same-origin API needs no reverse proxy and no CORS: your frontend calls /api/... on its own origin. Use ${service:NAME:path} if you want the mount path itself rather than a full URL.

Four shapes are still refused, each with its own message: two front doors (nothing can serve / twice — split the app), a type: static service with a path: (a static origin cannot forward a path), two services claiming /, and a mount that reaches outside its own service.

${service:NAME:internal} — stays in-VPC, and no longer goes stale

An internal-network variant keeps the call inside the VPC instead of going out over the public route — roughly 3 ms instead of ~25 ms. It used to go stale after the callee's second deploy; that was fixed in BUGS #680, and the address a service is given is now permanent.

Both services must be in the same environment and the same branch.

8.6 Frontend variables

For a static frontend, deploy[].environment values are exported during the build, so your bundler bakes them in. Use your framework's public prefix:

Framework Prefix
Vite VITE_
Create React App REACT_APP_
Next.js NEXT_PUBLIC_
- name: frontend
  type: static
  static_site: { build_command: "npm ci && npm run build", output_dir: "dist" }
  environment:
    VITE_API_URL: "${service:backend}"

Anything baked into a browser bundle is public

Never reference a secret from a frontend service's environment.


9. Built-in email

AppMecca has email built in — use it instead of wiring up a mail provider.

What it is for

Notifying yourself: the developer, the site owner, the team that runs the app.

  • A contact form was submitted — send yourself the contents.
  • A threshold was crossed (stock low, signups spiked, disk filling) — send an alert.
  • Something happened the owner would want to know about (new order, failed job, new registration) — send a notification.

What it is not: a bulk, marketing or customer-mailing service. Do not build newsletters, drip campaigns or mass customer email on it — the quotas (below) are sized for notification volume, and the From address is platform-stamped, not your brand.

When a user asks for "email me when the form is submitted", "notify me if X", or any send-myself-a-message feature, reach for this first — it needs no mail server, no third-party account, no credentials and no DNS. One line of YAML:

Enabling it

email:
  enabled: true

Redeploy, and your container receives APPMECCA_MAILER_URL (the complete send endpoint — do not append a path to it), APPMECCA_MAILER_TOKEN and APPMECCA_MAILER_FROM. Send by POSTing JSON:

import os, requests
requests.post(
    os.environ["APPMECCA_MAILER_URL"],
    headers={
        "Authorization": f"Bearer {os.environ['APPMECCA_MAILER_TOKEN']}",
        "X-AppMecca-App": os.environ["APPMECCA_APP"],   # REQUIRED — 401 without it
    },
    json={
        "to": ["user@example.com"],
        "subject": "Welcome",
        "text": "Plain-text body",
        "html": "<p>HTML body</p>",
    },
    timeout=10,
)

Both headers are required on every send: the bearer token and X-AppMecca-App set to your app's canonical name — always read it from the injected APPMECCA_APP variable, never hardcode it.

Field Required Notes
to Yes List of addresses, at least one
subject No No control characters
text / html At least one Body
cc, bcc No Lists of addresses
reply_to No Single address
idempotency_key No Prevents duplicate sends on retry

Rules

  • The From address is stamped by the platform ({app}@{env-domain}) — you cannot set it. Use reply_to if replies should reach a real mailbox.
  • Unknown fields are ignored, not rejected — a typo in a key is silently dropped and the send still returns 200, so check your field names against the table above rather than relying on an error.
  • Recipients that hard-bounce or complain are suppressed automatically; later sends to them are refused. This protects the platform's deliverability — another reason this is a notify-yourself service.
  • Quotas (defaults): 2,400 emails per rolling 24 hours, per app per branch; bursts within the window are unrestricted (no per-minute limit). Ample for notifications; deliberately not sized for bulk mail. Every response carries X-Quota-Limit/X-Quota-Remaining/X-Quota-Reset; over-quota sends return 429 with a Retry-After.
  • Set idempotency_key on sends triggered by user actions (form submissions), so a retry never emails you twice.

A typical contact-form handler:

@app.post("/contact")
def contact(form: ContactForm):
    requests.post(
        os.environ["APPMECCA_MAILER_URL"],
        headers={
            "Authorization": f"Bearer {os.environ['APPMECCA_MAILER_TOKEN']}",
            "X-AppMecca-App": os.environ["APPMECCA_APP"],
        },
        json={
            "to": [os.environ["OWNER_EMAIL"]],        # yourself — set via `mecca var set`
            "subject": f"Contact form: {form.name}",
            "text": f"From: {form.name} <{form.email}>\n\n{form.message}",
            "reply_to": form.email,                    # so you can reply directly
            "idempotency_key": form.submission_id,
        },
        timeout=10,
    )
    return {"ok": True}

Testing it from the CLI

After deploying with email.enabled: true, prove the whole path works — before writing any application code — with:

cd ~/myapp                              # run from inside the app's repo
# Smoke-test the relay from inside the container (must be YOUR logged-in email):
mecca app exec myapp -- sh -c 'curl -sS -X POST "$APPMECCA_MAILER_URL" -H "Authorization: Bearer $APPMECCA_MAILER_TOKEN" -H "Content-Type: application/json" -d "{\"to\":[\"you@example.com\"],\"subject\":\"test\",\"text\":\"ok\"}"'

Three things to know about how it targets the app:

  • There is no app argument. The app is inferred from the directory you run it in — the last path segment of git remote get-url origin. Run it from the app's repo.
  • This assumes the repo's name is the app name. If the app was registered under a different name, detection mismatches and the command reports the app as not found.
  • For an app in a shared group, prefix the root flag: mecca --group <slug> app exec ….

It runs a send inside your live container through the injected APPMECCA_MAILER_* variables — the real production path, not a shortcut — and the errors are actionable: 401 → the deploy with email.enabled: true hasn't landed yet; 403 → the recipient is suppressed; 429 → quota, wait Retry-After. The recipient must be your own logged-in address — any other is refused.

If auto-detection can't work (renamed repo, no local checkout), the manual equivalent gives explicit control over app and group:

mecca app exec myapp -- sh -c 'curl -sS \
  -H "Authorization: Bearer $APPMECCA_MAILER_TOKEN" \
  -H "X-AppMecca-App: $APPMECCA_APP" \
  -H "Content-Type: application/json" \
  -d "{\"to\": [\"you@example.com\"], \"subject\": \"test\", \"text\": \"relay works\"}" \
  "$APPMECCA_MAILER_URL"'

Then observe with:

mecca email status  myapp        # enablement + send volume
mecca email events  myapp        # per-send delivery log (sent / bounced / suppressed)
mecca email suppress list myapp  # who is on the suppression list

10. Cron

A cron job is a scheduled HTTP request AppMecca makes to one of your own endpoints — not a command run in a container.

cron:
  - name: cleanup-sessions
    schedule: "0 3 * * *"      # 5 fields, UTC
    url: /internal/cleanup     # must start with /
    service: web               # required when the app has more than one service
    method: POST               # GET or POST, default POST
    timeout: 30                # 5-300 seconds
  • Write the endpoint yourself; guard it (it is reachable on your service's normal URL).
  • Keep it fast and idempotent. If work takes longer than timeout, have the endpoint enqueue rather than execute.
mecca cron list    myapp        # note: app name is POSITIONAL, there is no -a flag
mecca cron history myapp
mecca cron trigger myapp cleanup-sessions
mecca cron disable myapp cleanup-sessions
mecca cron enable  myapp cleanup-sessions

11. Visibility — public and private apps

deploy:
  - name: web
    container: web
    port: 8000
    visibility: private     # public (default) | private
  • public — anyone with the URL reaches your app.
  • private — anonymous visitors are redirected to sign in; only members of your group are let through.

Visibility is app-wide

If any service declares visibility: private, the whole app becomes private. You cannot mix public and private services in one app.

Flip it after registration without editing YAML:

mecca app protect myapp            # → private
mecca app publish myapp            # → public
mecca app protect myapp -b dev     # a specific branch

Allow up to ~90 s. The ALB listener rule is rewritten within ~15 s, but each gateway replica resolves visibility from its own hostmap cache (60 s TTL) and expires independently — measured on qa, an app was still served publicly by some replicas at 62 s and fully converged at ~100 s. Until then a visitor may still reach the old behaviour; give it the full window before investigating.

⚠ If you are flipping an app private in response to a leak, the ~90 s is the number that matters — the app stays anonymously readable until every replica has refreshed. Content that was already public is what remains readable.

⚠ On a multi-branch app, pass -b. visibility is a per-branch column, so flipping one branch leaves its siblings untouched — believing otherwise means believing an app is protected while another branch of it is still public. Both commands refuse rather than guess when an app has more than one branch, and name the branches so you can choose.


12. Edge caching (deploy[].cache)

Puts AppMecca's content delivery cache in front of a container service.

deploy:
  - name: web
    container: web
    port: 8000
    cache:
      enabled: true
      default_ttl: 0                # unmatched paths: 0 = do not cache
      invalidate_on_deploy: true
      behaviors:
        - path: "/static/*"
          ttl: 31536000
          compress: true
        - path: "/api/*"
          ttl: 0
          forward_all_headers: true
          allowed_methods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
Field Default Notes
enabled true On as soon as the block exists. Set false to declare-but-disable.
default_ttl 0 Seconds for paths matching no behavior. 0 = no caching.
invalidate_on_deploy true Purge the cache after each deploy.
behaviors[].path — Pattern, e.g. /static/*.
behaviors[].ttl 86400 Seconds. 0 = do not cache.
behaviors[].compress true Compress responses.
behaviors[].forward_query_strings false Include the query string in the cache key.
behaviors[].forward_cookies none none, all, or whitelist.
behaviors[].forward_all_headers false Effectively disables caching for that path.
behaviors[].allowed_methods [GET, HEAD] Add the write methods for any /api/* path, or POST/PUT/DELETE are rejected.

Two traps

  • Writing cache: {} under a service enables edge caching (enabled defaults to true). To keep it off, omit the block entirely.
  • A cached path serves stale content until its TTL expires. Never cache authenticated responses — use ttl: 0 and forward_all_headers: true for them.

type: static services are already served from the edge; they do not need this block.


13. Custom domains

domain: and domains: in appmecca.yaml are ignored. Certificates cannot be driven from config. Use the CLI:

mecca domain add    myapp www.example.com -s web
mecca domain list   myapp
mecca domain verify myapp www.example.com
mecca domain remove myapp www.example.com

The command prints the DNS records to create. Your platform URL keeps working alongside the custom domain.


14. The Dockerfile contract

What AppMecca requires

  • Your process must listen on 0.0.0.0 (not 127.0.0.1) on the port you declared in deploy[].port.
  • The image must run without privileged capabilities.
  • If you declare a command: in YAML, the image needs a shell (it runs via sh -c).

What AppMecca does to your container at runtime

Behaviour Consequence for you
Runs with all Linux capabilities dropped and no-new-privileges A minimal set is restored for a service on container port 80/443, and — for any other declared port — when the image runs as root and declares an EXPOSE. A service with no port: gets none. Prefer running as a non-root user with cache directories pre-created and chowned at build time, so no capability is needed at runtime.
Process limit of 256 A very large worker pool or a supervisor tree can hit the ceiling. Keep worker counts modest.
Overrides your HEALTHCHECK when the service declares health_check Your image must contain one of curl, wget, python3 or python for the probe to run.
Disables your HEALTHCHECK when the service declares none Do not rely on the Dockerfile HEALTHCHECK as the liveness signal — the service's health_check is the real gate.
Builds with no layer cache Cache-ordering tricks buy nothing. Optimise for cold-build cost: small bases, multi-stage builds, target:.

EXPOSE is not enough

If you omit deploy[].port, AppMecca guesses from the image's exposed ports using a preference list (8000, 8080, 3000, 5000, 80) — so EXPOSE 3000 8080 silently selects 8080. Always declare port: explicitly.

Build arguments and secrets

deploy[].environment values and containers[].args are passed to docker build as --build-arg. Declare ARG in your Dockerfile to use them.

Values set with mecca var set are passed as secrets, not build args. The build can read them, but they are not stored in the image. Read one in the RUN step that needs it; the id is the variable's name:

RUN --mount=type=secret,id=NPM_TOKEN \
    NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" npm ci

A name must be letters, digits and underscores and must not start with a digit. A variable with any other name is not passed to the build, and the build log says so.

If a stored variable is not a secret and your build needs it as an ARG (for example a public URL baked into a frontend bundle), list it under the container:

containers:
  - name: web
    dockerfile: Dockerfile
    build_args_from_vars: [PUBLIC_API_URL]

Build args are stored in the image

Anyone who can pull the image can read every build arg from its history. Never pass a secret as a build arg. Do not copy a secret into a file the image keeps: RUN cat /run/secrets/X > .npmrc leaves it in that layer.

Release 6.4 transition

In release 6.4, a stored variable that your Dockerfile declares with ARG, and that is not in build_args_from_vars, is still passed as a build arg. The build log and mecca queue status show a warning naming it. From release 6.5 that build is refused. Read it as a secret, or list it in build_args_from_vars.

Architecture

Images are built for the platform's host architecture, which may be ARM64. Do not hardcode an x86-64 binary download. Use the multi-arch variable:

ARG TARGETARCH
RUN curl -fsSL "https://example.com/tool-linux-${TARGETARCH}.tar.gz" | tar xz

Base images

Public images from Docker Hub, GitHub Container Registry and public registries are pulled through a platform cache automatically — no change needed. Private third-party registries are not supported; vendor what you need or publish to a supported registry.

A good Dockerfile

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Run as a non-root user
RUN useradd -m -u 10001 appuser && chown -R appuser /app
USER appuser

EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2"]

Multi-stage, building a frontend into a backend image:

FROM node:20-slim AS frontend
WORKDIR /fe
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
COPY --from=frontend /fe/dist ./static
EXPOSE 8080
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]

15. Deploying

First time

# 1. Register the repository
mecca register git@github.com:org/myapp.git

# 2. Build and deploy
mecca app deploy myapp

mecca register does not start a build. It records the app and runs a repository pre-flight check: can AppMecca reach the repo and branch, is there an appmecca.yaml, is it valid, do the referenced Dockerfiles exist. The app cannot build or deploy until pre-flight passes.

Verdict Meaning Fix
ready Good to go —
pending Check still running mecca app recheck myapp
no_access Cannot reach the repo/branch Grant AppMecca access to the repository; check the branch name
needs_config No appmecca.yaml found Add one and push
config_invalid The YAML is invalid Fix it, push, then mecca app recheck myapp

After pushing a fix: mecca app recheck myapp.

Day to day

mecca app redeploy myapp              # rebuild from the repo and deploy
mecca app deploy   myapp --no-build   # redeploy the current image (fast)
mecca build trigger                   # queue a build; deploys automatically on success
mecca build trigger --no-deploy       # build without deploying

Branches

Each branch is registered and deployed independently, with its own URL, database and storage.

mecca register git@github.com:org/myapp.git -b dev
mecca app deploy myapp -b dev

Watching a deploy

mecca queue list                     # everything in flight
mecca queue status <action-id>       # one action
mecca queue logs   <action-id>       # its logs
mecca logs myapp --type build        # build output
mecca logs myapp --type deploy       # deploy output
mecca logs myapp --type container --service web   # your container's own stdout/stderr
mecca app show myapp                 # current state and URLs

If an action sits in queued, run mecca queue diagnose <action-id> — it explains what is holding it (capacity, ordering behind another action, or a declared-storage refusal).

Rolling back

mecca build list -a myapp                                  # Tag column; pick a SUCCEEDED build
mecca app show     myapp                                   # what is running RIGHT NOW
mecca app deploy   myapp --image-tag <tag> --no-build      # ship a previous image

--no-build re-uses an image that already exists, so a rollback cannot be blocked by whatever broke the build.

How a deploy runs

AppMecca starts the new container alongside the old one, waits for it to pass its health check, switches traffic, then drains and removes the old container. If the new container never becomes healthy, the old one keeps serving — a failed deploy is not an outage.

This is why startup_timeout matters: it is the budget for your new container to answer its first successful health check.


16. Restoring a database

From a dump you have (the usual case)

# 1. Create the dump — custom format is strongly recommended
pg_dump -Fc mydb > prod.dump

# 2. Restore it
# Dashboard → myapp → Database tab → choose the file → confirm
# (no CLI equivalent on the shipping `mecca` — FEATURES #366)

You are asked to confirm, then to type the app name back. The file uploads directly and the platform:

  1. Validates the dump (format, PostgreSQL version, content).
  2. Takes a safety backup of the current database — the restore is refused if this fails.
  3. Resets the schema and applies your dump.
  4. Rolls back to the safety backup automatically if the restore fails.

Then check the outcome:

mecca queue status <action-id>                      # queue row; per-table counts are on the dashboard

Rules and limits:

Maximum size 5 GB
Accepted formats Custom (pg_dump -Fc) recommended; plain .sql; gzipped plain .sql.gz
Rejected A gzipped custom dump (.dump.gz)
PostgreSQL version A dump from a newer PostgreSQL than the platform's is refused. Older → newer is fine.
Branch -b defaults to main. Pass it explicitly for any other branch.
Authentication Requires a signed-in session, plus the object's name typed back. API keys are refused. ⚠ The typed name is a foot-gun guard, not an auth control — the session itself is the credential.

A plain dump taken with default flags is refused

Default pg_dump output contains ALTER ... OWNER TO, GRANT and REVOKE statements referring to roles that do not exist on AppMecca. The platform refuses it before touching anything and tells you to re-dump. Use either:

pg_dump -Fc mydb > prod.dump                        # recommended
pg_dump --no-owner --no-acl mydb | gzip > prod.sql.gz
The custom format needs no extra flags — ownership is dropped structurally on restore.

From a platform backup

mecca app backup   myapp                      # take one now
mecca app backups  myapp                      # list them
mecca app restore  myapp <bundle-prefix>              # database + drives
mecca app restore  myapp <bundle-prefix> --db-only    # database only
mecca app download myapp <bundle-prefix>              # download the bundle
mecca app rotate-credentials myapp -b main --confirm myapp --wait # revoke the DB + cache passwords (needs a signed-in session; an API key is refused)

A bundle contains the database and a copy of each drive — not bucket objects. Contents are checksum-verified before anything is written, and drives are swapped atomically. On a multi-branch app, -b is required.

Seeding a new app from another app's backup

mecca register git@github.com:org/myapp.git --seed-from otherapp:main:latest

The seed runs once, on the first deploy.


17. Complete worked examples

Flask API with a database

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

deploy:
  - name: web
    container: web
    port: 8000
    health_check:
      path: /health
    resources:
      memory: 256m

database:
  enabled: true
  max_connections: 5

Django + Celery + database + cache

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

deploy:
  - name: web
    container: app
    port: 8000
    command: "gunicorn myproject.wsgi --bind 0.0.0.0:8000 --workers 3"
    health_check:
      path: /health
      startup_timeout: 300
    resources:
      memory: 512m
    environment:
      DJANGO_SETTINGS_MODULE: myproject.settings.production

  - name: worker
    container: app
    command: "celery -A myproject worker -l info"
    resources:
      memory: 256m

  - name: beat
    container: app
    command: "celery -A myproject beat -l info"
    singleton: true
    resources:
      memory: 128m

database:
  enabled: true
  max_connections: 5

redis:
  enabled: true

React SPA — static, no Dockerfile

static_site:
  build_command: "npm ci && npm run build"
  output_dir: "dist"
  spa_fallback: true
  index_document: "index.html"
  error_document: "index.html"

deploy:
  - name: web
    type: static
    primary: true

Full-stack: API container + static SPA + media bucket + drive

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

deploy:
  - name: api
    container: api
    path: /api
    port: 8000
    health_check:
      path: /api/health
    resources:
      memory: 512m
    drives:
      - name: workdir
        path: /data/work

  - name: web
    primary: true
    type: static
    static_site:
      build_command: "cd frontend && npm ci && npm run build"
      output_dir: "frontend/dist"
      spa_fallback: true
      index_document: "index.html"
      error_document: "index.html"
    environment:
      VITE_API_URL: "${service:api}"

database:
  enabled: true
  max_connections: 5

redis:
  enabled: true

storage:
  drives:
    - name: workdir
      size_gb: 2
  buckets:
    - name: uploads       # user media — object storage, not the drive

email:
  enabled: true

cron:
  - name: nightly-cleanup
    schedule: "0 3 * * *"
    url: /api/internal/cleanup
    service: api

Documentation site (non-SPA static)

static_site:
  build_command: "pip install -r requirements.txt && mkdocs build"
  output_dir: "site"
  spa_fallback: false         # real 404s for a real static site
  index_document: "index.html"
  error_document: "404.html"

deploy:
  - name: docs
    type: static
    primary: true

18. Pre-flight checklist

Before deploying, verify every line:

Structure

  • [ ] File is appmecca.yaml in the repository root.
  • [ ] Every key matches this reference exactly — no typos (§2.0).
  • [ ] deploy: has at least one service.
  • [ ] containers: present unless every service is type: static.
  • [ ] Every deploy[].container matches a containers[].name.
  • [ ] Every containers[] entry writes dockerfile: explicitly.

Services

  • [ ] Every web-facing service declares port: explicitly.
  • [ ] Your process binds 0.0.0.0, not 127.0.0.1, on that port.
  • [ ] Web services have a health_check returning 200 at the declared path.
  • [ ] healthy_threshold / unhealthy_threshold are ≥ 2 if set.
  • [ ] Workers have no port and no health_check.
  • [ ] replicas is 1–3.

Static

  • [ ] type: static services declare no port, command, health_check, drives, or replicas > 1.
  • [ ] A static_site: block exists (top-level or per-service) for every type: static service.
  • [ ] output_dir matches what the build actually writes.
  • [ ] spa_fallback: true for client-routed SPAs; false for true static sites.
  • [ ] No bare type: spa without command: or static_site:.

Resources

  • [ ] Every resources.memory has a m or g suffix.
  • [ ] JVM services declare at least 512m.
  • [ ] No reliance on resources.cpu.

Backing services

  • [ ] database.max_connections matches the real pool size.
  • [ ] No hardcoded database name — code reads DATABASE_URL.
  • [ ] No hardcoded bucket name — code reads APPMECCA_BUCKET_{NAME}_NAME.
  • [ ] Bulk media is in a bucket, not a drive.
  • [ ] Every deploy[].drives[].name matches a declared storage.drives[].name.
  • [ ] Mount paths are absolute and not system directories.
  • [ ] Cache keys are prefixed with CACHE_PREFIX.

Secrets

  • [ ] No credentials in appmecca.yaml.
  • [ ] Secrets set with mecca var set.
  • [ ] No secret referenced from a static/frontend service's environment.
  • [ ] AWS_ACCESS_KEY_ID and friends are not set by you if you use buckets.

Other

  • [ ] No domain: / domains: keys (ignored — use mecca domain add).
  • [ ] Cron url values start with /, and service is set on multi-service apps.
  • [ ] Edge cache: blocks are intentional (the block being present turns it on).

19. Troubleshooting

Symptom Most likely cause
A setting "does nothing" Typo in the key name — silently ignored (§2.0). Check spelling first, always.
Deploy fails at the health check Your app binds 127.0.0.1, the port does not match deploy[].port, the health path 404s, or startup_timeout is too short.
Container starts then dies immediately Out of memory. Raise resources.memory (§3). JVM apps need ≥ 512m.
Static site: home page works, deep links 404 Prerendered pages (about.html on disk): set flat_html_urls: true (§4.2). Client-routed SPA (routes only exist in JS): set spa_fallback: true. ⚠ Do not reach for spa_fallback on a prerendered site — it returns 200 from the shell, so the links look fixed while the prerendered HTML is never served.
Static deploy fails looking for the entry document output_dir is wrong, or the build emits 200.html — set index_document to match.
Frontend calls undefined / ${service:...} literally The variable lacks the framework's public prefix, or the template was not the whole value (§8.5, §8.6).
App talks to the wrong database A stale DATABASE_URL from mecca var set is overriding the platform's (§8.4). Run mecca var list.
Bucket calls fail with an auth error You set AWS_ACCESS_KEY_ID yourself and shadowed the injected credential (§7.2).
Bucket variables missing entirely Credential injection is best-effort; redeploy. Check mecca app show.
Files uploaded on main invisible on dev Drives and buckets are per-branch by design (§7).
Deploy refused: declared drive storage Over the free allowance / group grant (§7.1). Lower size_gb or request a grant.
POST to a cached path is rejected Add the write methods to allowed_methods for that behavior (§12).
Serving stale content An edge cache TTL. Never cache authenticated responses.
Cannot build or deploy at all Pre-flight has not passed. mecca app show myapp, fix, mecca app recheck myapp (§15).
Action stuck in queued mecca queue diagnose <action-id>.
Restore refused before starting Dump taken with default pg_dump flags, or from a newer PostgreSQL (§16).

Always start with:

mecca debug myapp        # one-shot diagnostic: config, recent errors, health, URLs
mecca logs  myapp        # recent output

20. Quick reference

app_name: My App                    # optional display name

containers:                         # omit when every service is type: static
  - name: web
    dockerfile: Dockerfile          # always write this
    context: .
    args: {}
    target: null

deploy:
  - name: web
    container: web
    port: 8000                      # omit for workers
    replicas: 1                     # 1-3
    command: null
    singleton: false
    visibility: public              # public | private
    primary: null                   # leave unset for auto-detect
    drain_delay_seconds: 30         # 0-300
    health_check:
      path: /health
      interval: 30                  # 5-300
      timeout: 5                    # 2-120
      healthy_threshold: 2          # 2-10  (minimum 2!)
      unhealthy_threshold: 3        # 2-10  (minimum 2!)
      startup_timeout: 120          # 30-600
    resources:
      memory: 256m                  # SUFFIX REQUIRED (k/m/g). cpu does nothing.
    environment:
      KEY: value                    # cross-service refs: "${service:<name>}" — §8.5
    drives:
      - name: media
        path: /data/media           # absolute; not /,/etc,/usr,/var,/bin,/sbin,/boot,/dev
        readonly: false
    cache:                          # EDGE caching — enabled:true by default!
      enabled: true
      default_ttl: 0
      invalidate_on_deploy: true
      behaviors:
        - path: "/static/*"
          ttl: 31536000
          compress: true
          forward_query_strings: false
          forward_cookies: none
          forward_all_headers: false
          allowed_methods: [GET, HEAD]

static_site:                        # required if any service is type: static
  build_command: "npm run build"
  output_dir: dist
  spa_fallback: false
  index_document: index.html
  error_document: 404.html

database:
  enabled: false
  max_connections: 5                # 1-50

redis:                             # managed Redis (ElastiCache)
  enabled: false

email:
  enabled: false

storage:
  drives:
    - name: media                   # kebab-case, starts with a letter
      size_gb: 5                    # 1-100
  buckets:
    - name: uploads

cron:
  - name: job
    schedule: "0 3 * * *"           # 5 fields, UTC
    url: /internal/job              # must start with /
    service: web                    # required on multi-service apps
    method: POST                    # GET | POST
    timeout: 30                     # 5-300

Command cheat sheet

# Lifecycle
mecca register <git-url> [-b branch]     # register (does NOT build)
mecca app recheck  myapp                 # re-run repository pre-flight
mecca app deploy   myapp                 # deploy
mecca app redeploy myapp                 # rebuild + deploy
mecca app deploy   myapp --no-build      # redeploy current image
mecca build list  -a myapp               # build history (Tag + Status)
mecca app undeploy myapp                 # stop serving
mecca app delete   myapp                 # remove entirely

# Observe
mecca app show   myapp
mecca app list
mecca debug      myapp
mecca logs       myapp [--type build|deploy|container] [--service web]
mecca queue list / status <id> / logs <id> / diagnose <id> / cancel <id>
mecca app stats  myapp

# Variables
mecca var set/list/unset/load myapp ...

# Storage
mecca drive ls/du/tree/stat/upload/download/mkdir/mv/cp/rm/purge myapp <drive> ...
mecca bucket list/reveal/rotate-key myapp

# Database
mecca app backup/backups/restore/download myapp ...   # uploading a dump: dashboard only

# Other
mecca domain add/list/verify/remove myapp ...
mecca cron   list/history/trigger/enable/disable myapp ...
mecca email  status/events myapp         # built-in email: enablement + delivery log
mecca email  suppress list myapp         # suppression list
mecca app exec myapp -- <command>        # one-off command in a running container

⚠ mecca app exec captures stdin whenever stdin is not a terminal — that is what makes cat fixture.sql | mecca app exec myapp -- psql -f - work. If you are invoking it from a harness that attaches a pipe to the CLI and never writes to it, append < /dev/null: the CLI now refuses after 10s rather than waiting (BUGS #835), because guessing "no input" would turn a piped restore into a silent no-op reported as success.


21. Agent operating procedure — taking a project live, end to end

This section is the ordered procedure an AI agent should follow when asked to put a project on AppMecca. It tells you what to inspect, what to ask the user and when, and which commands to run in which order. Do not skip the questions — several decisions (media placement, backup restore, branch) cannot be inferred from code and are expensive to change later.

Step 1 — Inspect the project's code

Before writing any configuration, establish these facts from the repository:

Question Where to look Drives
Does anything run a server process? entry points, package.json scripts, manage.py, main.go… App shape (§1)
What port does it listen on, and does it bind 0.0.0.0? server startup code, existing Dockerfile EXPOSE deploy[].port, §14
Is there a frontend built to static files? vite.config.*, package.json build script, framework config type: static service (§4.2), its output_dir
Does it use a SQL database? ORM config, DATABASE_URL reads, migration folders database.enabled (§5) + Step 3 questions
Does it use Redis / a key-value cache / Celery / Sidekiq / Bull? broker config, REDIS_URL reads cache.enabled (§6)
Does it read or write files at runtime (uploads, media, generated files)? upload handlers, MEDIA_ROOT, multer, file writes Storage (§7) + Step 3 questions
Are there background workers or schedulers? Celery/Sidekiq/Bull workers, cron-like code Port-less services (§2.5), singleton, cron (§10)
Does it send (or want to send) email — contact forms, alerts, notifications? mail-library usage, SMTP config, form handlers with nowhere to send Built-in email (§9) — use it instead of a mail provider
Is it a JVM app? pom.xml, build.gradle resources.memory: 512m minimum (§3)
What secrets does it need? .env.example, config that reads env vars mecca var set list for Step 5
Is there an existing health endpoint? routes health_check.path; add one if missing
Is there an existing Dockerfile, and does it match §14's contract? Dockerfile Fix binding/arch/user issues before first build

Step 2 — Draft appmecca.yaml and the Dockerfile

  1. Pick the app shape with the decision tree in §1.
  2. Write appmecca.yaml from the reference in §2 (copy the nearest worked example in §17 and adapt — do not write from memory).
  3. Write or fix the Dockerfile per §14. Migrations belong in the container's start command (§5).
  4. Run the entire pre-flight checklist in §18, line by line.

Step 3 — Interview the user

Ask only the questions made relevant by Step 1, but never skip a relevant one. Present them together, as one short list, before touching the platform.

Always ask:

  • "Which environment and which Git branch am I deploying?" (branch matters: every branch gets its own database, storage and URL — §5, §7)
  • "Should the app be public (anyone with the URL) or private (your group only)?" (§11)
  • "Do you have the mecca CLI configured and signed in?" (If unsure, verify with mecca whoami. Everything below fails without it.)
  • "Is there a custom domain to attach, now or later?" (§13 — done after deploy, never in YAML)

If the app uses a database, also ask:

  • "Is there an existing database to bring over — a backup or dump from a previous host — or do we start empty?"
  • If yes: "Can you produce it as pg_dump -Fc (custom format)? What size is it, and which PostgreSQL version produced it?" (≤ 5 GB; a dump from a newer PostgreSQL than the platform's is refused — §16)
  • "Should the restore land before users see the site?" (usually yes — plan restore between first deploy and announcing the URL)

If the app handles uploads/media/files, also ask:

  • "Is there an existing media library to migrate (e.g. an uploads folder from the old server)? Roughly how large?"
  • "Bucket or drive?" — then recommend based on §7: bucket for user uploads and anything that grows; drive only when the framework demands a filesystem path or the data must ride in platform backups. State the trade-off (drive: ~13× cost, 5 GB free / 100 GB hard cap, in backups; bucket: cheap, unbounded, not in backups) and let the user decide.
  • If drive and the media is large: "Your declared size_gb total above 5 GB needs an operator storage grant — do you have one, or should we use a bucket instead?"

If Step 1 found secrets, also ask:

  • "Please provide values for: <list every env var the code reads that is not platform-injected>." (Never guess secrets; never write them into appmecca.yaml — §8.1)

If the app has forms, alerts or anything worth notifying the owner about, also ask:

  • "Do you want the site to email you when things happen — form submissions, alerts, system events? Which address should notifications go to?" (If yes: enable the built-in email, §9 — no mail provider needed. Store the address with mecca var set <app> OWNER_EMAIL=..., never hardcoded.)
  • If the user asks for customer/marketing/bulk email: explain the built-in email is for notifying themselves, not their customers, and that bulk mailing needs an external provider.

Step 4 — Register and pass pre-flight

mecca register <git-url> [-b <branch>]
mecca app show <app>          # check preflight_status

Fix any verdict per the table in §15 (mecca app recheck <app> after pushing a fix). Do not proceed until the verdict is ready.

Step 5 — Set environment variables before the first build

Secrets and per-environment values collected in Step 3:

mecca var set <app> KEY1=value1 KEY2=value2 -b <branch>

Set them before the first deploy: variables are also delivered to the build as build arguments, and a static frontend bakes its VITE_*/REACT_APP_*/NEXT_PUBLIC_* values in at build time (§8.6). Remember: omitting -b writes to every branch (§8.2).

Step 6 — First deploy

mecca app deploy <app> [-b <branch>]
mecca queue list                          # find the action id(s)
mecca queue status <action-id>            # watch; a mixed app has TWO actions
mecca app show <app>                      # URL once running

If it fails, work the troubleshooting table in §19 — health-check binding, memory, and startup_timeout cover most first-deploy failures. A failed deploy never takes down a previously-running version (§15).

Step 7 — Restore the database (if the user has one)

After the first successful deploy (the deploy also provisions the database):

# Uploading a pg_dump is DASHBOARD ONLY on the shipping CLI (FEATURES #366):
#   your app -> Database tab -> choose the file -> confirm
mecca queue status <action-id>                        # follow the queue row

Follow §16 exactly: custom format (pg_dump -Fc) is the format that works with no extra flags; a plain default-flags dump is refused. The platform takes a safety backup first and rolls back automatically on failure, so this step is safe to attempt.

Note this command prompts for a typed confirmation — it cannot run fully unattended; hand it to the user or run it while they are present.

Step 8 — Migrate media (if the user has any)

To a drive (declared in Step 2, provisioned by the deploy in Step 6):

mecca drive upload <app> <drive> ./media/ [-b <branch>]   # recursive, resumable
mecca drive du     <app> <drive>                          # confirm usage vs size_gb

To a bucket — there is no CLI upload (§7.3). The two working patterns:

  1. Stage the files where the container can read them (a temporarily-declared drive, or a downloadable archive), then run an import inside the running container, which already holds the bucket credential:
    mecca app exec <app> -- python import_media.py   # copies files → bucket via APPMECCA_BUCKET_* / standard S3 client
    
  2. Or use the application's own upload/import feature if it has one.

Verify by exercising an app URL that serves the migrated media.

Step 9 — Final verification

  • [ ] mecca app show <app> — running, correct URL.
  • [ ] Open the URL: pages load, deep links work (static SPAs: spa_fallback, §4.2).
  • [ ] Restored data visible in the app (spot-check a known record).
  • [ ] Migrated media serves correctly.
  • [ ] mecca logs <app> — no error loop.
  • [ ] mecca debug <app> — env-var drift report is clean.
  • [ ] Visibility matches the user's answer (mecca app protect/publish <app>, §11).
  • [ ] If email is enabled: the relay curl in §9 delivers to your login email, and a real trigger (e.g. submitting the contact form) lands in the owner's inbox.
  • [ ] If requested: mecca domain add <app> <domain> -s <service>, give the user the DNS records it prints, then mecca domain verify (§13).

Step 10 — Report back

Tell the user: the URL, which backing services were provisioned (database, cache, drives, buckets), what data was restored and how verified, which env vars were set (names only — never echo values), and anything deferred (custom domain pending DNS, media import still running, a storage grant to request).


This guide reflects the platform's current validation rules. When AppMecca's behaviour and this document disagree, the platform is right — report the discrepancy.