Skip to content

Services

The deploy section of appmecca.yaml defines how your containers run in production. Each entry in the deploy list is a service — a running instance of a container image.

Basic Example

deploy:
  - name: web
    container: myapp
    port: 8000
    health_check:
      path: /health

This runs the myapp container as a service called web, listening on port 8000, with a health check at /health.

All Fields

Field Required Default Description
name Yes — Service name (used in URLs and discovery). 1-63 characters, lowercase letters, digits and hyphens; must not start with a hyphen. Underscores are allowed only on a service with no port. See the note below.
container Conditional — Container name (must match a containers[].name). Not required for type: static, or for a type: spa service that has a static_site: block and no command: — neither runs a container.
port No — Port your app listens on inside the container. Omit for a worker with no HTTP surface — it then gets no load-balancer route.
command No — Override the Dockerfile CMD. On a type: spa service this also forces the container path — see below.
replicas No 1 Number of container instances (1-3), each on a different host (anti-affinity). Placement is tiered: the base tier is spread-packed across hosts, later tiers bin-packed.
health_check No — Health check configuration. Omitting it disables container health checking rather than applying defaults.
environment No {} Environment variables for this service. Non-sensitive only — use mecca var set for secrets.
resources No — CPU and memory limits. The memory default is runtime-aware, not a flat number — see Resources.
drain_delay_seconds No 30 How long the platform router drains connections from the old container during a deploy (0-300).
runtime No container function runs the service's image on AWS Lambda instead of a host. See Function Services.
function No — Memory, timeout and concurrency for a runtime: function service. Read only when runtime: function is set.
visibility No public public or private. A private service sits behind the auth gateway. Most-restrictive wins: one private service makes the whole app private.
primary No — Marks the app's front door — the service whose URL and preview are shown. Auto-detected when unset.
static_site No — Per-service build settings, overriding the top-level static_site: block. Lets a multi-frontend app build each bundle differently.
drives No [] Drives to mount into this service. Declare them under top-level storage: first.
type No — static, spa, ssr or api. static is the important one — it skips the container entirely. See Service types.
spa_root No /app/build Where the file server serves from, for a containerised SPA only. Irrelevant on the static path.
singleton No false Deploy on the leader instance only. Use for services that must run exactly once (e.g. a Celery beat scheduler).
cache No — Edge content caching configuration. See Content Caching.
~~domain~~ No — Ignored. Custom domains are managed with mecca domain add, never in this file.
~~domains~~ No [] Ignored. Custom domains are managed with mecca domain add, never in this file.

Service names become DNS labels

Your service name is interpolated into every URL the platform generates for that service — {app}-{service}-{group}.{env}.appmecca.net — and into the ALB routing rule behind it. It is therefore validated:

  • lowercase only. Web is refused. DNS is case-insensitive but the platform's internal lookups are not, so a capital letter used to produce a hostname that some code could generate and other code could never match.
  • no /, ?, #, spaces, dots or non-ASCII. These are not valid in a DNS label. A dot would inject an extra label into your hostname.
  • no leading or trailing hyphen, and no --. These are stripped or collapsed when the hostname is built, so the service would resolve at a name different from the one you declared.
  • underscores are allowed only on a service that gets no URL — one with no port and no static build. The platform's own internal-service convention uses them (cron_nightly, _internal-api), and such a service never becomes a DNS label. Add a port to it and the name must drop the underscore, because the load balancer's host rule rejects one.

Good (any service): web, api, status-page, auth-service. Good (workers, no port): cron_nightly, _internal-api. Refused: Web, a/b, web-, a--b, a.b, my service — and cron_nightly if you give it a port.

⚠ If you have an existing app whose service name breaks one of these rules, it kept working until now because the name was passed through unvalidated. Its URL contains the sanitised form, and renaming the service in appmecca.yaml to that sanitised form is the fix — the URL does not change.

Unknown keys are silently ignored

A key AppMecca does not recognise is dropped with no error and no warning. A misspelled helth_check:, a resource: (singular) block, or a field indented one level too far simply does nothing — and because health checking is off by default, a typo in health_check: leaves the service with no health check at all rather than failing loudly.

Service types

type decides whether your service runs as a container at all — it is the most consequential field in this table.

type Runs a container? Needs a Dockerfile? Served by
static No No Object storage behind the AppMecca edge cache
spa (with static_site:, no command:) No No Object storage behind the AppMecca edge cache
spa (with command:) Yes Yes Your file server (nginx, serve, …) behind the platform router
ssr, api, or unset Yes Yes Your process behind the platform router

A bare type: spa is rejected

type: spa with neither a command: nor a static_site: block is ambiguous — the platform cannot tell whether you want the static path or a container — and the deploy is refused at config validation. Add static_site: for the static path, or command: to force the container path.

type values are not checked

type is not validated against the list above. A typo such as type: statik or type: SPA is accepted silently and the service is treated as an ordinary container service — so a static site quietly tries to run a container it has no Dockerfile for. Spell it exactly: static, spa, ssr, api.

type: static rejects container-shaped fields

A type: static service must not declare port, replicas greater than 1, drives, command, or health_check. None of them mean anything without a container, and the config is refused at validation time listing exactly which field offended.

SPA Runtime Config Injection

Static SPA containers (React, Vue, SvelteKit adapter-static) serve pre-built JavaScript bundles via nginx or a similar file server. These servers don't inject runtime environment variables into the bundle.

When you set type: spa on a service, AppMecca generates a __appmecca_config.js file at deploy time with all resolved environment variables and volume-mounts it into the container:

deploy:
  - name: frontend
    container: frontend
    type: spa
    spa_root: /usr/share/nginx/html   # Where nginx serves files from
    port: 80
    command: 'nginx -g "daemon off;"'  # disambiguation — see below
    environment:
      PUBLIC_API_URL: "${service:backend}"   # the PUBLIC_ prefix is required — see below

  - name: backend                      # must exist — ${service:backend} refers to it
    container: backend
    port: 8080

${service:X} must name a service you declared

The reference is checked at config validation. Pointing it at a service that is not in your own deploy: list fails the deploy immediately with a message listing the services you do have.

Only prefixed variables reach the bundle

A variable is written into __appmecca_config.js only if its name starts with PUBLIC_, VITE_, NEXT_PUBLIC_ or REACT_APP_. Anything else is dropped on purpose, so a stray DATABASE_URL can never be published to every visitor's browser.

The drop is silent — there is no error and no warning in your build. A service that sets a bare API_URL and then reads window.__APPMECCA_CONFIG__?.API_URL gets undefined, falls through to whatever default the code has (usually localhost), and fails only in the browser, in production.

Name the variable PUBLIC_API_URL (or VITE_API_URL, matching your bundler) and read it back under that same name. Variables containing PASSWORD, SECRET, TOKEN or KEY are excluded even with a safe prefix.

Pick a path: static OR container

A bare type: spa service with NO command: AND NO static_site: block is ambiguous and rejected at config validation time. The platform can't tell whether you want:

  • Static deploy (object storage + edge cache + __appmecca_config.js) — add a static_site: block (top-level or per-service) declaring build_command + output_dir.
  • Containerised SPA (regular container, runtime config via __appmecca_config.js) — add command: to force the container path, OR drop type: spa entirely (PUBLIC_* env vars get injected into every container regardless of type).

The yaml above uses command: to force the container path — the most common shape for nginx-served SPAs that need runtime config injection.

Edge coverage is declared once, for the whole app

Static sites and container services share one CloudFront distribution, so coverage is not a per-service setting — it lives in a top-level cdn: block:

cdn:
  coverage: extended

The default (standard) has no African, Asian or South American edge, so if your readers are outside North America and Europe, set extended. See Edge coverage for what each level covers and what it costs. It converges on your next deploy, in both directions.

Deep links work automatically on static SPA deploys

A type: spa service deployed the static way (with a static_site: block) serves client-side-routed deep links such as /dashboard/settings correctly out of the box: unmatched paths fall back to your bundle's entry document, including during the first-deploy edge warm-up window. You do not need to set spa_fallback yourself.

If your generator doesn't emit index.html

Some static generators name the SPA shell something other than index.html. SvelteKit's adapter-static({ fallback: '200.html' }), for example, writes 200.html and emits no index.html at all when the / route isn't prerendered (e.g. it has export const prerender = false).

Declare that filename as the error_document and turn on spa_fallback:

deploy:
  - name: web
    type: static
    static_site:
      build_command: npm run build:static
      output_dir: build
      spa_fallback: true
      index_document: "index.html"   # preferred, if your build emits it
      error_document: "200.html"     # the shell your build actually writes

On an SPA (spa_fallback: true) the platform resolves the entry document against what the build actually produced: index_document when it exists, otherwise error_document. The resolved file becomes the edge cache's root object and the target of its 403/404 → 200 remap, so / and every deep link serve correctly. Setting index_document directly to 200.html works too.

If neither file exists the deploy fails during artifact verification with a message listing exactly what your build did produce.

If your generator emits about.html instead of about/index.html

Static generators split into two camps. Some write the directory form — about/index.html — and some write the flat form — about.html. SvelteKit's adapter-static, Astro, and Jekyll with pretty_urls: false all produce the flat form.

Object storage serves a key, not a path, so /about only resolves when about/index.html exists. Set flat_html_urls: true and the build writes that second key for every flat page:

deploy:
  - name: web
    type: static
    static_site:
      build_command: npm run build
      output_dir: build
      flat_html_urls: true    # about.html -> also about/index.html
      spa_fallback: false     # required — see below

Your root document and error document are never duplicated: / is already served, and /404 should not become a real page.

flat_html_urls and spa_fallback are mutually exclusive

Setting both is refused at config validation, and the reason is worth understanding because the failure it prevents is invisible.

spa_fallback: true maps every miss to your shell document at status 200. So on a prerendered site it does not fix deep links — it hides that they are broken: /about returns 200, the browser renders the page via client-side routing, and you never see the prerendered HTML the build produced. Search engines index the shell, and first paint is slower.

The rule is simple:

Your site Setting
Prerendered pages, flat filenames flat_html_urls: true, spa_fallback: false
Prerendered pages, directory filenames neither — it already works
Genuinely client-routed SPA (routes exist only in JS) spa_fallback: true

⚠ type: spa turns on SPA fallback implicitly, so it is refused with flat_html_urls too. A prerendered build wants type: static.

Per-path cache control (static services)

A type: static bundle is uploaded to object storage and served through the AppMecca edge. Without cache_control, every object gets the same cache lifetime — one day. That is wrong in both directions: a hashed, content-addressed asset that can never change is re-fetched 365 times more often than it needs to be, and your HTML is held for a day when you probably want a deploy to be visible immediately.

cache_control sets the Cache-Control header on the objects themselves, so it is correct for the edge and for a direct read of the file:

deploy:
  - name: web
    type: static
    static_site:
      build_command: npm run build
      output_dir: build
      default_max_age: 300          # everything not matched below
      cache_control:
        - path: /_app/immutable/*   # hashed filenames — safe to pin for a year
          max_age: 31536000
          immutable: true
        - path: /images/*
          max_age: 86400
        - path: /*.html             # always revalidate
          max_age: 0
Field Required Default Description
path Yes — Glob matched against your built files, e.g. /_app/immutable/*. A shell-style glob, not a regex.
max_age Yes — Cache lifetime in seconds. 0 is valid and means "always revalidate".
immutable No false Adds the immutable directive — the browser will not revalidate at all within max_age.
public No true public (shared caches may store it) or private.

default_max_age applies to everything no rule matched. Leaving it out keeps today's behaviour exactly.

immutable is only safe for content-addressed filenames

It tells the browser never to check for a new version. That is correct for app.a1b2c3.js, whose name changes when its bytes change, and wrong for app.js, which users would keep for a year after you fixed it.

Declaring immutable: true with max_age: 0 is refused — "never revalidate, for zero seconds" is contradictory, and it is almost always a mistake rather than an intent.

Rules are matched most-specific-first

When two globs both match a file, the one with the longer literal prefix wins — /_app/immutable/* beats /_app/* beats /*. Two rules on the same path are refused rather than silently resolved.

cache.behaviors does nothing on a static service

The cache: block with its behaviors list applies to container services only. On a type: static service it parses and validates and is then ignored, because your bundle is never served through a container. cache_control is the equivalent for static services.

Redirects

A type: static service can declare 301/302 redirects that are answered at the edge, before your files are touched:

deploy:
  - name: web
    type: static
    static_site:
      build_command: npm run build
      output_dir: build
      redirects:
        - from: /pricing
          to: /engagements          # 301 by default
        - from: /leadership
          to: /about#team
          prefix: true              # also /leadership/ and /leadership/bio
        - from: /beta
          to: /
          status: 302               # temporary
Field Required Default Description
from Yes — Request path to match. Must start with /.
to Yes — Destination — a path, a path with a fragment, or an absolute URL.
status No 301 301 (permanent) or 302 (temporary).
prefix No false Match from and everything beneath it, at path-segment boundaries.

Rules are tried in the order you write them and the first match wins, so put your most specific rules first.

Redirects are checked before the directory-index rewrite

This ordering is the whole mechanism, and it is why a redirect on an extensionless path works at all.

/pricing has no file extension, so the edge would normally rewrite it to /pricing/index.html to find your page. If redirects ran after that, a rule written on /pricing could never match — you would deploy it, see no error, and it would silently never fire. Redirects are evaluated first.

prefix: true stops at a path segment, not at a character

from: /leadership with prefix: true matches /leadership, /leadership/ and /leadership/bio — but not /leadershipfoo, which is a different page rather than a child of that one.

A fragment in to is carried through

to: /about#team sends Location: /about#team. Fragments are resolved by the browser, never sent to a server, so this looks like it should be stripped and is correct as written.

Capped at 20 rules / 1024 bytes — and an over-cap config is refused

Redirect rules are stored per hostname at the edge, in a slot the CDN caps at 1 KB. Your rules are encoded compactly (about 40 bytes each, so roughly 25 short rules fit), and a config that would not fit is rejected at validation time, naming both limits.

It refuses rather than dropping the rules that do not fit: a silently truncated redirect is a live 404 nobody is told about.

Redirects apply to every hostname the app answers on — its platform URL and each of its custom domains. Removing the redirects: block and redeploying clears them.

A page that does not exist serves your error_document, at 404

On a non-SPA static site, a request that resolves to no object serves the file you named as error_document (default 404.html) with status 404 — not a 200, so search engines do not index missing pages, and not a raw storage error.

This holds on both paths your site is served over: the CDN in the steady state, and the platform's own origin during the first-deploy window and for the whole life of a visibility: private app. If your build does not emit the file you named, you get a plain 404 instead — the deploy is not failed for it.

⚠ Before this was fixed (BUGS #711/#714) neither path served it: the CDN returned raw AccessDenied XML and the origin returned 502. If you are looking at an older environment and seeing either of those for a missing page, that is what you are seeing.

Then add one script tag to your index.html:

<script src="/__appmecca_config.js"></script>

Your JavaScript can read the variables from window.__APPMECCA_CONFIG__:

const apiUrl = window.__APPMECCA_CONFIG__?.PUBLIC_API_URL || "http://localhost:8080";

The spa_root field defaults to /app/build. Set it to match where your static file server serves from (e.g., /usr/share/nginx/html for nginx).

When to use type: spa

Only set type: spa for frontends served by a static file server (nginx, serve, etc.). If your app runs a Node.js server (e.g., SvelteKit with adapter-node), it can read environment variables normally and doesn't need this.

SPA memory minimum: 32 MB

type: spa services need at least 32 MB of memory. AppMecca will refuse the deploy at pre-flight if resources.memory resolves below that.

Why: the nginx master + worker + Docker's periodic HEALTHCHECK wget collectively use ~15–20 MB under load. Below 32 MB the kernel OOM-killer targets the nginx worker (not PID 1, the master). Because the master survives, Docker's State.OOMKilled flag stays false and the platform's OOM detector never records the kill — but your service starts returning 503 as soon as the worker dies.

The fix is to bump resources.memory to 32m (or higher if your SPA includes large bundled assets):

deploy:
  - name: web
    container: web
    type: spa
    command: 'nginx -g "daemon off;"'   # serves the built bundle from a container
    port: 80
    resources:
      memory: 32m

Worker-level OOM kills (where the container keeps running but the worker died) are recorded too. Run mecca debug <app> to see recent kills.

Services With and Without Ports

This is an important concept:

  • Services with a port get a public URL. AppMecca configures the platform HTTPS router to send traffic to them. These are your web servers, APIs, and frontends.

  • Services without a port are internal only. They don't get a URL or any web traffic. These are your background workers, task queues, and cron jobs.

deploy:
  # This gets a public URL
  - name: web
    container: myapp
    port: 8000
    health_check:
      path: /health

  # This does NOT get a URL — it's a background worker
  - name: worker
    container: myapp
    command: "celery -A myapp worker -l info"

Understanding Ports

The port field is the container port — the port your application listens on inside the Docker container. This is the port in your app's configuration (e.g., gunicorn --bind 0.0.0.0:8000 or app.listen(3000)).

You do not need to worry about host ports. AppMecca automatically allocates a host port (from the range 7001-8999) and maps it to your container port. The platform router handles the rest.

User → HTTPS → AppMecca router → host port (auto) → container port (your config)

Match your app's port

The port in appmecca.yaml must match the port your application actually listens on. If your Dockerfile has CMD ["gunicorn", "--bind", "0.0.0.0:8000"], then set port: 8000.

Linux capabilities and root-dropping servers

Every container runs with all Linux capabilities dropped (--cap-drop ALL) plus no-new-privileges. Most applications never notice: a Python, Node, Go or Java process that simply binds a port and serves requests needs none of them.

Some web servers do. nginx (and images built on it) start as root, bind the port, chown their cache directories, then drop to an unprivileged user. That sequence needs capabilities, so a limited set is restored in two cases:

Your service What is restored
port: 80 or port: 443 NET_BIND_SERVICE, CHOWN, SETUID, SETGID, DAC_OVERRIDE
Any other port:, if your image runs as root and declares an EXPOSE CHOWN, SETUID, SETGID
No port: at all (workers, cron, queues) Nothing — they never need it

NET_BIND_SERVICE is only granted on 80/443 because it exists solely to bind ports below 1024; above that it would be surface for nothing.

If your container exits with Operation not permitted

A message like nginx: [emerg] chown("/var/cache/nginx/client_temp", 101) failed (1: Operation not permitted) is a dropped capability — even though the process reporting it is running as root, which makes the message read as nonsense.

The most robust fix is to not need the capability at runtime: give your image a non-root USER, and pre-create and chown any cache directories at build time in the Dockerfile. Failing that, declare the port your server actually listens on.

mecca deploy preflight does not apply this hardening

The local preflight starts your image without the capability baseline, so an image that hits this will start cleanly there and still fail on deploy. Preflight remains the right tool for "does my app boot against real data" — it just cannot catch this particular class.

⚠ mecca deploy preflight is not yet available on the mecca you installed from npm or Homebrew. The command exists, but it refuses and prints the fallback rather than running:

pipx run appmecca deploy preflight <app>

So the note above describes what preflight does when you run it through the Python CLI. See FEATURES #366 for the port.

Health Checks

Any service with a port should declare a health check. This is how AppMecca knows your app is working and when a new container is ready to take traffic.

health_check:
  path: /health
  interval: 30
  timeout: 5
  healthy_threshold: 2
  unhealthy_threshold: 3
  startup_timeout: 120
Field Default Range Description
path /health — HTTP path to check.
interval 30 5-300 Seconds between checks.
timeout 5 2-120 Seconds before a check times out.
healthy_threshold 2 2-10 Consecutive successes to mark healthy.
unhealthy_threshold 3 2-10 Consecutive failures to mark unhealthy.
startup_timeout 120 30-600 Max seconds to wait for the container to pass health checks during deployment.

Omitting health_check: turns health checking OFF

There is no "apply the defaults" fallback. A service with no health_check: block gets no health check — which is what you want for a background worker, and almost never what you want for something serving HTTP. The defaults in the table apply only to fields you leave out within a block you did write.

The thresholds have a minimum of 2

healthy_threshold: 1 and unhealthy_threshold: 1 are rejected at config validation — the accepted range for both starts at 2. If you want a service marked healthy faster, lower interval instead.

Connection draining (drain_delay_seconds)

When a deploy replaces your service (blue/green swap), the old container is taken out of the router's rotation and in-flight requests get a graceful window to complete before it is removed. The platform default is 30 seconds — plenty for request/response traffic. Streaming responses, WebSockets, and long-polls that must survive a deploy can raise it per service:

deploy:
  - name: stream
    container: stream
    port: 8080
    drain_delay_seconds: 180   # optional; default 30; range 0-300
Field Default Range Description
drain_delay_seconds 30 0-300 Seconds in-flight requests on the OLD container survive after a deploy swaps traffic to the new one. New requests always go to the new container immediately.

Raising this makes each redeploy of the service take proportionally longer to finish cleaning up (the platform waits out the drain before removing the old container). The 300-second ceiling is enforced.

Slow-starting runtimes

Java/Spring Boot and .NET apps often need 3-5 minutes to start (JVM warmup, Spring context initialization, EF Core migrations). Set startup_timeout: 300 for these. Python, Ruby, and Go apps typically start in under 60 seconds and work fine with the default.

Your health check endpoint must:

  • Return HTTP 200 when the app is healthy
  • Respond within the timeout period
  • Be lightweight (don't do heavy database queries)

Simple health check examples

Python (Flask):

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

Node.js (Express):

app.get("/health", (req, res) => {
  res.json({ status: "healthy" });
});

Go:

http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"status":"healthy"}`))
})

Command Override

The command field overrides the CMD in your Dockerfile. This is useful when you want to run multiple services from the same image:

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

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

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

All three services use the same Docker image (myapp) but run different commands.

Environment Variables

You can set environment variables for each service directly in appmecca.yaml:

deploy:
  - name: web
    container: myapp
    port: 8000
    environment:
      DJANGO_SETTINGS_MODULE: myapp.settings.production
      LOG_LEVEL: info
      WORKERS: "4"

Don't put secrets here

The environment section in appmecca.yaml is committed to your Git repository. Don't put passwords, API keys, or other secrets here. Use mecca var set for sensitive values instead. See Environment Variables for details.

Inter-service URL templates

If your app has multiple services that need to talk to each other, you can use template variables:

deploy:
  - name: frontend
    container: frontend
    port: 3000
    environment:
      API_URL: "${service:backend}"

  - name: backend
    container: backend
    port: 8080

The ${service:backend} template resolves to the public HTTPS URL of the backend service (e.g. https://myapp-backend-acme.dev.yourdomain.io).

Prefer the plain ${service:name} form

There is also a ${service:name:internal} variant that resolves to a direct container-to-container address. It is currently unreliable: the address it resolves to stops working once the service you are calling is next redeployed. Use the plain form, or declare both and fall back — see Environment Variables.

See Inter-Service Communication for the full template reference.

Resource Limits

You can optionally cap a service's memory:

deploy:
  - name: web
    container: myapp
    port: 8000
    resources:
      memory: 512m
Field Description Examples
memory Maximum memory. A unit suffix is mandatory — k, m or g. 256m, 512m, 1g, 2g
cpu Accepted by the schema but has no effect — see below. —

memory without a unit means BYTES

The value is a size string, not a number of megabytes. memory: 512 is 512 bytes, not 512 MB — the container will be killed the instant it starts. Always write the suffix: 512m.

mb / gb are not recognised either: 512mb fails to parse, so the platform's memory-floor checks silently skip the service and the raw string is handed to the container runtime, which rejects it at launch. Use the single-letter form.

resources.cpu currently does nothing

cpu is accepted by the config schema — it will not fail validation — but no CPU limit is ever applied to your container. Setting it has no effect on how much CPU your service can use. Only memory is enforced today.

If you don't set resources.memory, the platform picks a default based on what it detects your image actually runs — it inspects the built image rather than applying one flat number:

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

So a Spring Boot service gets 512 MB without you asking — the detection exists because a JVM given 128 MB dies during boot, and that failure used to look like a broken app rather than a memory limit. You can still set resources.memory explicitly, and an explicit value always wins.

When a container exceeds its memory limit, Docker kills it (OOM kill). The platform detects this, records the event, and shows a warning in mecca app show. The host itself stays alive — only the offending container is killed.

Java / .NET apps

The JVM and .NET CLR allocate significant heap memory at startup. If your app is getting OOM-killed, increase the memory limit:

resources:
  memory: 512m   # Recommended minimum for Java/Spring

The validator emits a warning at mecca register time when a service's command looks JVM-shaped (java …, java … .jar, mvn …, gradle/./gradlew) and resources.memory is set below 512m (or unset — falls back to the 128m default). Advisory only — the deploy still proceeds if you know what you're doing.

If a JVM deploy fails anyway, the agent now reports the specific cause instead of a generic "Connection refused":

Container died during startup: ExitCode=137, OOMKilled=true, FinishedAt=…
ExitCode=137 (SIGKILL) or OOMKilled=true means kernel OOM — the fix is always to raise resources.memory.

Custom Domains

Custom domains are additive — your service remains accessible on the system-generated URL AND your custom domain(s).

Custom domains are managed exclusively via CLI or API — not via appmecca.yaml. The domain and domains fields in the schema are ignored during deployment because the certificate lifecycle (issuing the certificate, validating it against DNS you control, attaching it to the router) requires steps that cannot be driven from a config file.

Declaring domain: in this file does nothing

It is not an error — the deploy succeeds and the domain is simply never created. Use mecca domain add instead.

# Add a custom domain
mecca domain add myapp www.example.com -s web -e dev

# Verify certificate + auto-deploy
mecca domain verify myapp www.example.com -e dev

# List all custom domains
mecca domain list myapp -e dev

# Remove a custom domain
mecca domain remove myapp www.example.com -e dev

After adding a domain, you must create DNS records at your provider. AppMecca issues and renews the TLS certificate for you. See Domain Routing for the full workflow.

Domain Routing

AppMecca automatically creates a URL for every service that declares a port:

https://{app}-{service}-{group}.{env}.{domain}

So an app myapp owned by group acme in the dev environment gets:

https://myapp-frontend-acme.dev.yourdomain.io
https://myapp-backend-acme.dev.yourdomain.io

The service name is always in the URL

There is no shorter myapp.dev.yourdomain.io form, not even for an app with a single service. Every service URL carries its own service name, so adding a second service later never changes the first one's URL.

Deploying a branch other than your production branch inserts the branch name too:

https://{app}-{branch}-{service}-{group}.{env}.{domain}

See Domain Routing for more details.

Next Steps