Environment Variables¶
Every container deployed with AppMecca receives environment variables from multiple sources. Understanding these sources and their priority is essential for configuring your application correctly, especially when dealing with secrets, inter-service communication, and database credentials.
Known limitation — internal URLs go stale after the target service redeploys
An internal URL carries the host port the target service was using when your container started. That port changes every time the target service is redeployed (a new container is brought up on a different port before the old one is removed), so the URL stops working once the service you are calling is next deployed. It is fixed again by redeploying the calling service — until the next time.
Until this is resolved, declare both URLs and fall back: if your app also
holds ${service:name}, it will keep working over the public route (a little
slower) instead of failing. An app that declares only the :internal form
will get connection-refused.
Tracked as a platform bug; a proper fix needs a fixed internal address that survives a redeploy.
The Four Sources¶
Environment variables come from 4 sources, listed here from lowest to highest priority:
| Priority | Source | How to set | Best for |
|---|---|---|---|
| 1 (lowest) | appmecca.yaml |
deploy[].environment section |
Non-sensitive config committed to your repo |
| 2 | Auto-generated | Enabled via database.enabled / cache.enabled |
Database credentials, Redis URLs, inter-service URLs |
| 3 | System variables | Automatic (always injected) | Knowing which environment/app/service is running |
| 4 (highest) | CLI / Dashboard | mecca var set |
Secrets, API keys, per-environment overrides |
Higher-priority sources override lower ones. For example, if you define DATABASE_URL in appmecca.yaml and also have database.enabled: true, the auto-generated value wins. If you then run mecca var set DATABASE_URL=custom://..., the CLI value wins over everything.
The golden rule
Put non-sensitive configuration in appmecca.yaml (committed to Git). Put secrets and API keys in mecca var set — AppMecca stores those securely on the platform, never in your repository.
Source 1: appmecca.yaml (Lowest Priority)¶
Define environment variables in the environment section of each service in your appmecca.yaml:
deploy:
- name: web
container: myapp
port: 8000
environment:
DJANGO_SETTINGS_MODULE: myapp.settings.production
LOG_LEVEL: info
WORKERS: "4"
ALLOWED_HOSTS: ".yourdomain.io"
These values are committed to your Git repository, so they should contain only non-sensitive configuration.
Never put secrets in appmecca.yaml
This file is committed to Git. Passwords, API keys, tokens, and other secrets should be set via mecca var set instead.
Per-Service Variables¶
Each service gets its own set of variables. This is useful when services need different configuration:
deploy:
- name: web
container: myapp
port: 8000
environment:
WORKERS: "4"
LOG_LEVEL: info
- name: worker
container: myapp
command: "celery -A myapp worker"
environment:
CONCURRENCY: "8"
LOG_LEVEL: warning
The web service gets WORKERS=4 and LOG_LEVEL=info. The worker service gets CONCURRENCY=8 and LOG_LEVEL=warning. They do not share these variables.
Auto-generated and system variables are shared
Variables from sources 2, 3, and 4 (auto-generated, system, CLI) are injected into all services. Only source 1 (appmecca.yaml) is per-service.
Source 2: Auto-Generated Variables¶
AppMecca automatically generates environment variables when you enable features like database, cache, or inter-service communication.
Database Variables¶
When database.enabled: true in your appmecca.yaml, these 6 variables are injected into all services:
| Variable | Example Value |
|---|---|
DATABASE_URL |
postgresql://am_9f3c1a7b42de_user:abc123@db-host:5432/am_9f3c1a7b42de_db |
DB_HOST |
db-host.internal |
DB_PORT |
5432 |
DB_NAME |
am_9f3c1a7b42de_db |
DB_USER |
am_9f3c1a7b42de_user |
DB_PASSWORD |
abc123... |
Database and user names are opaque
They are generated hashes derived from your app and branch — never
{your_app}_db. Always read them from these variables rather than assuming a name.
See Database for the full database guide.
Cache Variables¶
When the top-level cache.enabled: true, these 2 variables are injected into all services:
| Variable | Example Value |
|---|---|
REDIS_URL |
redis://cache-host:6379/0 |
CELERY_BROKER_URL |
redis://cache-host:6379/0 |
See Cache for the full cache guide.
Inter-Service URL Variables¶
When your app has multiple services, AppMecca can resolve URLs between them using template syntax. These are defined in your appmecca.yaml and resolved at deploy time.
See the Template Syntax section below.
Source 3: System Variables¶
AppMecca injects these system variables into every container. These tell your application about its runtime context:
| Variable | Example Value | Description |
|---|---|---|
APPMECCA_ENV |
dev |
The environment name (e.g., dev, staging, prod) |
APPMECCA_APP |
my-blog |
Your application name |
APPMECCA_SERVICE |
web |
The service name from your appmecca.yaml |
APPMECCA_REGION |
(your environment's region) | The region the app is deployed in |
APPMECCA_BRANCH |
feature/payments |
The branch this container is deployed from |
CACHE_PREFIX |
my-blog:feature-payments: |
Prefix for cache keys (Redis isolation between branches) |
APPMECCA_SYSTEM_HOST |
my-blog-web-myteam.dev.appmecca.net |
This service's own public system hostname — the host part of the URL AppMecca routes to it |
APPMECCA_SYSTEM_HOST — the hostname your app is served on
A private (auth-gated) app's request reaches your container with an internal
Host header, so a framework that derives absolute URLs or checks allowed
hosts from Host can't see the public hostname. APPMECCA_SYSTEM_HOST gives
it to you directly — use it for Django ALLOWED_HOSTS, absolute-URL
generation, cookie domains, etc. (e.g. Django: ALLOWED_HOSTS = [os.environ["APPMECCA_SYSTEM_HOST"]]).
It is only set for services that have a public URL (an internal-only worker won't have it), and it is the system hostname only — if you also serve the app on a custom domain, add that domain to your allowed-hosts list yourself.
These are useful for:
- Conditional logic: Behave differently in
devvsprod - Logging: Include the environment, service, and branch in log output
- Cache key prefixing: Use
CACHE_PREFIXto avoid collisions in shared Redis between branches - Feature flags: Enable features only in certain environments
- Branch awareness: Know which branch your container is running from
Example usage in Python:
import os
if os.environ["APPMECCA_ENV"] == "production":
DEBUG = False
SECURE_SSL_REDIRECT = True
else:
DEBUG = True
SECURE_SSL_REDIRECT = False
Example usage in Node.js:
const isProduction = process.env.APPMECCA_ENV === "production";
const logLevel = isProduction ? "error" : "debug";
Source 4: CLI / Dashboard (Highest Priority)¶
Sensitive values like API keys, third-party tokens, and secret keys should be set via the CLI. AppMecca stores these securely on the platform and they are never committed to your repository.
Setting Variables¶
# Set a single variable
mecca var set my-blog -e dev SECRET_KEY=your-secret-key-here
# Set multiple variables (one command per variable)
mecca var set my-blog -e dev STRIPE_API_KEY=sk_live_xxx
mecca var set my-blog -e dev SENDGRID_API_KEY=SG.xxx
Environment variables are stored per branch. To target one branch, pass -b:
Omitting -b writes to EVERY branch
-b is a selector, not a single-branch flag, and its default is all branches
the app is registered on — not main.
# Writes STRIPE_API_KEY to main, feature/payments, staging — everything
mecca var set my-blog -e dev STRIPE_API_KEY=sk_live_xxx
That is usually what you want for a shared value, and exactly what you do not
want for a production-only secret. Pass -b main to be sure.
The selector accepts a comma-separated list of glob patterns, so you can target a set of branches in one call:
mecca var set my-blog -e dev -b 'feature/*' API_BASE=https://staging.example.com
mecca var set my-blog -e dev -b main,staging LOG_LEVEL=warning
Every write prints the branches it touched.
See Branch Deployments for details.
Listing Variables¶
With no -b, this lists every branch, in per-branch sections. Values are shown in
plain text (this surfaces real secrets — run it where that's safe); --json gives
machine-readable output. List a specific branch's variables with -b:
Export to a .env file¶
--dotenv emits redirect-clean KEY=value lines, so you can pull an app's
variables — including platform-injected ones like the APPMECCA_MAILER_*
mailer config — straight into a local .env for development. It needs the
branch selector to resolve to a single branch, so pass -b:
The output loads with python-dotenv, shell source, and mecca var load;
values that need it are quoted. A one-line note on stderr (never in the
file) flags that docker --env-file reads quotes literally, and any value that
can't be represented in .env (a multi-line value, or a ' mixed with
shell-special characters) is omitted with a # … comment rather than silently
corrupted. --dotenv and --json can't be combined.
Removing Variables¶
To remove an environment variable:
For a specific branch:
The variable name is a positional argument -- it comes after all flags.
unset follows the same branch rule
With no -b, the variable is removed from every branch. Pass -b to remove it
from just one.
Copying Variables From Another Branch¶
A new branch starts with no variables. Copy them from a branch that already has them:
The command first shows what would happen, by key name only:
- New — keys the destination doesn't have. They are copied.
- Identical — same key, same value. Nothing to do.
- Conflicts — same key, different value. You choose:
--skipkeeps the destination's value,--overwritereplaces it with the source's.
Without --yes it asks you to confirm; in scripts (no terminal, or --json) pass --yes,
and pass --skip or --overwrite if there are conflicts, or the command stops with exit
code 2 and changes nothing.
You can also copy while registering a new branch, before its first build:
mecca register git@github.com:you/my-blog.git -e dev -b feature/payments --from-branch main --components vars
In the dashboard, use Copy from branch… on the app's Env tab, or the Start from section when you Deploy another branch.
Production is never overwritten
You can copy from the production branch, never into it. If registering a branch
would make it production — which happens on an app with no main branch and nothing
marked production, because the newest branch counts as production — the copy is
refused and the message tells you which mecca app set-production command to run
first.
Values are copied as they are
A value that names a branch-specific address (a hard-coded hostname, for example) is
copied unchanged. ${service:…} templates are fine — they resolve per branch at deploy.
When Do Changes Take Effect?¶
Variables set via mecca var set take effect on the next deployment. If your app is already running, redeploy to pick up the new values:
Overriding Auto-Generated Variables¶
Because CLI variables have the highest priority, you can override any auto-generated variable:
# Override the auto-generated DATABASE_URL with a custom one
mecca var set my-blog -e dev DATABASE_URL=postgresql://custom@external-host:5432/mydb
Override with care
Overriding auto-generated variables like DATABASE_URL means AppMecca's auto-provisioned database will be ignored. Only do this if you need to connect to an external database.
Template Syntax¶
Templates let you reference dynamic values in your appmecca.yaml environment section. They are resolved at deploy time.
Inter-Service URL Templates¶
Use these to connect services within the same application:
| Template | Resolves To | Example |
|---|---|---|
${service:name} |
External HTTPS URL | https://myapp-backend.dev.yourdomain.io |
${service:name:internal} |
Internal HTTP URL | http://backend.myapp.dev.appmecca.internal:8080 |
External URLs go through the platform's HTTPS router (public). Use these for browser-facing references and public API calls.
Internal URLs go directly between containers on the private network (HTTP, no TLS overhead). They are faster in principle, but see the limitation at the top of this page — prefer the external form unless you have measured a reason not to.
deploy:
- name: frontend
container: frontend
port: 3000
environment:
# Browser will call this URL (goes through the platform router, HTTPS)
PUBLIC_API_URL: "${service:backend}"
# Server-side rendering calls this URL (direct, faster)
INTERNAL_API_URL: "${service:backend:internal}"
- name: backend
container: backend
port: 8080
health_check:
path: /health
What these resolve to
For an app called myapp deployed to the dev environment on yourdomain.io:
${service:backend}resolves tohttps://myapp-backend.dev.yourdomain.io${service:backend:internal}resolves tohttp://backend.myapp.dev.appmecca.internal:8080
When to use external vs internal
Use external (${service:name}) when the URL will be used in a browser (JavaScript fetch calls, link hrefs, redirect URLs) — and, for now, for server-to-server calls too.
internal (${service:name:internal}) skips the router and TLS, so it is faster, but it currently goes stale after the service you are calling is redeployed. If you use it, declare the external URL alongside it and fall back.
Credential Templates¶
Use these to map auto-generated credentials to custom variable names:
| Template | Resolves To |
|---|---|
${database_url} |
Full PostgreSQL connection URL |
${db_host} |
Database hostname |
${db_port} |
Database port |
${db_name} |
Database name |
${db_user} |
Database username |
${db_password} |
Database password |
${redis_url} |
Redis connection URL |
These are useful when your framework expects specific variable names:
deploy:
- name: web
container: myapp
port: 8000
environment:
# Prisma expects this exact variable name
DATABASE_URL: "${database_url}"
# Your custom framework expects these
POSTGRES_HOST: "${db_host}"
POSTGRES_PORT: "${db_port}"
# Worker queue needs Redis under a different name
QUEUE_URL: "${redis_url}"
database:
enabled: true
redis:
enabled: true
You usually don't need credential templates
AppMecca already injects DATABASE_URL, DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, REDIS_URL, and CELERY_BROKER_URL automatically when the database or cache is enabled. Templates are only needed when you need to map these values to different variable names.
Build-Time vs Deploy-Time Resolution¶
Templates are resolved at two different stages:
| Template Type | Build Time | Deploy Time |
|---|---|---|
${service:name} (external) |
Resolved | Resolved |
${service:name:internal} |
Skipped (port not yet allocated) | Resolved |
${database_url}, ${db_*} |
Skipped | Resolved |
${redis_url} |
Skipped | Resolved |
Build time means during docker build, when the variable is passed as a --build-arg. This is useful for frontend apps that bake the API URL into the JavaScript bundle.
Variables set with mecca var set reach the build differently: as secrets, which a RUN step reads from /run/secrets/NAME and which are not stored in the image. To pass a non-secret stored variable as a build argument, list it in containers[].build_args_from_vars.
Production and staging: a changed baked variable needs a new commit
On environments whose images cannot be overwritten (production and staging), a rebuild of the same commit with a changed variable that your Dockerfile declares with ARG, or a changed secret it mounts, is refused: the build names the variable and nothing is deployed. Push a new commit (an empty one is enough: git commit --allow-empty -m "rebuild" && git push) and deploy again. Variables your Dockerfile does not declare are only read at run time, so changing them never needs a rebuild — except the proxy variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY and their lowercase forms), which Docker applies to a build without an ARG.
Deploy time means when the container starts. The variable is passed as a runtime environment variable via docker run -e.
Internal URLs at build time
Internal templates (${service:name:internal}) cannot be resolved at build time because the host port is not yet allocated. If you need an internal URL in your Docker build, use the external URL instead or restructure your code to read the URL at runtime.
Multiple Templates in One Value¶
A single environment variable value can contain more than one template. They all get resolved in place:
environment:
# Comma-separated list of origins — both templates resolve
CORS_ALLOWED_ORIGINS: "${service:svelte},${service:react}"
# Embedded in a longer string
ALLOWED_HOSTS: "localhost,${service:web},admin.example.com"
Unresolved Templates Fail the Deploy¶
If a template references a service that doesn't exist in your deploy: block (typo, stale config, wrong environment), the failure happens at config validation time — before the build is even queued:
Service 'web': environment variable 'PUBLIC_API_URL' references
unknown service '${service:svlete}'. Known services: [api, frontend, web].
validate_appmecca_yaml() runs on every mecca register, mecca build trigger, and appmecca config validate, so typos never reach the deploy phase. This is the primary guard as of 2026-04.
If the config slips through anyway (e.g. a deploy queued before validation landed), the deploy-time resolver catches it as a last-resort fail-fast. The message lists each offending token with a per-token hint and splits the known-services list in two, so you can tell at a glance whether the service is missing from public routing, from private service discovery, or is simply a typo:
Deploy failed: Unresolved template variable(s) in service 'web':
- ${service:svlete} (not in external map; closest match: 'svelte')
Known services:
External (ALB HTTPS): api, beat, react, svelte, web, worker
Internal (Private DNS): api, beat, react, svelte, web, worker
For ${service:<name>:internal} lookups, the hint also covers two common cases: the internal map is empty (private service discovery is not available in this environment — a platform issue, not a config issue), or the name is known externally but not internally (its private DNS record could not be created).
ALB / Private DNS in that output
Those labels are the platform's internal wording for the public router and the private service-discovery layer respectively. They are reproduced above exactly as the tool prints them so you can match what you see in your terminal.
Previously an unknown reference would silently ship the literal string ${service:svlete} into the container, causing cryptic framework-level errors downstream (e.g. Django's corsheaders.E013 complaining that ${service:svelte} is missing a scheme).
Self-references are allowed¶
A service can reference its own external URL: ${service:web} inside the web service resolves to https://myapp-web.dev.yourdomain.io. Useful when the container needs to know the public URL it's being served at — e.g. as a CSRF_TRUSTED_ORIGINS entry, or in a <meta> tag baked into the page.
Priority in Action¶
Here is an example showing how all 4 sources interact:
appmecca.yaml:
deploy:
- name: web
container: myapp
port: 8000
environment:
LOG_LEVEL: info # Source 1: appmecca.yaml
API_URL: "${service:backend}" # Source 1, resolved to Source 2
- name: backend # the service API_URL points at
container: myapp
port: 8080
database:
enabled: true # Source 2: auto-generates DATABASE_URL, etc.
CLI command:
What the container sees:
| Variable | Value | Winning Source |
|---|---|---|
LOG_LEVEL |
debug |
Source 4 (CLI overrides appmecca.yaml) |
API_URL |
https://myapp-backend-acme.dev.yourdomain.io |
Source 1 (template resolved from Source 2) |
DATABASE_URL |
postgresql://am_9f3c1a7b42de_user:xxx@db-host:5432/am_9f3c1a7b42de_db |
Source 2 (auto-generated) |
DB_HOST |
db-host.internal |
Source 2 (auto-generated) |
REDIS_URL |
(not set) | Cache not enabled |
APPMECCA_ENV |
dev |
Source 3 (system) |
APPMECCA_APP |
myapp |
Source 3 (system) |
APPMECCA_SERVICE |
web |
Source 3 (system) |
APPMECCA_REGION |
(your environment's region) | Source 3 (system) |
APPMECCA_BRANCH |
main |
Source 3 (system) |
CACHE_PREFIX |
myapp:main: |
Source 3 (system) |
Common Patterns¶
Django Production Settings¶
deploy:
- name: web
container: myapp
port: 8000
environment:
DJANGO_SETTINGS_MODULE: myapp.settings.production
ALLOWED_HOSTS: ".yourdomain.io"
LOG_LEVEL: info
database:
enabled: true
redis:
enabled: true
Then set secrets via CLI:
mecca var set myapp -e dev SECRET_KEY=your-django-secret-key
mecca var set myapp -e dev SENTRY_DSN=https://xxx@sentry.io/123
Next.js Frontend Calling a Backend API¶
deploy:
- name: frontend
container: frontend
port: 3000
environment:
NEXT_PUBLIC_API_URL: "${service:backend}"
- name: backend
container: backend
port: 8080
database:
enabled: true
NEXT_PUBLIC_ prefix
Next.js exposes variables prefixed with NEXT_PUBLIC_ to the browser. Because ${service:backend} resolves at build time, the API URL gets baked into your frontend JavaScript bundle.
Flask with External Services¶
deploy:
- name: web
container: flask-app
port: 5000
environment:
FLASK_ENV: production
database:
enabled: true
redis:
enabled: true
Then set third-party API keys via CLI:
mecca var set flask-app -e dev SECRET_KEY=flask-secret
mecca var set flask-app -e dev STRIPE_SECRET_KEY=sk_live_xxx
mecca var set flask-app -e dev MAILGUN_API_KEY=key-xxx
Sending email?
You do not need a third-party mail provider — set email: enabled: true in
appmecca.yaml and AppMecca injects credentials for its own mail relay. See
Email.
Next Steps¶
- Database -- Enable PostgreSQL and understand credential injection
- Cache -- Enable Redis and Celery support
- Services -- Configure ports, health checks, and commands
- Inter-Service Communication -- Deep dive into service URL templates