Pulling Composer packages from a private Git repository, an npm token-protected registry, or a paid WordPress plugin store sounds like a one-line config change. In practice, it is the step that quietly breaks half of CI/CD pipelines: the build host has no credentials, the public keys you added locally do not exist on the build server, and rotating a leaked token means hunting through three different YAML files.
This guide walks through the two authentication patterns that cover almost every private dependency case — SSH-based Git access and token-based access keys — plus the specific gotchas that bite teams running automated deployments from GitHub, GitLab, or Bitbucket. We will use Composer as the worked example because it is the noisiest offender, but the same patterns apply to npm, Yarn, pnpm, Bundler, pip, and Cargo.
When you need this
You hit a private-dependency problem any time the build host cannot resolve a package by URL alone. The most common triggers:
- Composer requires from a private VCS — internal libraries, white-labelled SDKs, or licensed plugins like Advanced Custom Fields Pro, Gravity Forms, or WP Migrate.
- npm or Yarn pulls from a private registry — GitHub Packages, npm Enterprise, JFrog Artifactory, or a Verdaccio instance.
- A
requirements.txt,Gemfile, orgo.modreferences a private Git URL — common when a single team maintains shared internal SDKs across services. - Submodules — fine in development, but submodules quietly fail in CI when the build container has no credentials.
- A CMS like Kirby or WordPress requires a paid Composer-distributed core or plugin — covered in detail in our Kirby CMS deployment guide.
Two authentication patterns cover almost all of these: Git-level authentication via SSH (best for first-party code) and token-based access keys (best for third-party vendors). Pick the wrong one for the situation and you end up rotating credentials every quarter, or worse, hard-coding them into committed config.
Pattern 1: Authentication via Git (SSH)
Tools like Composer, Bundler, and Cargo will often clone the underlying repository to resolve a dependency. If that repository is private, the build host needs a way to authenticate as a Git client — which usually means an SSH key.
The naive approach is to add the build server's public key to a single repository as a deploy key. That works until the build also needs a second private dependency. Most Git hosts allow each SSH key to be attached to only one place for security reasons, so a deploy key tied to repo-a cannot also be used for repo-b.
Use a machine user, not a personal SSH key
The cleanest solution is a machine user (sometimes called a service account): a real Git host account, separate from any human, that exists only to authenticate builds.
For GitHub:
- Create a new GitHub account dedicated to CI (
acme-deploy-bot@yourcompany.com). - Invite that user to every private repository the build needs to read — with read-only access.
- Generate an SSH key on the build host and add the public key to the machine user's SSH keys (not as a per-repo deploy key).
- Enable two-factor authentication on the account.
Any private repo the machine user can read, the build host can clone — no per-repo key juggling required. GitLab calls the same pattern a deploy user
or project access token user
; Bitbucket calls it a Bitbucket access token
against an App user. The mechanics differ, but the principle is identical: one identity, scoped read access to many repos.
If you are using DeployHQ's build pipeline feature, the SSH key generated for your project is reusable across any private repository — just add the project's public key to the machine user's profile, and Composer, npm, and any Git client running inside the build container can authenticate without further setup. The official build pipelines documentation on fetching remote dependencies covers this end-to-end.
Composer with a private VCS
A typical composer.json for an internal library:
{
"repositories": [
{
"type": "vcs",
"url": "git@github.com:acme/internal-payments-sdk.git"
}
],
"require": {
"acme/internal-payments-sdk": "^2.0"
}
}
When the build runs composer install, Composer shells out to Git, Git uses the SSH key on the build host, and the machine user reads the private repo. No tokens in composer.json, no secrets in the repository.
Two gotchas that bite people
known_hostsnot seeded: the first SSH connection to GitHub prompts for host key verification. In a non-interactive build, that prompt hangs the job. Seed the known hosts file in your build script (ssh-keyscan github.com >> ~/.ssh/known_hosts) or, on DeployHQ, the build environment handles this for you.- HTTPS URLs in
composer.lock: if a developer rancomposer requireover HTTPS locally, the lockfile may pin HTTPS URLs that the build host has no token for. Force the SSH form with a globalgit config --global url."git@github.com:".insteadOf "https://github.com/".
Pattern 2: Authentication via access keys
For genuinely third-party dependencies — a paid WordPress plugin, a Gravity Forms add-on, a private npm registry — SSH usually is not on offer. Vendors give you an access key or API token instead.
The key gets included in a build-time config file (auth.json for Composer, .npmrc for npm) that the build process reads to authenticate against the vendor's package server. The hard rule: this file must never live in your repository.
Composer with a paid plugin store
A real-world auth.json for ACF Pro and a private Composer repository:
{
"http-basic": {
"connect.advancedcustomfields.com": {
"username": "your-acf-license-key",
"password": "https://yoursite.com"
}
},
"bearer": {
"packagist.yourcompany.com": "yourBearerTokenHere"
}
}
The matching composer.json references the vendor's package URL, and Composer fetches the package using the credentials in auth.json — without those credentials ever appearing in your committed code.
npm with a private registry
For npm and the GitHub Packages registry, the equivalent file is .npmrc:
@acme:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=ghp_yourTokenHere
Scoped registries let you keep npm install running against the public registry for everything else, and only authenticate when fetching @acme/* packages.
How DeployHQ handles this safely
The risky part is getting auth.json or .npmrc onto the build host without committing it. On DeployHQ this is the config files feature: you paste the contents once, the file is encrypted at rest, and it gets injected into the build environment at the path you specify (auth.json in the project root, for example). The repository stays clean, key rotation is a single edit in the dashboard, and per-user access controls govern who can see the secret.
The same pattern works for .npmrc, .env, pip.conf, ~/.gradle/gradle.properties, and any other build-time secret file.
Token rotation: the part everyone forgets
A private-dependency setup that works on day one quietly rots. Three failure modes we see repeatedly:
- Hard-coded token in
package.jsondependenciesURL —"acme-sdk": "git+https://abc123token@github.com/acme/sdk.git". Visible in every CI log, every error message, and every developer's local clone. Always use registry-scoped credentials in.npmrcinstead. - Long-lived personal access tokens — convenient until that employee leaves and the build breaks at 2 AM. Use machine users with their own credentials, or short-lived tokens scoped to a single repo.
- Tokens in CI environment variables, but also in committed config — when one is rotated and the other is not, the build appears to pass locally but fails on CI, or vice versa. Pick one source of truth.
A rough rotation cadence we recommend: personal access tokens every 90 days, machine user SSH keys annually, and vendor licence keys whenever they reach you (do not wait for the build to break to notice that ACF Pro renewed and your stored licence is now stale).
Reference for the most common build tools
| Tool | Auth file | Auth mechanism | Private registry support |
|---|---|---|---|
| Composer | auth.json |
HTTP basic, bearer tokens, OAuth | Yes — Private Packagist, Satis |
| npm / Yarn | .npmrc / .yarnrc.yml |
Auth token, scoped registries | Yes — GitHub Packages, JFrog, Verdaccio |
| pnpm | .npmrc |
Same as npm | Yes — registry mirroring |
| Bundler | bundle config credentials |
Plain text or env-substituted | Yes — Gemfury, Geminabox |
| pip | pip.conf / .pypirc |
Index URLs with credentials | Yes — pypiserver, Artifactory |
| Cargo | ~/.cargo/credentials |
Token-based | Yes — alternate registries |
Official documentation for the most-asked tools:
- Composer — Repositories documentation
- npm — Creating and publishing private packages
- RubyGems — Publishing gems guide
Wiring this into your deployment pipeline
The pattern that scales: keep public dependencies in your repository, keep credentials out of it, let the build pipeline supply secrets at build time, and let the deployment tool ship the resulting build artifact to your servers. That is exactly what DeployHQ's automated deployment workflow does — Git push triggers a build, the build runs composer install or npm ci with whatever credentials you have configured, and the resulting tree (including vendored private dependencies) ships to your servers with zero-downtime deployments and one-click rollback if something goes wrong.
A few related reads if you are tightening up the rest of your pipeline:
- Git Pull vs Git Push deployments — when each model handles private-dependency fetching better.
- OWASP security checklist for deployments — covers credential hygiene beyond build-time secrets.
- How to deploy with Git — the broader picture, from Git push to live servers.
If you are currently managing private dependencies by SSH-ing onto each server and running composer install by hand — or worse, committing vendor/ directly — start a free DeployHQ trial and move that step into the build pipeline where it belongs.
Questions about a specific build tool or private registry setup? Email support@deployhq.com or reach out on X / Twitter — we are happy to help you wire up Composer, npm, or any other package manager against a private store.