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¶
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.
Webis 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
portand 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 aportto 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 astatic_site:block (top-level or per-service) declaringbuild_command+output_dir. - Containerised SPA (regular container, runtime config via
__appmecca_config.js) — addcommand:to force the container path, OR droptype: spaentirely (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:
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:
Your JavaScript can read the variables from window.__APPMECCA_CONFIG__:
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
portget 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
portare 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.
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:
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
timeoutperiod - Be lightweight (don't do heavy database queries)
Simple health check examples
Python (Flask):
Node.js (Express):
Go:
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:
| 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:
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":
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:
So an app myapp owned by group acme in the dev environment gets:
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:
See Domain Routing for more details.
Next Steps¶
- Database — Add a PostgreSQL database
- Cache — Add Redis caching
- Environment Variables — Pass secrets and configuration
- Inter-Service Communication — Connect services to each other