Configuration Hierarchy
Backstage's native config system merges multiple --config files in the order
they are supplied. The merge is deep (object keys are combined) and
last-wins on scalar values: if two files set the same leaf key, the later
file's value survives. DevPortal's entrypoint assembles that --config chain
at boot from base distribution files, preset-generated files, and your
overrides. Knowing the chain tells you exactly where to put a setting and why it
takes effect — or why it doesn't.
Quick answer — where do I put my override?
- Operator customizations (branding, catalog locations, integration credentials):
app-config.local.yaml(layer 5). - Auth provider and SCM integration (set once per environment): selected via
VEECODE_PRESETS, which generates the preset layer (layer 4). See Presets. - Plugin-specific backend config (Kubernetes cluster URLs, SonarQube base URL, etc.): also goes in
app-config.local.yaml, or is injected viapluginConfigindynamic-plugins.yaml(layer 6). See Composing a Portal for the relationship between plugin loading and backend config.
The precedence chain
Entries are listed lowest to highest priority. A file loaded later wins on any overlapping key.
| Order | File | When loaded |
|---|---|---|
| 1 | app-config.yaml | Base distribution defaults (shipped in the image) |
| 2 | app-config.production.yaml | Container / production overrides (shipped in the image) |
| 3 | app-config.distro.yaml | VeeCode distro defaults (~10 lines, escape hatch; shipped in the image) |
| 4 | app-config.preset-<name>.yaml | One per selected preset, in VEECODE_PRESETS order |
| 5 | app-config.local.yaml | Your operator overrides (volume-mounted app-config.local.yaml only) |
| 6 | dynamic-plugins-root/app-config.dynamic-plugins.yaml | Generated at boot from each enabled plugin's pluginConfig: |
| 7 | app-config.saas.yaml | Decoded from VEECODE_APP_CONFIG (base64); wins over all earlier layers |
Files 1–3 are always present inside the image. File 4 is emitted once per
preset that declares an appConfig: block, in VEECODE_PRESETS order. Files
5–7 are conditional — each is skipped if absent.
The source of truth is the EXTRA_ARGS construction in entrypoint.sh.
V2 has no app-config.<profile>.yaml layer. The profile slot (old layer 3) is
replaced by one file per selected preset (layer 4 above), assembled in
VEECODE_PRESETS order. See Presets.
Where app-config.local.yaml sits
app-config.local.yaml is your primary customization target. It loads at
position 5 — after every preset-generated config, so it wins over preset
fragments. You do not need to replicate anything from earlier files; supply
only the keys you want to override or add, and Backstage deep-merges the rest.
docker run \
-v $(pwd)/app-config.local.yaml:/app/app-config.local.yaml:ro \
veecode/devportal:2.1.3
Example: override a single preset value
Suppose the github preset enables the GitHub catalog provider at a 30-minute
refresh frequency and you want 5 minutes. In your app-config.local.yaml:
catalog:
providers:
github:
default:
schedule:
frequency: { minutes: 5 }
Because app-config.local.yaml loads after all preset files, your frequency
wins. You do not repeat the rest of the provider block — the deep-merge keeps it.
VEECODE_APP_CONFIG — config without a file mountIn deployments where you cannot mount a file (ArgoCD managing plain manifests,
a CI-injected environment, or the VeeCode SaaS), encode your operator config as
base64 and pass it as VEECODE_APP_CONFIG. The entrypoint decodes it into
/app/app-config.saas.yaml (position 7), which wins over everything — preset
configs, plugin configs, and any mounted local.yaml. Use it for
deployment-specific values (database URLs, ingress hosts, secret references)
that must not be hardcoded.