Server
You can adjust the server configuration with the following environment variables.
Variables
| Env var | Required | Description | Supported values | Default value when missing |
|---|---|---|---|---|
DD_VERSION | ⚪ | Runtime version metadata shown in the API, debug dump, startup banner, and self-update checks. Official images set this at build time; override only for custom builds or tests. | Non-empty string (unknown is ignored) | package version, then unknown |
DD_PUBLIC_URL | ⚪ | Public-facing URL for absolute links and OIDC callbacks. Required when any OIDC authentication provider is configured; other links fall back to request-based detection when unset. | URL (e.g., https://drydock.example.com) | auto-detected for non-OIDC request links |
DD_SERVER_NAME | ⚪ | Server name used in notifications to identify this instance (useful in multi-server setups with agents) | Any string | os.hostname() |
DD_SERVER_ENABLED | ⚪ | If REST API must be exposed | true, false | true |
DD_SERVER_UI_ENABLED | ⚪ | Serve the web UI (set to false for headless/API-only mode) | true, false | true |
DD_SERVER_PORT | ⚪ | Http listener port | from 0 to 65535 | 3000 |
DD_SERVER_TRUSTPROXY | ⚪ | Trust X-Forwarded-For headers when behind a reverse proxy | true, false, or hop count (1, 2, etc.) | false |
DD_SERVER_TLS_ENABLED | ⚪ | Enable HTTPS+TLS | true, false | false |
DD_SERVER_TLS_KEY | ⚪ | TLS server key (required when DD_SERVER_TLS_ENABLED is enabled) | File path to the key file | |
DD_SERVER_TLS_CERT | ⚪ | TLS server certificate (required when DD_SERVER_TLS_ENABLED is enabled) | File path to the cert file | |
DD_SERVER_CORS_ENABLED | ⚪ | Enable CORS Requests | true, false | false |
DD_SERVER_CORS_ORIGIN | 🔴 (when CORS enabled) | Allowed CORS origin — required when DD_SERVER_CORS_ENABLED=true; startup will abort if CORS is enabled but this is unset | * or a single origin URL (for example https://drydock.example.com) | — |
DD_SERVER_CORS_METHODS | ⚪ | Supported CORS methods | Comma separated list of valid HTTP verbs | GET,HEAD,PUT,PATCH,POST,DELETE |
DD_COMPAT_WUDCARD | ⚪ | Mount the opt-in wud-card compatibility endpoints at the unversioned /api/* path | true, false | false |
DD_SERVER_COMPRESSION_ENABLED | ⚪ | Enable gzip response compression (SSE responses are excluded automatically) | true, false | true |
DD_SERVER_COMPRESSION_THRESHOLD | ⚪ | Minimum response size in bytes before compression is applied | integer (>=0) | 1024 |
DD_SERVER_FEATURE_CONTAINERACTIONS | ⚪ | Enable start, stop, restart, and update actions via API and UI | true, false | true |
DD_SERVER_FEATURE_DELETE | ⚪ | If deleting operations are enabled through API & UI | true, false | true |
DD_SERVER_METRICS_AUTH | ⚪ | Require authentication on /metrics endpoint | true, false | true |
DD_SERVER_METRICS_TOKEN | ⚪ | Bearer token for /metrics Prometheus scraping (takes priority over session auth when set) | String (minimum 16 characters) | |
DD_SESSION_SECRET | ⚪ | Override the auto-generated, persisted session secret used for cookie signing | Any non-empty string (whitespace-only values are ignored) | auto-generated and persisted to /store on first boot |
DD_SERVER_COOKIE_SAMESITE | ⚪ | Session cookie SameSite policy for auth flows (none requires HTTPS) | strict, lax, none | lax |
DD_SERVER_SESSION_MAXCONCURRENTSESSIONS | ⚪ | Maximum concurrent authenticated sessions per user (oldest sessions are revoked first at login when limit is reached) | integer (>=1) | 5 |
DD_SERVER_RATELIMIT_IDENTITYKEYING | ⚪ | Key authenticated-route rate limits by session/username instead of IP (prevents collisions for multiple users behind shared proxies) | true, false | false |
DD_AUTH_LOCKOUT_MAX_TRACKED_IDENTITIES | ⚪ | Maximum number of unique client identities held in the auth lockout state. Older entries are pruned when the cap is reached. | integer (>0) | 5000 |
DD_AUTH_LOCKOUT_PRUNE_INTERVAL_MS | ⚪ | Interval for pruning expired auth lockout entries from memory and the persisted lockout sidecar file | integer (>0) | 60000 |
DD_AUDIT_UPDATE_AVAILABLE_DEDUPE_MS | ⚪ | Suppress repeated update-available audit rows for the same agent, watcher, container, and version transition. A no-update report or a changed target starts a new transition immediately. Set to 0 to disable deduplication. | integer (>=0, milliseconds) | 3600000 (1 hour) |
DD_SSE_MAX_CLIENTS | ⚪ | Maximum total concurrent SSE connections across all sessions (global cap; individual sessions are also capped at 10). Connections beyond this limit receive a 429 Too Many Requests response. | integer (>0) | 500 |
DD_RUN_AS_ROOT | ⚪ | Request break-glass root mode (requires DD_ALLOW_INSECURE_ROOT=true) | true, false | false |
DD_ALLOW_INSECURE_ROOT | ⚪ | Explicit acknowledgment for break-glass root mode | true, false | false |
For log output configuration (DD_LOG_LEVEL, DD_LOG_FORMAT), see Logs configuration.
CORS Security Guidance
DD_SERVER_CORS_ORIGIN is required when DD_SERVER_CORS_ENABLED=true. Drydock will abort at startup if CORS is enabled without an explicit origin. To intentionally allow all origins (local testing only), set DD_SERVER_CORS_ORIGIN=* explicitly.
For production deployments, set a specific trusted origin:
DD_SERVER_CORS_ORIGIN=https://drydock.example.comDD_SERVER_CORS_ORIGIN=https://ops.example.com
wud-card Compatibility
The unversioned /api/* alias was removed in v1.6.0 — see Deprecations. Setting DD_COMPAT_WUDCARD=true opts back in to a narrow slice of that surface: the four endpoints the Home Assistant wud-card integration calls. Three of them (the container list, per-container triggers list, and watch endpoints) are reshaped into the bare-array response that card expects instead of drydock's { data, total, limit, offset, hasMore, _links } envelope; the fourth (running a trigger) already returns a small non-enveloped body and passes through unmodified. These endpoints stay mounted at /api/* even after the unversioned alias itself is removed, served by the compat router's own internal API router instance rather than by falling through to the removed alias. It exists purely to keep wud-card working against drydock and is off by default. Every other route under /api/* is unaffected by this flag; most of them now return 410 Gone, except two standalone auth aliases — GET /api/auth/methods and GET /api/auth/status — that keep responding unconditionally because they're registered directly on the app rather than through the removed alias router. See Deprecations for details.
The same flag also covers Homepage's native whatsupdocker widget: Homepage's dashboard GETs /api/containers expecting the same bare array wud-card does, and proxies the request server-side, so no CORS configuration is needed for that integration.
This is a best-effort compatibility shim, not a supported integration surface — it carries no compatibility guarantee and may change or be removed without the usual deprecation window. Requests through it are authenticated and rate-limited exactly like the rest of the API; there is no security bypass.
If Home Assistant runs on a different origin than drydock, pair DD_COMPAT_WUDCARD=true with the CORS settings above: set DD_SERVER_CORS_ENABLED=true and DD_SERVER_CORS_ORIGIN to the Home Assistant frontend's origin (for example https://homeassistant.example.com), not drydock's own address. CORS headers alone do not make session-cookie login work across origins — drydock's CORS middleware never sends Access-Control-Allow-Credentials, and wud-card's fetch() calls never request credentials either, so the session cookie is never sent cross-origin. The instance must be reachable either with no authentication configured or via HTTP Basic auth, the only auth mode that actually completes cross-origin. wud-card also offers a Bearer-token auth option, but drydock has no static API token equivalent yet, so that option will silently fail — configure wud-card for Basic auth (or an auth-less instance) instead.
Container Healthcheck
The official Docker image includes a built-in HEALTHCHECK that polls the /health endpoint using a lightweight static binary. When DD_SERVER_TLS_ENABLED=true, the healthcheck automatically switches to HTTPS (with --insecure semantics for self-signed certificates). No additional configuration is needed — remove any custom healthcheck: blocks from your compose file.
curl-based healthcheck overrides (e.g. test: curl --fail http://localhost:3000/health) remain supported for backward compatibility in v1.5.x. When Drydock can inspect its own container through the local Docker socket, the UI shows a deprecation banner if it detects a curl-based override. v1.6.0 is the final warning release, and curl will be removed from the image in v1.7.0. Use the built-in probe or switch to /bin/healthcheck ${DD_SERVER_PORT:-3000} for custom intervals.Plain HTTP Deployments
When DD_SERVER_TLS_ENABLED is not set or is false, drydock automatically adjusts its security headers for plain HTTP:
- HSTS is omitted (since the browser is not on HTTPS)
upgrade-insecure-requestsCSP directive is omitted (prevents browsers from blocking sub-resource loads)
No additional configuration is required — drydock detects the TLS state and adapts automatically. If you run drydock behind a TLS-terminating reverse proxy, set DD_SERVER_TRUSTPROXY=1 (use a hop count, not true, to avoid IP spoofing — see the Reverse proxy section below) so drydock sees the correct protocol from X-Forwarded-Proto.
Session Cookie SameSite Guidance
- Use
lax(default) for typical web + OIDC setups. - Use
strictonly when drydock and IdP are same-site and you want the strictest cookie policy. - Use
noneonly when you explicitly need cross-site cookies (for example embedded UI), and only over HTTPS. SettingDD_SERVER_COOKIE_SAMESITE=nonecauses a startup validation check -- drydock will refuse to start unlessDD_SERVER_TLS_ENABLED=trueorDD_SERVER_TRUSTPROXYis set, ensuring cookies are only sent over a secure transport.
Concurrent Session Guidance
DD_SERVER_SESSION_MAXCONCURRENTSESSIONS controls how many authenticated sessions each user may keep active at once. When a login would exceed the cap, drydock revokes that user's oldest existing sessions first.
Examples
Disable http listener
services:
drydock:
image: codeswhat/drydock
...
environment:
- DD_SERVER_ENABLED=falsedocker run \
-e DD_SERVER_ENABLED=false \
...
codeswhat/drydockSet http listener port to 8080
services:
drydock:
image: codeswhat/drydock
...
environment:
- DD_SERVER_PORT=8080docker run \
-e DD_SERVER_PORT=8080 \
...
codeswhat/drydockEnable HTTPS
services:
drydock:
image: codeswhat/drydock
...
environment:
- DD_SERVER_TLS_ENABLED=true
- DD_SERVER_TLS_KEY=/drydock_certs/server.key
- DD_SERVER_TLS_CERT=/drydock_certs/server.crtdocker run \
-e "DD_SERVER_TLS_ENABLED=true" \
-e "DD_SERVER_TLS_KEY=/drydock_certs/server.key" \
-e "DD_SERVER_TLS_CERT=/drydock_certs/server.crt" \
...
codeswhat/drydockReverse proxy (trust proxy)
Required when drydock runs behind a TLS-terminating reverse proxy (Traefik, Nginx, Caddy, HAProxy, etc.). Without this setting, drydock cannot determine the real protocol or client IP from forwarded headers, which causes:
- CSRF validation failures (403 errors on POST/PUT/DELETE requests) — drydock sees
http://internally while the browser sendsOrigin: https://... - Incorrect security headers — HSTS and secure cookie flags may not be applied
- Wrong client IPs in logs and rate limiting — drydock sees the proxy's IP instead of the real client
Set DD_SERVER_TRUSTPROXY=1 to trust one proxy hop. Use a higher number if you have chained proxies (e.g. CDN → load balancer → app proxy = 3).
services:
drydock:
image: codeswhat/drydock
...
environment:
- DD_SERVER_TRUSTPROXY=1docker run \
-e DD_SERVER_TRUSTPROXY=1 \
...
codeswhat/drydockYour proxy must forward the X-Forwarded-Proto header so drydock knows whether the original request used HTTPS. Most proxies (Traefik, Caddy, NGINX Proxy Manager) do this by default. For Nginx, add proxy_set_header X-Forwarded-Proto $scheme; to your location block. See the FAQ for complete Traefik and Nginx examples.
Reverse proxy caching (blank page after an update)
After you upgrade drydock, the dashboard can come up as a blank white page on some devices (a phone is the classic case) while a desktop hitting the same reverse proxy loads fine. A hard refresh or clearing the site's data on the affected device fixes it.
Here's why: drydock's UI is a single-page app, and its JS/CSS bundles are content-hashed, so the filenames change with every release and the previous release's files are removed from the image. drydock serves the HTML document itself with Cache-Control: no-store, and serves the hashed files under /assets/ with a long-lived immutable cache, which is correct and intentional. If something ignores no-store and hands back a stale copy of the HTML, that page references bundle files that no longer exist, so the browser has nothing to run and you get a blank page. Most reverse proxies don't cache responses unless you've explicitly turned caching on, so the usual culprits are either a proxy cache you enabled, or the device just holding on to an old copy.
Immediate recovery: clear the browser's site data (or hard-reload) on the affected device. If you've added drydock to a phone's home screen, remove it and re-add it. If your proxy has caching enabled, purge its cache too.
Prevention: make sure whatever sits in front of drydock doesn't cache the HTML document, or that it honors the upstream Cache-Control: no-store. Caching /assets/* is fine, in fact it's the point, those filenames are unique per build so they're safe to cache forever.
If you've turned on proxy_cache for drydock, exclude the HTML document from it. The simplest fix is to not enable caching on drydock's location at all and let Nginx forward drydock's own Cache-Control header, which it does by default:
location / {
proxy_pass http://drydock:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# no proxy_cache directive here — drydock's own
# Cache-Control headers control caching
}Caddy's reverse_proxy doesn't cache responses on its own, so there's nothing to configure. If you've added a caching plugin or directive in front of drydock, exclude Content-Type: text/html from it.
Traefik doesn't cache HTTP responses by default either, so no action is needed out of the box. If you've added a caching middleware or plugin, exclude the HTML document from it there too.
Cache-Control: no-store on the HTML document, and as of v1.6 returns a clean 404 for a hashed asset that no longer exists, so a stale page fails fast instead of rendering a blank screen. A caching layer that overrides no-store can still serve a stale page though, which is why this ultimately comes down to your proxy's (or device's) cache configuration.Advanced Tuning
These variables control outbound HTTP calls, the icon proxy cache used to serve container icons, and fleet-stat aggregation. Defaults are suitable for most deployments.
| Env var | Required | Description | Supported values | Default value when missing |
|---|---|---|---|---|
DD_OUTBOUND_HTTP_TIMEOUT_MS | ⚪ | Default timeout for outbound registry, trigger, and provider HTTP requests that do not set a narrower provider-specific timeout | integer (>0) | 30000 |
DD_ICON_CACHE_TTL_MS | ⚪ | TTL for cached icon files on disk | integer (ms) | 2592000000 (30 days) |
DD_ICON_CACHE_MAX_FILES | ⚪ | Maximum number of icon files in cache | integer (>0) | 5000 |
DD_ICON_CACHE_MAX_BYTES | ⚪ | Maximum total size of icon cache in bytes | integer (>0) | 104857600 (100 MB) |
DD_ICON_CACHE_ENFORCEMENT_INTERVAL_MS | ⚪ | Interval for enforcing icon cache file/count/byte limits | integer (>0) | 10000 |
DD_ICON_IN_FLIGHT_TIMEOUT_MS | ⚪ | Timeout for coalesced in-flight icon fetches before another request may retry | integer (>0) | 15000 |
DD_ICON_PROXY_RATE_LIMIT_WINDOW_MS | ⚪ | Sliding window for icon proxy request rate limiting | integer (>0) | 60000 |
DD_ICON_PROXY_RATE_LIMIT_MAX | ⚪ | Maximum icon proxy requests allowed per rate-limit window | integer (>0) | 100 |
DD_STATS_INTERVAL | ⚪ | Stats aggregator poll cycle in seconds — how often the fleet-stats snapshot is refreshed | integer (>0) | 10 |
DD_STATS_HISTORY_SIZE | ⚪ | Number of historical fleet-stat snapshots retained per container (older snapshots are evicted when the cap is exceeded) | integer (>0) | 60 |
Secrets from files
Any DD_* environment variable can be loaded from a file by appending __FILE to the variable name and setting the value to the file path. This is useful for Docker secrets and other secret management tools.
services:
drydock:
image: codeswhat/drydock
...
environment:
- DD_SERVER_WEBHOOK_TOKEN__FILE=/run/secrets/webhook_token
- DD_SESSION_SECRET__FILE=/run/secrets/session_secret
secrets:
- webhook_token
- session_secret
secrets:
webhook_token:
file: ./webhook_token.txt
session_secret:
file: ./session_secret.txtdocker run \
-e DD_SERVER_WEBHOOK_TOKEN__FILE=/run/secrets/webhook_token \
-e DD_SESSION_SECRET__FILE=/run/secrets/session_secret \
-v ./webhook_token.txt:/run/secrets/webhook_token:ro \
-v ./session_secret.txt:/run/secrets/session_secret:ro \
...
codeswhat/drydockFile-sourced secrets follow the common Docker *_FILE convention:
- If both
DD_FOOandDD_FOO__FILEare set, the file value wins. - Trailing whitespace is trimmed, so secrets created with
echoor an editor do not include accidental trailing newlines. - On Unix-like systems, Drydock logs a non-fatal warning when a secret file is readable by group or others. Use
chmod 600 <secret-file>where possible. The permission check is skipped on Windows because POSIX mode bits are not meaningful there. DD_SESSION_SECRET__FILEis supported and uses the same rules as otherDD_*__FILEsecrets.