Deploying apps

An app that depends on private crates needs them wherever it is built. Where that is decides what you need: nothing at all when the build runs in GitHub Actions, or a narrow, read-only GitHub token when your platform builds the image itself.

Which way your platform deploys

PlatformIts usual way to deployWhat the build needs
Fly.io, Google Cloud Run, AWS, AzureFrom GitHub ActionsNothing: vendor the crates in the job
Railway, Render, Heroku, DigitalOcean App PlatformThe platform builds from your Git repositoryA fine-grained GitHub token, as a build secret

Deploying from GitHub Actions

In Actions the credential provider needs no secret: the job’s OIDC token is the credential. Let the job fetch every dependency with cargo vendor, then build the image offline or hand the directory to your platform’s deploy command (fly deploy, gcloud run deploy --source .). Nothing that reaches the image or the platform can read the registry. CI without secrets has the workflow and the Dockerfile.

Platforms that build from Git

Railway, Render and similar platforms build your Dockerfile themselves when you push, on their own machines, which have no GitHub identity for us to check. Give the build a GitHub token instead: a fine-grained token that can see only the repositories your app’s crates come from, and nothing in them but their metadata. GitHub creates, expires and revokes it; PrivateCrates never issues or stores it, and only asks GitHub what it may read, as it does for every token.

The token

  1. Sign in to GitHub as a service account: a GitHub user for machines, which many organisations already have. It needs read access to the repositories your app’s crates are published from. (A person’s own account works too, but the token stops working when they leave.)
  2. Open Settings → Developer settings → Fine-grained tokens → Generate new token. Choose:
    • Resource owner: your organisation. If it is not in the list, an organisation owner first allows fine-grained tokens, under Organisation settings → Personal access tokens;
    • Expiration: a date, and a reminder to rotate it (your organisation may set a maximum);
    • Repository access: Only select repositories, choosing the repositories whose crates the app depends on. The token can download exactly those crates; every other crate looks as if it does not exist.
    • Permissions: under Repository permissions, add Metadata and set it to Read-only, and nothing else. GitHub does not always add it for you, and without it the token cannot see the repositories at all. It is all the registry needs: the token cannot read code or change anything.
  3. If your organisation requires approval for fine-grained tokens, an owner approves it under Organisation settings → Personal access tokens.
  4. Store it on your platform as described below, and nowhere else.

Why not a classic token?

A classic personal access token reaches every repository the account can, with broad scopes. The fine-grained token above reaches only the repositories you pick, read-only, and your organisation’s owners can see and revoke it.

If the build then finds no crates (“not found” for each, or “no access” for the registry), check the token on GitHub: that its resource owner is the organisation, that an owner has approved it if your organisation requires that, that Metadata: Read-only is listed, and that the crate’s repository is selected. The registry answers exactly as GitHub lets the token see.

Render

Add the token as a Secret File named privatecrates-token. Render mounts secret files into Docker builds as build secrets, so the token is a file only while cargo build runs and never part of an image layer. cargo:token makes Cargo send it as it is, in place of the credential provider your .cargo/config.toml names for developers (CARGO_REGISTRIES_ACME_… is for a registry named acme).

Dockerfile
# syntax=docker/dockerfile:1
FROM rust:1 AS build
WORKDIR /src
COPY . .
# The token exists only while this step runs; cargo:token makes Cargo send it as it is.
RUN --mount=type=secret,id=privatecrates-token,dst=/run/secrets/privatecrates-token \
    CARGO_REGISTRIES_ACME_CREDENTIAL_PROVIDER=cargo:token \
    CARGO_REGISTRIES_ACME_TOKEN="$(cat /run/secrets/privatecrates-token)" \
    cargo build --release --locked

FROM debian:bookworm-slim
COPY --from=build /src/target/release/story-app /usr/local/bin/story-app
CMD ["story-app"]

Railway

Add the token as a sealed service variable named PRIVATECRATES_TOKEN: Railway passes it to the build, but never shows it again. Railway gives variables to Docker builds only as build arguments, so declare it in the build stage alone. The image that runs is the second stage, which copies just the binary, so it contains no trace of the token.

Dockerfile
FROM rust:1 AS build
# A sealed Railway variable. Declared in this stage only: the final image below has no trace of it.
ARG PRIVATECRATES_TOKEN
WORKDIR /src
COPY . .
RUN CARGO_REGISTRIES_ACME_CREDENTIAL_PROVIDER=cargo:token \
    CARGO_REGISTRIES_ACME_TOKEN="$PRIVATECRATES_TOKEN" \
    cargo build --release --locked

FROM debian:bookworm-slim
COPY --from=build /src/target/release/story-app /usr/local/bin/story-app
CMD ["story-app"]

Other platforms

The same two lines work anywhere: set CARGO_REGISTRIES_ACME_CREDENTIAL_PROVIDER=cargo:token and CARGO_REGISTRIES_ACME_TOKEN for the cargo build step only. Prefer a build secret where the platform has one; otherwise a build argument declared in a build stage that the final image does not inherit.

Rotating and revoking

Everything happens on GitHub. To rotate, regenerate the token and update the platform’s secret. To revoke it, delete it from the service account, or, as an organisation owner, under Organisation settings → Personal access tokens. Our permission cache forgets it within five minutes. To let the app use crates from another repository, edit the token’s repository access; no new token is needed.