
Dify Docs Env Vars
- 3 installs
- 174 repo stars
- Updated August 4, 2026
- langgenius/dify-docs
Helps with ai & agent building tasks.
About
dify-docs-env-vars is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- dify-docs-env-vars
- AI & Agent Building
- AI-coding skill
Dify Docs Env Vars by the numbers
- 3 all-time installs (skills.sh)
- Ranked #13,657 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/langgenius/dify-docs --skill dify-docs-env-varsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 174 |
| Last updated | August 4, 2026 |
| Repository | langgenius/dify-docs ↗ |
What it does
Helps with ai & agent building tasks.
Files
Dify Environment Variable Documentation
Before Starting
Read these shared guides:
1. writing-guides/style-guide.md 2. writing-guides/formatting-guide.md 3. writing-guides/glossary.md
Source of Truth: docker/.env.example + docker/envs/**/*.env.example
After Dify PR #31586, the supported self-host knob surface is split across:
docker/.env.example— essential startup valuesdocker/envs/**/*.env.example— categorized optional vars (core-services, databases, infrastructure, security, vectorstores, middleware)
The verifier reads both. Pass --env-example docker/.env.example --env-example docker/envs (the second arg is a directory; the verifier globs **/*.env.example recursively).
| Var location | Action |
|---|---|
In any .env.example file, uncommented | Document. |
In any .env.example file, commented (#FOO=bar) | Document; add to Verifier false positives in ignored-vars.md (the verifier can't parse defaults from comments). |
Only in api/configs/ Pydantic, not in any .env.example | Don't document. Upstream-deferred; file a PR adding it to the appropriate .env.example file first. |
Removed from .env.example because the code no longer reads it | Remove from docs. Documenting unreferenced vars implies they still take effect. Discoverability for upgraders belongs in upstream Dify release notes, not this docs site. |
The verifier's "extra in docs" signal is not an escape hatch. Never suppress it for Pydantic-only vars via ignored-vars.md.
Four-Step Process
Pull the latest Dify code before tracing. In the Dify codebase directory:
git fetch origin && git checkout main && git pull origin mainThis process applies to every variable without exception. Do not skip variables because they seem "obvious" — every variable must be traced, explained, and described.
Step 1: Trace the Variable in the Codebase
Agent granularity: When using subagents for tracing, assign 3–5 related variables per agent.
Tracing depth depends on variable type:
- Python config variables (defined in
api/configs/): Full tracing — find definition, all usage locations, and behavior when empty vs set. - Frontend variables (mapped in
web/docker/entrypoint.sh): Trace fromentrypoint.shto find the Docker-to-NEXT_PUBLIC_*mapping, verify the default in bothdocker/.env.exampleandweb/.env.example, and check whether the variable is also used in Python code (dual-purpose). For Next.js-only variables (UI knobs likeMAX_TOOLS_NUM), light verification is sufficient. - Docker/container service variables (only in
docker-compose.yaml): Light verification — grep to confirm the variable is not used in Python code, then document from.env.examplecomments. - Plugin daemon variables (
PLUGIN_*not inapi/configs/): Document from.env.examplecomments.
For full tracing, search the Dify codebase:
1. Find the definition in api/configs/ — note the Pydantic field type, default, description, and any validation_alias (fallback) settings. 2. Find every usage — grep for both the env var name and the Python attribute (e.g., dify_config.VARIABLE_NAME). Read surrounding code to understand what each usage does. 3. Determine behavior when empty vs set — trace fallback chains and identify what features break.
Step 2: Write a Plain-Language Explanation
Write an explanation covering:
- What the variable actually does (in practical terms, not code terms)
- Specific features that depend on it (name them)
- What happens if left empty (what breaks, what falls back)
- What happens if set (what works)
- Key code locations (file paths, no line numbers — they shift)
Save to deep-dive.md (in this skill directory) under the appropriate section heading.
Step 3: Write the User-Facing Description
Transform the explanation into a concise documentation description. The description must:
- Lead with the practical impact, not the technical mechanism
- Name the features that require this variable (e.g., "Required for the Human Input node" not "used for frontend references")
- Explain what breaks if misconfigured (e.g., "If empty, email links will be broken")
- Mention fallback behavior if the variable has one (e.g., "falls back to
CONSOLE_API_URL") - Include relationships with other variables when relevant
- End with an example value for non-obvious variables
Step 4: Confirm with Reviewer
Present the proposed description to the user for review before editing the documentation file.
Document Structure
The env var doc is organized into three sections following docker/.env.example section order:
1. Backend (API + Worker) — Python API server and Celery worker variables. 2. Frontend (Web) — Next.js frontend variables. Uses <Tabs> to show Docker and source code variable names. 3. Infrastructure (Docker Compose / AWS AMI Only) — database, Redis, Nginx, and other container variables. Not applicable to source code deployments.
When to use tables: Groups of related, straightforward variables (connection settings, credentials, tuning knobs).
When to use individual headings: Important variables needing explanation — typically enum-type selectors (STORAGE_TYPE, VECTOR_STORE) or variables where the "why" matters (SECRET_KEY, FILES_URL).
When to use tabs: Frontend section variables where Docker and source code deployments use different variable names. Tabs cannot be placed inside table cells, so all tabbed variables require individual headings.
When to use accordions: Provider-specific configuration (storage backends, vector databases, mail providers) — users only need one provider.
Reader Persona
Same audience as en/self-host/ documentation (see dify-docs-guides skill): DevOps engineers and system administrators deploying Dify. Assume strong infrastructure knowledge.
Additional context for env var docs: Readers are actively configuring a deployment. They need to know what each variable does, when to change it, and what breaks if they get it wrong. They are not reading linearly—they are scanning for a specific variable.
Style Overrides
Rules specific to env var docs (override or extend the shared style guide):
- Use
(empty)for empty-string defaults, not""or blank - For empty defaults with a fallback:
(empty; falls back to X)or(empty; defaults to X) - Never include real or example secret keys — GitHub push protection blocks
sk-*patterns. Use descriptions like(pre-filled in .env.example; must be replaced for production)
Consistency over variety in reference tables. The general style guide says to vary sentence patterns. In reference tables, consistency aids scanning. Use predictable patterns for connection credentials (hostname, port, username, password) across providers. Vary descriptions only when variables genuinely differ in behavior or purpose.
Variable descriptions should be self-contained. The general style guide says not to restate the heading. Variable descriptions must state what the variable does—even if the name partially implies it. Not all variable names are self-explanatory, and users may arrive at a description via search without seeing the surrounding section context.
Include actionable technical mechanisms. The general style guide favors user outcomes over technical mechanisms. For env var docs, include technical mechanisms that help users configure, troubleshoot, or understand trade-offs—algorithm names, encoding behavior, fallback chains, version requirements. Exclude mechanisms that only describe code architecture—factory patterns, lazy imports, class names—unless understanding them is necessary for configuration.
- Keep: "URL-encoded in the connection string, so
@,:,%are safe to use", "HMAC-SHA256", "Requires Milvus >= 2.5.0", "Falls back toCONSOLE_API_URL" - Remove: "Dify's storage dispatcher lazily imports the selected backend", "Sends POST to /v1/sandbox/run with X-Api-Key header"
No specific recommended values for tuning parameters. For numeric tuning parameters without clear boundaries (connection pool sizes, worker counts, timeouts, buffer sizes), do not prescribe values. Describe the symptom that indicates the value needs changing: "If you experience connection rejections under load, try increasing this value." Exception: when a value has a well-established recommendation (e.g., PostgreSQL shared_buffers = 25% of RAM), include it with a reference link.
Description Anti-Patterns
| Anti-Pattern | Better |
|---|---|
| "Used for frontend references" | "Required for the Human Input node — form links in email notifications are built from this URL" |
| "The backend URL of the console API" | "Set this if you use OAuth login (GitHub, Google) or Notion integration — these features need an absolute callback URL" |
| "Upload file size limit, default 15" | "Maximum file size in MB for uploads" |
| Restating the code comment verbatim | Explaining when you'd change it and what happens if you don't |
Verification
Run after completing any documentation change:
python3 .claude/skills/dify-docs-env-vars/verify-env-docs.py \
--env-example <path-to-docker/.env.example> \
--docs <path-to-environments.mdx>The script reports:
- Missing from docs: Variables in
.env.examplenot yet documented (address over time) - Extra in docs: Variables documented but not in
.env.example(verify manually) - Default mismatches: Documented defaults that don't match
.env.example— must be zero before work is complete
Use .env.example defaults (what Docker Compose users actually get), not Pydantic code defaults.
Intentionally ignored variables
Some variables in .env.example are deliberately not documented (Cloud-only, experimental, or verifier false positives). The verifier reads these from ignored-vars.md (same directory) and filters them out. When you:
- Remove a variable from the docs as Cloud-only → add it under Cloud-only (SaaS) in
ignored-vars.md. - Skip documenting an experimental or internal flag → add it under Experimental / internal.
- Document a supported variable whose
.env.exampleentry is commented out (e.g.,#FOO=bar) → add it under Verifier false positives. This bucket is only for vars that exist in.env.examplein commented form. Do not use it to suppress verifier signal for vars that are absent from.env.exampleentirely — those are upstream-deferred (see Source of Truth) and must not be documented.
Every entry must include a source reference (PR, commit, or audit date).
Translation
The automated translation pipeline does not cover en/self-host/configuration/environments.mdx. After editing that English file, manually update zh/self-host/configuration/environments.mdx and ja/self-host/configuration/environments.mdx to match.
Post-Writing Verification
After completing the document, run the post-writing checks listed in writing-guides/index.md#post-writing-verification.
Intentionally Ignored Environment Variables
Variables listed here appear in Dify's docker/.env.example or api/configs/, but are deliberately not documented in en/self-host/configuration/environments.mdx. The verifier script reads this file and skips matching variables when comparing docs against .env.example.
When to update this list
Add an entry when you:
- Remove a variable from the docs because it only applies to Dify Cloud.
- Skip documenting a new variable because it's experimental, internal, or not user-tunable.
- Identify a verifier false positive (e.g., the variable is commented-out in
.env.examplebut documented because the code supports it).
Remove an entry when the reason no longer holds (e.g., an experimental flag graduates to a stable, user-facing feature).
Every entry requires: variable name, category, reason, and a source reference (commit, PR, or issue). This enforces traceability so later maintainers can audit the decision.
Format
The verifier parses the tables below. A line is treated as an ignore entry when it matches | \VARIABLE_NAME\ | .... Additional columns are informational.
---
Cloud-only (SaaS)
Meaningful only on the hosted Dify Cloud deployment; self-hosted users cannot use or benefit from them. Removing these from the self-host docs prevents confusion.
| Variable | Reason | Source |
|---|---|---|
ENABLE_WEBSITE_JINAREADER | Cloud UI feature flag for Jina Reader crawler. | PR #721, commit 9248032 |
ENABLE_WEBSITE_FIRECRAWL | Cloud UI feature flag for Firecrawl. | PR #721, commit 9248032 |
ENABLE_WEBSITE_WATERCRAWL | Cloud UI feature flag for WaterCrawl. | PR #721, commit 9248032 |
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX | Cloud-specific UI toggle. | PR #721, commit 9248032 |
TIDB_API_URL | TiDB Cloud control plane. | PR #721, commit 9248032 |
TIDB_IAM_API_URL | TiDB Cloud IAM control plane. | PR #721, commit 9248032 |
TIDB_PRIVATE_KEY | TiDB Cloud credential. | PR #721, commit 9248032 |
TIDB_PUBLIC_KEY | TiDB Cloud credential. | PR #721, commit 9248032 |
TIDB_PROJECT_ID | TiDB Cloud project reference. | PR #721, commit 9248032 |
TIDB_REGION | TiDB Cloud region. | PR #721, commit 9248032 |
TIDB_SPEND_LIMIT | TiDB Cloud billing guard. | PR #721, commit 9248032 |
TIDB_ON_QDRANT_URL | Hybrid TiDB-Qdrant Cloud-only backend. | PR #721, commit 9248032 |
TIDB_ON_QDRANT_API_KEY | Hybrid TiDB-Qdrant Cloud-only backend. | PR #721, commit 9248032 |
TIDB_ON_QDRANT_CLIENT_TIMEOUT | Hybrid TiDB-Qdrant Cloud-only backend. | PR #721, commit 9248032 |
TIDB_ON_QDRANT_GRPC_ENABLED | Hybrid TiDB-Qdrant Cloud-only backend. | PR #721, commit 9248032 |
TIDB_ON_QDRANT_GRPC_PORT | Hybrid TiDB-Qdrant Cloud-only backend. | PR #721, commit 9248032 |
CREATE_TIDB_SERVICE_JOB_ENABLED | Cloud-side TiDB pre-provisioning job. | PR #721, commit 9248032 |
AMPLITUDE_API_KEY | Cloud product analytics integration. | PR #721, commit 9248032 |
Experimental / internal
Feature flags for unfinished or staff-only features. Not yet meant for self-hosted tuning.
| Variable | Reason | Source |
|---|---|---|
EXPERIMENTAL_ENABLE_VINEXT | Switches the web container to an experimental Vite-based server (web/docker/entrypoint.sh). Not a supported user-facing knob. | 1.14 sync audit, 2026-04-22 |
Verifier false positives
The variable is documented in environments.mdx and supported by the backend, but the verifier reports it as missing from .env.example because the example entry is commented out.
| Variable | Reason | Source |
|---|---|---|
ALIYUN_CLOUDBOX_ID | Commented-out #ALIYUN_CLOUDBOX_ID=your-cloudbox-id in docker/.env.example; backend field exists in api/configs/middleware/storage/aliyun_oss_storage_config.py. | 1.14 sync audit, 2026-04-22 |
#!/usr/bin/env python3
"""
Verify Dify environment variable documentation against .env.example sources.
Parses one or more env-example files plus the docs MDX, extracts variable names
and defaults, and reports discrepancies.
After Dify PR #31586, env vars were split across `docker/.env.example` and
`docker/envs/**/*.env.example`. The verifier accepts either a single file or
a directory; passing a directory globs `**/*.env.example` recursively.
Usage:
python3 verify-env-docs.py --env-example PATH [--env-example PATH ...] --docs PATH
`--env-example` may be repeated and may point to either a file or a directory.
"""
import argparse
import re
import sys
from pathlib import Path
def parse_env_file(path: Path) -> dict[str, str]:
"""Parse a single env-example file and return {VARIABLE_NAME: default_value}."""
variables = {}
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
# Skip comments and empty lines
if not line or line.startswith("#"):
continue
# Match VARIABLE=value (value can be empty)
match = re.match(r"^([A-Z][A-Z0-9_]+)=(.*)", line)
if match:
name = match.group(1)
value = match.group(2).strip()
variables[name] = value
return variables
def collect_env_files(sources: list[str]) -> list[Path]:
"""Resolve each source (file or directory) into a list of env-example files.
Files are returned as-is. Directories are scanned recursively for any
file whose name ends with `.env.example` (matches both `.env.example`
and `<name>.env.example`).
"""
files: list[Path] = []
seen: set[Path] = set()
for source in sources:
p = Path(source)
if not p.exists():
print(f"ERROR: env source not found: {source}", file=sys.stderr)
sys.exit(1)
if p.is_dir():
for f in sorted(p.rglob("*.env.example")):
if f not in seen:
files.append(f)
seen.add(f)
# Also catch the bare `.env.example` filename, which rglob's
# `*.env.example` pattern does include via the leading wildcard,
# but be explicit for clarity.
for f in sorted(p.rglob(".env.example")):
if f not in seen:
files.append(f)
seen.add(f)
else:
if p not in seen:
files.append(p)
seen.add(p)
return files
def parse_env_example(sources: list[str]) -> tuple[dict[str, str], list[Path]]:
"""Parse all env-example files reachable from the given sources.
Later files override earlier ones for duplicate keys. Returns the merged
variable map plus the list of files actually parsed (for diagnostics).
"""
files = collect_env_files(sources)
merged: dict[str, str] = {}
for f in files:
merged.update(parse_env_file(f))
return merged, files
def parse_ignored_vars(path: str) -> set[str]:
"""Parse the ignored-vars markdown file and return the set of ignored names.
Table rows beginning with `| \`VARIABLE_NAME\` |` register an ignore entry.
The header row `| Variable | ...` is skipped naturally because it isn't backticked.
"""
ignored: set[str] = set()
if not Path(path).exists():
return ignored
with open(path, encoding="utf-8") as f:
for line in f:
match = re.match(r"^\|\s*`([A-Z][A-Z0-9_]+)`\s*\|", line)
if match:
ignored.add(match.group(1))
return ignored
def parse_mdx_docs(path: str) -> dict[str, str]:
"""Parse MDX documentation and extract documented defaults.
Handles two formats:
1. Table rows: | `VARIABLE` | `value` | description |
2. Heading + Default line:
### VARIABLE
Default: `value`
"""
variables = {}
with open(path, encoding="utf-8") as f:
lines = f.readlines()
i = 0
while i < len(lines):
line = lines[i].strip()
# Format 1: Table rows — | `VAR_NAME` | `default` | description |
table_match = re.match(
r"^\|\s*`([A-Z][A-Z0-9_]+)`\s*\|\s*(.*?)\s*\|", line
)
if table_match:
name = table_match.group(1)
default_cell = table_match.group(2).strip()
# Extract value from backticks if present
backtick_match = re.match(r"^`(.*)`$", default_cell)
if backtick_match:
variables[name] = backtick_match.group(1)
elif default_cell.startswith("("):
# (empty), (empty; falls back to...), (auto-generated), etc.
variables[name] = ""
else:
variables[name] = default_cell
i += 1
continue
# Format 2: ### VARIABLE_NAME followed by Default: `value`
heading_match = re.match(r"^###\s+([A-Z][A-Z0-9_]+)\s*$", line)
if heading_match:
name = heading_match.group(1)
# Look ahead for "Default:" line within next 5 lines
for j in range(1, 6):
if i + j >= len(lines):
break
next_line = lines[i + j].strip()
if not next_line:
continue
default_match = re.match(r"^Default:\s*(.+)", next_line)
if default_match:
raw = default_match.group(1).strip()
# Extract from backticks
bt = re.match(r"^`(.*)`", raw)
if bt:
variables[name] = bt.group(1)
elif raw.startswith("("):
variables[name] = ""
else:
variables[name] = raw
break
# Stop if we hit content that's not the default line
if next_line.startswith("#") or next_line.startswith("|"):
break
i += 1
continue
i += 1
return variables
PLACEHOLDER_PATTERNS = [
"your-", "your_", "YOUR-", "YOUR_",
"xxx", "difyai", "dify-sandbox",
"sk-9f73s", "lYkiYYT6", "QaHbTe77",
"testaccount", "testpassword", "difypassword",
"gp-test.", "gp-ab",
"instance-name",
"WVF5YTha",
]
PLACEHOLDER_EXACT = {
"dify", "password", "admin",
}
PLACEHOLDER_CONTAINS = [
"your-object-storage",
"xxx-vector",
]
def is_placeholder(value: str) -> bool:
"""Check if a value is a placeholder (not a real default)."""
v = value.strip()
if v in PLACEHOLDER_EXACT:
return True
for pattern in PLACEHOLDER_PATTERNS:
if v.startswith(pattern):
return True
for pattern in PLACEHOLDER_CONTAINS:
if pattern in v:
return True
return False
def normalize(value: str) -> str:
"""Normalize a value for comparison."""
v = value.strip().strip('"').strip("'")
# Normalize boolean representations
if v.lower() in ("true", "yes", "1"):
return "true"
if v.lower() in ("false", "no", "0"):
return "false"
# Normalize empty
if v in ("null", "None", "none", ""):
return ""
# Treat placeholder values as empty (they're not real defaults)
if is_placeholder(v):
return ""
return v
DEFAULT_IGNORED_PATH = Path(__file__).parent / "ignored-vars.md"
def main():
parser = argparse.ArgumentParser(
description="Verify env var documentation against .env.example"
)
parser.add_argument(
"--env-example",
required=True,
action="append",
help=(
"Path to a .env.example file or to a directory containing them. "
"May be repeated. When given a directory, the verifier globs "
"**/*.env.example recursively. Pass both `docker/.env.example` and "
"`docker/envs/` to capture the post-PR-#31586 layout."
),
)
parser.add_argument(
"--docs",
required=True,
help="Path to MDX documentation file (e.g., en/self-host/configuration/environments.mdx)",
)
parser.add_argument(
"--ignored",
default=str(DEFAULT_IGNORED_PATH),
help=f"Path to ignored-vars markdown file (default: {DEFAULT_IGNORED_PATH}).",
)
args = parser.parse_args()
if not Path(args.docs).exists():
print(f"ERROR: Documentation not found at {args.docs}")
sys.exit(1)
env_vars, env_files = parse_env_example(args.env_example)
doc_vars = parse_mdx_docs(args.docs)
ignored = parse_ignored_vars(args.ignored)
print(f"Parsed {len(env_vars)} variables from {len(env_files)} env-example file(s):")
for f in env_files:
print(f" - {f}")
print(f"Parsed {len(doc_vars)} variables from documentation")
print(f"Loaded {len(ignored)} ignored variables from {args.ignored}")
print()
# --- Check 1: Variables in .env.example but missing from docs ---
missing_from_docs = sorted(
(set(env_vars.keys()) - set(doc_vars.keys())) - ignored
)
if missing_from_docs:
print(f"=== MISSING FROM DOCS ({len(missing_from_docs)}) ===")
for name in missing_from_docs:
print(f" {name}={env_vars[name]}")
print()
# --- Check 2: Variables in docs but not in any env-example source ---
extra_in_docs = sorted(
(set(doc_vars.keys()) - set(env_vars.keys())) - ignored
)
if extra_in_docs:
print(f"=== IN DOCS BUT NOT IN ANY .env.example ({len(extra_in_docs)}) ===")
for name in extra_in_docs:
print(f" {name} (doc default: {doc_vars[name]!r})")
print()
# --- Check 3: Default value mismatches ---
common = sorted(set(env_vars.keys()) & set(doc_vars.keys()))
mismatches = []
for name in common:
env_val = normalize(env_vars[name])
doc_val = normalize(doc_vars[name])
if env_val != doc_val:
mismatches.append((name, env_vars[name], doc_vars[name]))
if mismatches:
print(f"=== DEFAULT MISMATCHES ({len(mismatches)}) ===")
for name, env_val, doc_val in mismatches:
print(f" {name}:")
print(f" .env.example: {env_val!r}")
print(f" documentation: {doc_val!r}")
print()
# --- Summary ---
total_issues = len(missing_from_docs) + len(extra_in_docs) + len(mismatches)
if total_issues == 0:
print("ALL CHECKS PASSED — documentation matches .env.example")
else:
print(f"TOTAL ISSUES: {total_issues}")
print(f" Missing from docs: {len(missing_from_docs)}")
print(f" Extra in docs: {len(extra_in_docs)}")
print(f" Default mismatches: {len(mismatches)}")
return 1 if total_issues > 0 else 0
if __name__ == "__main__":
sys.exit(main())