The core problem: build infra needs read access
Deploying from a private Git repository is fundamentally an authentication problem. Your build system has to clone code it isn't, by default, allowed to see. The wrong way is to paste a personal access token into a build script. The right way depends on the integration, but every good option shares two properties: least privilege (read-only, scoped to the repos that need it) and no secrets baked into the image.
The authentication options
| Method | Scope | Best for |
|---|---|---|
| OAuth / GitHub App | Per-repo, app-managed, revocable | Platform integrations (the cleanest option) |
| Deploy key (SSH) | Single repo, read-only | One repo, CI you control |
| Fine-grained PAT | Specific repos, specific permissions | Scripts, narrow automation |
| Classic PAT | Broad, user-wide | Avoid, too broad |
OAuth / GitHub App (preferred)
When a platform integrates via a GitHub (or GitLab) App, you grant access to specific repositories through an authorization screen. The platform gets short-lived, scoped installation tokens, you can see and revoke access in your Git provider's settings, and there's no long-lived token sitting in a config file. This is the model you want for any hosted deploy platform.
Deploy keys (SSH)
A deploy key is an SSH key pair where the public key is added to a *single* repository as read-only. The private key lives in your CI. It's tightly scoped (one repo) and simple, but it doesn't scale well across many repos.
# Generate a read-only deploy key
ssh-keygen -t ed25519 -C "deploy@myapp" -f deploy_key
# Add deploy_key.pub to: repo > Settings > Deploy keys (leave 'write' unchecked)
# Provide deploy_key (private) to your build system as a secretFine-grained personal access tokens
GitHub's fine-grained PATs let you scope a token to specific repositories and specific permissions (e.g., "Contents: read-only") with an expiry. Far safer than classic PATs. Use these for scripts where an App isn't available.
# Clone with a fine-grained token (don't hardcode it, read from env/secret store)
git clone https://x-access-token:${GIT_TOKEN}@github.com/org/private-repo.gitRules for not leaking credentials
- 1Never commit tokens. Use your platform's secret store or CI secret variables.
- 2Never bake credentials into the image. If you clone with a token during a multi-stage build, make sure the token isn't in the final layer. Use build secrets that don't persist.
- 3Scope and expire. Read-only, specific repos, short-lived where possible.
- 4Rotate on exposure. If a token ever lands in logs, revoke it immediately, don't just delete the log line.
The build-secret trap
A subtle leak: in a naive Dockerfile, copying or using a token in one layer leaves it in the image history even if a later layer "removes" it. Use BuildKit's --mount=type=secret so the secret is available during a build step but never written to a layer:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=gittoken \
GIT_TOKEN=$(cat /run/secrets/gittoken) \
git clone https://x-access-token:${GIT_TOKEN}@github.com/org/repo.gitThe secret is mounted only for that RUN and never persisted in the image.
Deploying a private repo on PandaStack
PandaStack's model is "Push code. It runs.", you connect a Git repo and it builds, deploys, and goes live. For private repositories, you authorize PandaStack to access the specific repos you choose via the Git integration, so it can clone them for builds without you handling raw tokens. Once connected:
- 1Connect your private repo through the dashboard's Git integration (grant access to just that repo).
- 2PandaStack auto-detects the framework (Node/Python/Go and more via buildpacks) or uses your Dockerfile.
- 3Builds run in rootless BuildKit inside ephemeral Kubernetes Job pods, images go to Google Artifact Registry, and Helm deploys to GKE.
- 4Add any runtime secrets (API keys, etc.) as environment variables on the service, not in the repo.
Because builds happen in isolated, ephemeral Job pods using rootless BuildKit (no host Docker socket), your source and any build secrets stay contained to that build. Every push triggers a new build with live logs, and you keep deploy history and rollbacks.
# Runtime secrets go in the service's env vars, never the repo:
STRIPE_SECRET_KEY=...
# Managed DB is auto-wired; DATABASE_URL is injected for you.Verifying your setup is clean
- Confirm no tokens appear in build logs (search the log output).
- Confirm the final image doesn't contain credentials:
docker historyshouldn't reveal them, and there should be no token files in the running container. - Confirm access is scoped: the integration/key should only reach the intended repo(s).
- Confirm you can revoke: know where to pull access in your Git provider if something leaks.
Conclusion
Deploying from a private repo is all about scoped, revocable, non-persisted credentials. Prefer an OAuth/GitHub-App integration where the platform manages short-lived scoped tokens; fall back to read-only deploy keys or fine-grained PATs for scripts. And never let a credential survive into an image layer or a log line.
PandaStack connects to your private repos through a scoped Git integration and builds them in isolated pods, so you get git-push deploys without juggling tokens. Try it on the free tier at https://dashboard.pandastack.io.
References
- GitHub fine-grained personal access tokens: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
- GitHub deploy keys: https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys
- Docker build secrets (BuildKit): https://docs.docker.com/build/building/secrets/
- GitLab deploy tokens: https://docs.gitlab.com/ee/user/project/deploy_tokens/