Summary
When a Docker Compose stack defines a build-time BuildKit secret sourced from an environment variable:
secrets:
my_token:
environment: MY_TOKEN
and MY_TOKEN is set in the Dokploy Environment tab, the secret file mounted into the build (/run/secrets/my_token) can be empty — even though the variable is configured. It appears to be a timing/propagation race: the first deploy after setting/creating the stack builds with an empty secret, and an unchanged redeploy then succeeds.
Because the secret is only empty (not missing), builds don't fail with an obvious "secret not set" error — they proceed with a blank value, which surfaces as a confusing downstream failure (in our case an HTTP 404 from a private npm registry, because an empty auth token is treated as an anonymous request).
Environment
- Dokploy version: 0.30.0
- Deploy type: Docker Compose (compose file with
build: + top-level secrets:)
- Server OS: Ubuntu 24.04 LTS (Noble)
- Docker: 29.6.1 (BuildKit)
How environment variables are documented to flow
Per the Docker Compose docs page, variables from the Environment tab are written to a .env file next to the compose file and are used for ${VAR} interpolation; they are not automatically injected into containers or the build.
The Compose secrets.<name>.environment: MY_TOKEN form does not interpolate — it looks MY_TOKEN up in the process environment of the docker compose build invocation. When Dokploy has not (yet) exported the UI variable into that process environment, the secret resolves to an empty string. This matches the behavior reported in #726, #1244, #2777, and #2473.
Reproduction
-
Create a Docker Compose stack in Dokploy whose build needs a secret at build time. Minimal Dockerfile:
FROM alpine
RUN --mount=type=secret,id=my_token \
echo "secret bytes: $(wc -c < /run/secrets/my_token)"
-
docker-compose.yml:
services:
app:
build:
context: .
secrets:
- my_token
image: repro:latest
secrets:
my_token:
environment: MY_TOKEN
-
In the Dokploy Environment tab set MY_TOKEN=hello-world.
-
Deploy for the first time. Observe the build log.
Expected
secret bytes: 12 (or 11) — the secret contains the configured value on the first deploy.
Actual
secret bytes: 0 — the secret is empty on the first deploy. Running an unchanged redeploy then prints the correct non-zero byte count. The value was configured the whole time; only the first build saw it empty.
Real-world impact
We hit this installing private packages from a GitLab npm registry during pnpm install --frozen-lockfile in a multi-stage build. The token is passed as a build secret:
RUN --mount=type=secret,id=gitlab_npm_token \
GITLAB_NPM_TOKEN="$(cat /run/secrets/gitlab_npm_token)" pnpm install --frozen-lockfile
With the secret empty, the registry auth header became Authorization: Bearer (empty), and GitLab returns 404 (not 401) for private packages accessed anonymously:
ERR_PNPM_FETCH_404 GET https://gitlab.com/api/v4/projects/.../@scope/pkg-2.3.0.tgz: Not Found - 404
An authorization header was used: Bearer [hidden]
This is especially hard to debug because:
- The variable is set in the UI, so it looks correct.
- The error is a downstream 404, not a "secret missing" error.
- An identical redeploy fixes it, making it look intermittent/environmental.
We confirmed via a direct byte-count probe (wc -c < /run/secrets/gitlab_npm_token) that the secret was empty on the failing build and populated after the redeploy — the token, package, and lockfile were all valid throughout.
Expected fix / request
One or more of:
- Ensure UI Environment variables are exported into the build process environment so
secrets.<name>.environment: VAR resolves them on the first deploy, deterministically (no redeploy race).
- If a build secret sourced from
environment: resolves empty, surface a warning/error in the build log ("secret my_token sourced from MY_TOKEN is empty") instead of silently mounting a blank file.
- Document, on the Docker Compose page, that
secrets.<name>.environment reads the process env (not the interpolated .env), and show the supported pattern for build-time secrets from UI variables.
Related
Summary
When a Docker Compose stack defines a build-time BuildKit secret sourced from an environment variable:
and
MY_TOKENis set in the Dokploy Environment tab, the secret file mounted into the build (/run/secrets/my_token) can be empty — even though the variable is configured. It appears to be a timing/propagation race: the first deploy after setting/creating the stack builds with an empty secret, and an unchanged redeploy then succeeds.Because the secret is only empty (not missing), builds don't fail with an obvious "secret not set" error — they proceed with a blank value, which surfaces as a confusing downstream failure (in our case an HTTP 404 from a private npm registry, because an empty auth token is treated as an anonymous request).
Environment
build:+ top-levelsecrets:)How environment variables are documented to flow
Per the Docker Compose docs page, variables from the Environment tab are written to a
.envfile next to the compose file and are used for${VAR}interpolation; they are not automatically injected into containers or the build.The Compose
secrets.<name>.environment: MY_TOKENform does not interpolate — it looksMY_TOKENup in the process environment of thedocker compose buildinvocation. When Dokploy has not (yet) exported the UI variable into that process environment, the secret resolves to an empty string. This matches the behavior reported in #726, #1244, #2777, and #2473.Reproduction
Create a Docker Compose stack in Dokploy whose build needs a secret at build time. Minimal Dockerfile:
docker-compose.yml:In the Dokploy Environment tab set
MY_TOKEN=hello-world.Deploy for the first time. Observe the build log.
Expected
secret bytes: 12(or11) — the secret contains the configured value on the first deploy.Actual
secret bytes: 0— the secret is empty on the first deploy. Running an unchanged redeploy then prints the correct non-zero byte count. The value was configured the whole time; only the first build saw it empty.Real-world impact
We hit this installing private packages from a GitLab npm registry during
pnpm install --frozen-lockfilein a multi-stage build. The token is passed as a build secret:With the secret empty, the registry auth header became
Authorization: Bearer(empty), and GitLab returns 404 (not 401) for private packages accessed anonymously:This is especially hard to debug because:
We confirmed via a direct byte-count probe (
wc -c < /run/secrets/gitlab_npm_token) that the secret was empty on the failing build and populated after the redeploy — the token, package, and lockfile were all valid throughout.Expected fix / request
One or more of:
secrets.<name>.environment: VARresolves them on the first deploy, deterministically (no redeploy race).environment:resolves empty, surface a warning/error in the build log ("secretmy_tokensourced fromMY_TOKENis empty") instead of silently mounting a blank file.secrets.<name>.environmentreads the process env (not the interpolated.env), and show the supported pattern for build-time secrets from UI variables.Related
.envnot generating