Skip to content

Compose build secret sourced from secrets.<name>.environment mounts EMPTY on first deploy (UI env var not injected into build), silently breaking private-registry installs #5167

Description

@salahmak

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

  1. 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)"
  2. docker-compose.yml:

    services:
      app:
        build:
          context: .
          secrets:
            - my_token
        image: repro:latest
    secrets:
      my_token:
        environment: MY_TOKEN
  3. In the Dokploy Environment tab set MY_TOKEN=hello-world.

  4. 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:

  1. 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).
  2. 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.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions