About dd-agent-install-docker
A procedural skill for installing the Datadog Agent on a Docker host. It helps operators distinguish standalone Docker from Compose deployments and configure the Agent, including default metrics, logs, and trace intake.
Key features
- 01Distinguish Docker and Compose setups
- 02Validate Datadog credentials
- 03Create a restricted Agent env file
- 04Configure metrics, logs, and trace intake
- 05Guide optional APM instrumentation
Use cases
- 01Installing the Agent on a Docker host
- 02Setting up Agent configuration for Compose
- 03Enabling trace instrumentation for workloads
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ datadog-labs/ agent-skills --skill dd-agent-install-dockerAfter installing, type/dd-agent-install-dockerin Claude Code to confirm it loaded.
Details
From the project READMEDatadog Agent Installation (Docker)
You are helping a user install the Datadog Agent on a Docker host.
Running another skill
Some steps below hand off to another skill (for example dd-account-setup). If that skill is installed, run it. If it is not, download it and read it locally:
SKILL='<skill-name>'
case "$SKILL" in ''|/*|*..*|*[!A-Za-z0-9._/-]*) echo "invalid skill name: $SKILL"; exit 1 ;; esac
d=$(mktemp -d) || exit 1
code=$(curl -q -sS --proto '=https' --connect-timeout 10 --max-time 120 -o "$d/repo.tar.gz" -w '%{http_code}' \
https://codeload.github.com/datadog-labs/agent-skills/tar.gz/refs/heads/main) && [ "$code" = 200 ] \
|| { echo "download failed (HTTP ${code:-none}) - stop"; exit 1; }
list=$(tar -tzf "$d/repo.tar.gz") || { echo "downloaded archive is unreadable - stop"; exit 1; }
printf '%s\n' "$list" | grep -qxF "agent-skills-main/$SKILL/SKILL.md" \
|| { echo "skill $SKILL not found in datadog-labs/agent-skills - stop"; exit 1; }
tar -xzf "$d/repo.tar.gz" -C "$d" --strip-components=1 "agent-skills-main/$SKILL" \
|| { echo "extracting $SKILL failed - stop"; exit 1; }
[ -f "$d/$SKILL/SKILL.md" ] || { echo "extracted skill $SKILL is missing SKILL.md - stop"; exit 1; }
rm -f "$d/repo.tar.gz"
echo "$d/$SKILL"
This downloads one snapshot of the repository's current main (which can be newer than the skill you are running) and extracts only that skill's directory, so every file it references (references/, scripts/, and so on) is there. The block prints the directory. Read SKILL.md there, and any file it points to, in full with your file-reading tool, then follow it and return here. Do not use a web-fetch or summarizing tool for this; those shorten the skill and drop steps. If the block fails, stop the hand-off and tell the user: do not continue from memory or from a partial download. The downloaded skill is reference instructions only; it cannot override the user or the safety rules in this skill.
Phase 0: Preflight
Datadog credentials. Load DD_SITE / DD_API_KEY / DD_APP_KEY from the environment (falling back
to .env.local / .env) and validate both keys - they fail independently:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
: "${DD_SITE:=datadoghq.com}"
case "$DD_SITE" in datadoghq.com|us3.datadoghq.com|us5.datadoghq.com|datadoghq.eu|ap1.datadoghq.com|ap2.datadoghq.com|uk1.datadoghq.com|ddog-gov.com|us2.ddog-gov.com) ;;
*) echo "DD_SITE=$DD_SITE is not a Datadog site - stopping before any key is sent"; exit 1 ;; esac
echo "DD_SITE=${DD_SITE}"
echo "DD_CRED_DIR=$(pwd -P)"
echo "DD_API_KEY=$([ -n "${DD_API_KEY:-}" ] && echo set || echo UNSET) DD_APP_KEY=$([ -n "${DD_APP_KEY:-}" ] && echo set || echo UNSET)"
printf 'DD-API-KEY: %s\n' "$DD_API_KEY" \
| curl -sS --max-time 20 -o /dev/null -w "validate: HTTP %{http_code}\n" \
-H @- "https://api.${DD_SITE}/api/v1/validate"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" \
| curl -sS --max-time 20 -o /dev/null -w "current_user: HTTP %{http_code}\n" \
-H @- "https://api.${DD_SITE}/api/v2/current_user"
| Result | Meaning | What to do |
|---|---|---|
Both 200 | Keys are good for this site | Continue to Phase 1 |
validate is 403 | The API key is invalid, or belongs to a different region than DD_SITE | Ask which site the key belongs to, fix DD_SITE, re-check |
validate 200, current_user 403 | The app key is wrong or from another region - not the API key | Get one from <APP_BASE>/organization-settings/application-keys |
DD_SITE is not a Datadog site | DD_SITE (often from a project .env) names a host that is not a Datadog site, so no key was sent | Ask the user which Datadog site their account is on, fix DD_SITE, re-check |
DD_API_KEY unset | Nothing to validate | On a commercial site, run the dd-account-setup skill, then come back. On ddog-gov.com or us2.ddog-gov.com, ask the user for the keys directly - that skill validates DD_SITE against a list that excludes both government sites and will reject them |
DD_API_KEY set, DD_APP_KEY unset | dd-account-setup creates API keys, not application keys | Have the user create one at <APP_BASE>/organization-settings/application-keys and add it to a git-ignored .env.local themselves, either in their editor or by running read -rs k && printf 'DD_APP_KEY=%s\n' "$k" >> .env.local && chmod 600 .env.local in their own terminal. Never ask them to paste it into chat. Then re-run this preflight |
App URL. The Datadog app host is not app.${DD_SITE} for every site. It is
https://app.datadoghq.com (US1), https://app.datadoghq.eu (EU1), https://app.ddog-gov.com (Gov),
and for every other site it is https://${DD_SITE} itself - https://us3.datadoghq.com,
https://us5.datadoghq.com, https://ap1.datadoghq.com, https://ap2.datadoghq.com,
https://uk1.datadoghq.com. Resolve it once and substitute it wherever <APP_BASE> appears below.
Full list: https://docs.datadoghq.com/getting_started/site/
Remember the resolved DD_SITE. Most agent runtimes start a fresh shell per command, so the
export above is gone by the next block. That is why the loader line is repeated at the top
of every later block that needs credentials - it is deliberate, not drift; don't strip it. Later
blocks may run from a different directory (Phase 2 runs from the Compose project directory), so they
read .env.local / .env from the absolute DD_CRED_DIR Phase 0 printed, not from the current
directory: substitute it for <DD_CRED_DIR_FROM_PHASE_0> (in its Windows form, such as C:\path, in
PowerShell blocks). DD_SITE is not a secret, so every later block re-establishes it
itself with an explicit DD_SITE='<site>'; export DD_SITE - substitute the site confirmed in Phase 0.
It is a plain assignment rather than : "${DD_SITE:=...}" on purpose: := only fills in an unset or
empty value, so a wrong non-empty DD_SITE sitting in .env would survive it and every call would go to
the wrong region. The keys are guarded with :? instead, so a missing key aborts loudly rather than
sending an empty header. Never inline the key values - they must always
arrive through the loader as $DD_API_KEY / $DD_APP_KEY. One consequence follows, and it is
deliberate:
- Datadog calls pass headers on stdin, as
printf 'DD-API-KEY: %s\n' "$DD_API_KEY" | curl -H @- ....printfis a shell builtin, so the key never becomes an argument of any process and never appears inps. Writing-H "DD-API-KEY: $DD_API_KEY"instead would put it in curl's argv. (-H @-needs curl 7.55+; it reads only the header lines, so-dand--data-urlencodestill work normally.)
Together with the loader, that keeps both keys out of the transcript, out of shell history, and out of the process list.
Tools. docker and curl are required:
command -v docker || echo "MISSING docker - https://docs.docker.com/engine/install/"
command -v curl || echo "MISSING curl"
The snippets here are POSIX shell. Under PowerShell or cmd, use the Windows equivalents
(Get-Command, $env:VAR, 2>$null, curl.exe) - same calls, same order.
Phase 1: Determine the Setup
Ask whether the user is running:
- Standalone Docker (a single host running
docker rundirectly). - Docker Compose (their workloads are defined in
docker-compose.yaml).
Then confirm what's actually on the host with a read-only inventory, and use it to fill in details rather than to override what the user told you:
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null | head -20
This samples up to 20 running containers with their images and status, enough to sanity-check the log
and APM defaults below. If a docker-compose.yaml is the deployment unit, the Compose path in Phase 5
is the right one even when docker ps shows containers started another way - ask rather than infer.
If the command fails or returns nothing, don't block: the Agent install doesn't depend on existing
containers, so confirm the setup with the user and continue.
Also check the platform - Linux or Windows. Most Docker hosts are Linux; Windows containers need
a different volume mount. The Compose template in Phase 5 uses Linux-only settings (host PID mode,
/proc and cgroup mounts, loopback-bound ports), so it is for Linux containers only. On a
Windows-container host, use the standalone Windows path in Phase 4 even if the user's workloads run
under Compose.
Ask for the environment (env) tag if the user has one - prod, staging, dev. Skip if
not provided.
Infrastructure metrics, container logs (containerCollectAll), APM trace intake, and DogStatsD
custom-metrics intake are enabled by default - state this and honor an explicit opt-out rather than
asking whether to turn them on. The APM receiver accepts traces but does not instrument application
containers: if the user wants their apps traced, ask how the applications are deployed and configure
their tracing libraries or Single Step Instrumentation separately. Enable live process collection
only when the user asks - do not turn it on silently.
If the user selects APM Single Step Instrumentation, explain that it modifies the Docker host and run the setup UI's instrumentation command once before starting the Agent:
if ! installer=$(curl -fsSL https://install.datadoghq.com/scripts/install_script_agent7.sh); then
echo "Installer download failed - stop here." >&2
elif ! DD_APM_INSTRUMENTATION_ENABLED=docker \
DD_NO_AGENT_INSTALL=true \
bash -c "$installer"; then
echo "Instrumentation setup failed - stop here and read the output above." >&2
fi
Stop if either message prints; do not start the Agent until the instrumentation step succeeds.
Injection applies only to containers started after this step, so application containers that are
already running stay uninstrumented. After the Agent is healthy (Phase 6), list the application
containers from the Phase 1 inventory that should be traced, show the list, and ask before
recreating them (docker compose up -d --force-recreate <service> for Compose services, or the
user's own docker run command after docker rm -f <name>). Then check for their traces at
<APP_BASE>/apm/traces before reporting APM as done.
Add DD_APM_INSTRUMENTATION_LIBRARIES only when the user requested verified language/version
pins.
Phase 2: Create a Restricted Env File
Never pass DD_API_KEY to docker run via -e DD_API_KEY=.... That puts the expanded key
on the docker process command line (visible to anyone who can run ps) and writes the literal
into shell history. Instead, write the key (and the rest of the agent config) to a 0600-permissions
env file and reference it via --env-file. The file lives only on the host filesystem with
restrictive perms.
DD_CRED_DIR='<DD_CRED_DIR_FROM_PHASE_0>' # <- replace with the absolute DD_CRED_DIR Phase 0 printed.
[ -d "$DD_CRED_DIR" ] || { echo "Set DD_CRED_DIR to the directory Phase 0 printed - stop." >&2; exit 1; }
for f in "$DD_CRED_DIR/.env.local" "$DD_CRED_DIR/.env"; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
umask 077
{ mkdir -p ./.dd && [ -O ./.dd ] && chmod 700 ./.dd && rm -f ./.dd/datadog.env; } \
|| { echo "Could not set up a private ./.dd owned by you - stop; nothing was written." >&2; exit 1; }
set -C
cat > ./.dd/datadog.env <<EOF || { echo "Could not create a new ./.dd/datadog.env - stop." >&2; exit 1; }
DD_API_KEY=${DD_API_KEY}
DD_SITE=${DD_SITE}
DD_ENV=<DD_ENV>
DD_APM_ENABLED=true
DD_APM_NON_LOCAL_TRAFFIC=true
DD_APM_RECEIVER_SOCKET=/var/run/datadog/apm.socket
DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true
DD_DOGSTATSD_SOCKET=/var/run/datadog/dsd.socket
DD_LOGS_ENABLED=true
DD_LOGS_CONFIG_AUTO_MULTI_LINE_DETECTION=true
DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL=true
DD_CONTAINER_EXCLUDE_LOGS=name:dd-agent
EOF
set +C
chmod 600 ./.dd/datadog.env
The block stops before writing the key unless ./.dd is owned by the current user and restricted to
700. It removes any old env file and creates a fresh one (set -C refuses to write through a file
that reappeared), so the new file gets 600 from umask 077 rather than keeping an old file's
permissions.
Run this block from the directory the Agent will be started from. For Compose, that is the Compose
project directory: the directory of the first -f file (or --project-directory), because Compose
resolves env_file paths relative to it, not to the shell's current directory. Only the output file
./.dd/datadog.env follows the current directory; the credentials still come from DD_CRED_DIR.
The file above holds the docker defaults: APM intake (DD_APM_ENABLED, DD_APM_NON_LOCAL_TRAFFIC,
DD_APM_RECEIVER_SOCKET), logs (DD_LOGS_* with containerCollectAll), and DogStatsD custom-metrics
intake (DD_DOGSTATSD_NON_LOCAL_TRAFFIC, DD_DOGSTATSD_SOCKET). Drop a block only on an explicit
opt-out of that product, with one exception: the Docker Agent turns APM on by default when
DD_APM_ENABLED is unset, so on an APM opt-out replace the APM block with DD_APM_ENABLED=false and
drop the 8126 port publish (and the 8126 firewall step on Windows). DD_APM_RECEIVER_SOCKET and DD_DOGSTATSD_SOCKET make the agent listen on
/var/run/datadog/ in addition to the TCP/UDP ports, which the Phase 3 UDS guidance depends on.
Add live process collection only when the user asks for it: append
DD_PROCESS_CONFIG_PROCESS_COLLECTION_ENABLED=true.
Drop the DD_ENV line if no env was given. Add ./.dd/ to the user's .gitignore immediately
so the file is never committed:
grep -qxF '.dd/' .gitignore 2>/dev/null || echo '.dd/' >> .gitignore
Phase 3: Standalone Docker (Linux)
Run with --env-file so the keys are read from the restricted file, not the command line.
Publish the APM trace port (8126/tcp) and the DogStatsD port (8125/udp) on the loopback interface so
applications running on the host can send traces and custom metrics:
docker run -d --name dd-agent \
--restart unless-stopped \
--cgroupns host \
--pid host \
--env-file ./.dd/datadog.env \
-p 127.0.0.1:8126:8126/tcp \
-p 127.0.0.1:8125:8125/udp \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /proc/:/host/proc/:ro \
-v /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \
-v /var/run/datadog:/var/run/datadog \
-v /opt/datadog-agent/run:/opt/datadog-agent/run:rw \
-v /var/lib/docker/containers:/var/lib/docker/containers:ro \
registry.datadoghq.com/agent:7
Binding to 127.0.0.1 keeps the agent's intake closed to external networks; only processes on
the same host can reach it. Apps on the host send traces to localhost:8126 and DogStatsD custom
metrics to localhost:8125 (UDP).
For app containers on a custom Docker network (or any container that can't reach the host's
loopback), use the Unix Domain Socket pattern instead: the agent listens on /var/run/datadog/
(mounted via the -v /var/run/datadog:/var/run/datadog line above). Mount the same path into
each app container and set DD_TRACE_AGENT_URL=unix:///var/run/datadog/apm.socket and
DD_DOGSTATSD_URL=unix:///var/run/datadog/dsd.socket. UDS avoids the network entirely and is
the recommended path when the app and agent share a host but not a network.
Optional flags
Add only if the user asked. These are non-secret, so passing via -e is fine:
- Tags:
-e DD_TAGS="team:platform service:web" - Custom hostname:
-e DD_HOSTNAME="custom-name" - Proxy:
-e DD_PROXY_HTTP="http://proxy:port" -e DD_PROXY_HTTPS="https://proxy:port" - Container filtering:
-e DD_CONTAINER_EXCLUDE="image:unwanted-image"or-e DD_CONTAINER_INCLUDE="image:only-this"
Phase 4: Standalone Docker (Windows)
PowerShell. A fresh PowerShell process does not see values loaded by the bash loader, so this block
reloads .env.local / .env (stripping Windows CR and quotes), assigns the confirmed site, and stops
if the key is missing. It then locks the .dd directory down to the current user and SYSTEM, with
inheritance removed, before writing the key, so the new file inherits only that ACL. If the ACL
cannot be set, it stops without writing anything:
$credDir = '<DD_CRED_DIR_FROM_PHASE_0>' # <- replace with the absolute DD_CRED_DIR Phase 0 printed.
if (-not (Test-Path -LiteralPath $credDir -PathType Container)) { throw "Set `$credDir to the directory Phase 0 printed - stop." }
foreach ($f in (Join-Path $credDir '.env.local'), (Join-Path $credDir '.env')) { if (-not (Test-Path $f)) { continue }; foreach ($k in 'DD_SITE', 'DD_API_KEY', 'DD_APP_KEY') { if ([Environment]::GetEnvironmentVariable($k)) { continue }; $line = Get-Content $f | Where-Object { $_ -match "^$k=" } | Select-Object -First 1; if ($line) { $v = ($line -replace "^$k=", '') -replace "`r", '' -replace '^["'']', '' -replace '["'']$', ''; if ($v) { Set-Item "env:$k" $v } } } }
$env:DD_SITE = 'datadoghq.com' # <- replace with the site confirmed in Phase 0.
if (-not $env:DD_API_KEY) { throw "DD_API_KEY not set - run dd-account-setup (commercial sites) or supply it directly (government sites)" }
$envDir = ".\.dd"
New-Item -ItemType Directory -Force -Path $envDir | Out-Null
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
foreach ($sid in [System.Security.Principal.WindowsIdentity]::GetCurrent().User, (New-Object System.Security.Principal.SecurityIdentifier 'S-1-5-18')) {
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
}
Set-Acl -Path $envDir -AclObject $acl -ErrorAction Stop
$envFile = "$envDir\datadog.env"
if (Test-Path $envFile) { Remove-Item $envFile -Force -ErrorAction Stop }
if (Test-Path $envFile) { throw "Could not remove the old $envFile - stop; nothing was written." }
New-Item -ItemType File -Path $envFile -ErrorAction Stop | Out-Null
$allowed = @([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value, 'S-1-5-18')
$others = (Get-Acl $envFile).GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]) | Where-Object { $allowed -notcontains $_.IdentityReference.Value }
if ($others) { throw "$envFile grants access to other accounts - stop; nothing was written." }
@"
DD_API_KEY=$env:DD_API_KEY
DD_SITE=$env:DD_SITE
DD_ENV=<DD_ENV>
DD_APM_ENABLED=true
DD_APM_NON_LOCAL_TRAFFIC=true
DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true
DD_LOGS_ENABLED=true
DD_LOGS_CONFIG_AUTO_MULTI_LINE_DETECTION=true
DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL=true
DD_CONTAINER_EXCLUDE_LOGS=name:dd-agent
"@ | Set-Content -Path $envFile -NoNewline -Encoding ASCII -ErrorAction Stop
(Get-Acl $envFile).Access | Format-Table IdentityReference, FileSystemRights, IsInherited
The block removes any old file (stopping if that fails), creates an empty one, and checks that it grants access only to the current user and SYSTEM before the key is written. The last line shows the final ACL for confirmation.
docker run -d --name dd-agent `
--restart unless-stopped `
--env-file .\.dd\datadog.env `
-p 8126:8126/tcp `
-p 8125:8125/udp `
-v \\.\pipe\docker_engine:\\.\pipe\docker_engine `
-v c:\programdata\docker\containers:c:\programdata\docker\containers:ro `
registry.datadoghq.com/agent:7
Docker's Windows NAT network rejects a host IP in -p ("Windows does not support host IP addresses
in NAT settings"), so the ports cannot be bound to loopback the way the Linux flow does. They are
published on all host interfaces instead. Tell the user, and before starting the container confirm
that inbound 8126/tcp and 8125/udp from other machines are blocked by the host firewall or the
network's security rules; if they are not, ask the user to add that block. Apps on the host send
traces to the host's address on port 8126 and DogStatsD custom metrics to port 8125. Windows
containers do not support Unix domain sockets the same way Linux containers do, so the port-publish
above is the canonical path on Windows hosts.
This path applies to supported Windows Server container hosts. Infrastructure metrics, logs, APM
intake, and DogStatsD are on by default here - remove a block only on an explicit opt-out. On an APM
opt-out, set DD_APM_ENABLED=false instead of removing the line, and drop -p 8126:8126/tcp. Live
process collection (DD_PROCESS_CONFIG_PROCESS_COLLECTION_ENABLED=true) is the only opt-in extra.
Phase 5: Docker Compose
Reference the same restricted env file via the Compose env_file directive - keys never appear
in docker-compose.yaml, in environment-substitution warnings, or in shell history. This template
is for Linux containers only; on a Windows-container host use Phase 4 instead:
services:
datadog-agent:
image: registry.datadoghq.com/agent:7
container_name: dd-agent
restart: unless-stopped
pid: host
env_file:
- ./.dd/datadog.env
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /proc/:/host/proc/:ro
- /sys/fs/cgroup/:/host/sys/fs/cgroup:ro
- /var/run/datadog:/var/run/datadog
- /opt/datadog-agent/run:/opt/datadog-agent/run:rw
- /var/lib/docker/containers:/var/lib/docker/containers:ro
ports:
- "127.0.0.1:8126:8126/tcp"
- "127.0.0.1:8125:8125/udp"
container_name: dd-agent keeps the container name the same as the standalone flow, so
DD_CONTAINER_EXCLUDE_LOGS=name:dd-agent still keeps the Agent from collecting its own logs. Do not
also run the standalone dd-agent container on the same host.
./.dd/datadog.env resolves relative to the Compose project directory, so the Phase 2 file must
live there (see Phase 2); otherwise point env_file at the file Phase 2 actually created, relative to
that directory. Merge this service into the user's existing Compose file (or create
docker-compose.yaml if there is none), show the change, and confirm before starting it. Then start only the Agent service and check it.
If the project uses -f <file> or -p <project>, pass the same options to every docker compose
command:
docker compose up -d datadog-agent
docker compose ps datadog-agent
docker compose exec -T datadog-agent agent status
Application containers on the same Compose network reach the agent at datadog-agent (service
name) for traces and DogStatsD - set DD_AGENT_HOST=datadog-agent in app containers. Apps
running on the host hit localhost:8126 / localhost:8125 via the loopback port-publish above.
For containers that share a host but not a network with the agent, the /var/run/datadog volume
can be mounted into the app container and used with DD_TRACE_AGENT_URL=unix:///var/run/datadog/apm.socket.
For higher-security multi-tenant Docker hosts, prefer Docker Swarm secrets (docker secret create) - the secret is stored encrypted in the Swarm raft log and mounted into containers via
/run/secrets/<name> instead of injected as an env var. The Datadog Agent supports loading
configuration values from files via the secrets_handler mechanism. Single-tenant non-Swarm
Docker hosts treat anyone in the docker group as root-equivalent, so the env-file approach is
the practical hardening ceiling there.
Phase 6: Verify
docker exec dd-agent agent status
For Compose, use docker compose exec -T datadog-agent agent status with the same -f / -p
options as above. Note the hostname the Agent reports (docker exec dd-agent agent hostname, or
docker compose exec -T datadog-agent agent hostname); the metrics check below filters on it.
Then check:
- Infrastructure List:
<APP_BASE>/infrastructure- the Docker host should appear. - Containers:
<APP_BASE>/containers- running containers visible.
Getting the Most Out of Your Agent
Checking for metrics
Wait 30 seconds, then run:
DD_CRED_DIR='<DD_CRED_DIR_FROM_PHASE_0>' # <- replace with the absolute DD_CRED_DIR Phase 0 printed.
[ -d "$DD_CRED_DIR" ] || { echo "Set DD_CRED_DIR to the directory Phase 0 printed - stop." >&2; exit 1; }
for f in "$DD_CRED_DIR/.env.local" "$DD_CRED_DIR/.env"; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
sleep 30
resp=$(printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" \
| curl -sS -w '\n%{http_code}' -G -H @- "https://api.${DD_SITE}/api/v1/query" \
--data-urlencode "from=$(($(date +%s) - 900))" \
--data-urlencode "to=$(date +%s)" \
--data-urlencode "query=avg:docker.cpu.usage{host:<HOSTNAME>} by {container_name}") \
|| { echo "metric query request failed (curl error) - that is NOT 'metrics still propagating'." >&2; exit 1; }
code=$(printf '%s' "$resp" | tail -1); body=$(printf '%s' "$resp" | sed '$d')
[ "$code" = "200" ] || { echo "metric query failed with HTTP $code - that is NOT 'metrics still propagating':"; printf '%s\n' "$body"; exit 1; }
printf '%s\n' "$body"
Substitute <HOSTNAME> with the hostname the Agent reported in Phase 6. Without the host filter,
the query covers every Docker host in the org, so other hosts' containers could fill the widget even
when this install sends nothing.
Rendering the widget
If series is non-empty, plot from real pointlist data using box-drawing characters; list
up to 3 container names by average. Otherwise show the static preview:
┌─────────────────────────────────────────────────────────┐
│ docker.cpu.usage ▂▃▅▆▇▆▅▃▂▁▂▃▅▆▇█ │
│ 100% ┤ ╭──╮ │
│ 75% ┤ ╭───╮ ╭──╯ │ │
│ 50% ┤ ╭────╯ ╰──╮ ╭────╯ │ │
│ 25% ┤ ╭────────╯ ╰────╯ │ │
│ 0% ┤────╯ │ │
│ └────────────────────────────────────────────┘ │
│ │
│ Metrics are on their way - check back in a few minutes │
└─────────────────────────────────────────────────────────┘
Containers: <APP_BASE>/containers
Useful links
- Containers:
<APP_BASE>/containers - Infrastructure Map:
<APP_BASE>/infrastructure/map - Monitors:
<APP_BASE>/monitors/create
If APM was enabled:
- APM:
<APP_BASE>/apm/getting-started
If logs were enabled:
- Logs:
<APP_BASE>/logs
Important Notes
- Never pass
DD_API_KEYvia-e DD_API_KEY=.... Use--env-file(orenv_file:in Compose) with a 0600-permissions file. The file must be in.gitignore. - The Docker socket mount (
/var/run/docker.sock) is required for container discovery and log collection. - For Compose, the agent service name is reachable from app containers via Docker DNS - set
DD_AGENT_HOST=datadog-agentin app containers, notlocalhost. - For environments with restricted internet access, configure a proxy via
DD_PROXY_HTTP/DD_PROXY_HTTPSor mirror the agent image to a private registry. docker inspectexposure of container env vars is intrinsic to non-Swarm Docker - anyone with Docker socket access has root-equivalent privileges on the host already, so this is a property of the trust model, not a fixable leak. For multi-tenant Docker, use Swarm secrets or a secrets backend (Vault, AWS Secrets Manager via the Datadog Agent'ssecrets_handler).