Skip to content

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 dev vs prod
  • Logging: Include the environment, service, and branch in log output
  • Cache key prefixing: Use CACHE_PREFIX to 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:

mecca var set my-blog -e dev -b feature/payments STRIPE_API_KEY=sk_test_xxx

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

mecca var list my-blog -e dev

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:

mecca var list my-blog -e dev -b feature/payments

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:

mecca var list my-blog -e dev -b main --dotenv > .env

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:

mecca var unset my-blog -e dev SECRET_KEY

For a specific branch:

mecca var unset my-blog -e dev -b feature/payments STRIPE_API_KEY

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:

mecca var copy my-blog -e dev --from main -b feature/payments

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: --skip keeps the destination's value, --overwrite replaces 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:

mecca var set my-blog -e dev NEW_API_KEY=abc123
mecca app deploy my-blog -e dev

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 to https://myapp-backend.dev.yourdomain.io
  • ${service:backend:internal} resolves to http://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:

mecca var set myapp -e dev LOG_LEVEL=debug    # Source 4: overrides Source 1

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