The usual way to get this wrong is to pass your token as a build argument. It works, but docker history shows build args, and a file written in one layer stays in the image even if a later step deletes it. If the image is ever pushed somewhere, so is your token.

BuildKit secret mounts avoid that. The secret is available to a single RUN step and is never written to a layer. This needs a reasonably recent Docker (BuildKit with Dockerfile syntax 1.10 or later).

1. Keep the token in an environment variable

Use a token with read access to Python packages. Don’t reuse your publish token for builds.

export REPOFORGE_TOKEN=your-read-token

2. Mount it during the install

# syntax=docker/dockerfile:1
FROM python:3.13-slim

WORKDIR /app
COPY requirements.txt .

RUN --mount=type=secret,id=repoforge_token,env=REPOFORGE_TOKEN \
    pip install --no-cache-dir \
      --index-url "https://__token__:${REPOFORGE_TOKEN}@api.repoforge.io/<your-id>/" \
      -r requirements.txt

COPY . .
CMD ["python", "-m", "myapp"]

With upstream mirroring enabled on your registry, that one index serves both your private packages and everything from PyPI. Using it as the only index, rather than adding it with --extra-index-url, means nobody can get a lookalike package from PyPI installed in place of yours.

requirements.txt just lists your private package next to its public dependencies:

my-internal-utils==1.4.0
requests

3. Build with the secret

docker build --secret id=repoforge_token,env=REPOFORGE_TOKEN -t myapp .

Check it worked: docker history myapp shows no token, and the token doesn’t appear anywhere in the image.

Using uv instead

Same Dockerfile, with uv doing the install:

# syntax=docker/dockerfile:1
FROM python:3.13-slim

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

WORKDIR /app
COPY requirements.txt .

RUN --mount=type=secret,id=repoforge_token,env=REPOFORGE_TOKEN \
    uv pip install --system \
      --index-url "https://__token__:${REPOFORGE_TOKEN}@api.repoforge.io/<your-id>/" \
      -r requirements.txt

COPY . .
CMD ["python", "-m", "myapp"]

Pin the uv image to a specific version once you’re happy with it, so builds don’t change under you.

In CI

The same build command works in CI. In GitHub Actions, put the token in a repository secret and expose it to the step:

- name: Build
  env:
    REPOFORGE_TOKEN: ${{ secrets.REPOFORGE_READ_TOKEN }}
  run: docker build --secret id=repoforge_token,env=REPOFORGE_TOKEN -t myapp .

GitLab CI works the same way with a masked CI/CD variable.

Publishing the package in the first place

If you haven’t uploaded your package yet, uv build then uv publish does it (see the uv guide), or follow the Poetry version.


RepoForge hosts private Python packages. There’s a free trial if you want to try this against your own index.