When deploying applications using GitHub Actions + SSH (appleboy/ssh-action) and pulling images from GitHub Container Registry (GHCR), two common errors often appear together:
-
unauthorizedwhen pulling Docker images from GHCR Cannot perform an interactive login from a non TTY device
These errors usually look confusing, but they both come from the same root issue: Docker authentication is not correctly handled in a non-interactive environment (CI/CD or SSH automation).
This article explains what is happening, why it fails, and how to fix it permanently.
The Problem (What You See in Logs)
1. GHCR Unauthorized Error
Error response from daemon:
Head "https://ghcr.io/v2/ORG/REPO/manifests/latest": unauthorized
This means Docker tried to pull a private image from GHCR but was not authenticated.
2. Non-TTY Login Error
Error: Cannot perform an interactive login from a non TTY device
This means Docker tried to run:
docker login ghcr.io
But failed because:
GitHub Actions and SSH scripts do not support interactive input (no terminal to type username/password).
Root Cause
Both errors come from the same issue:
Docker login is either:
- Missing entirely ❌
- Or written in interactive mode ❌
- Or not receiving credentials correctly ❌
In CI/CD environments:
- There is no keyboard input
- There is no terminal (TTY)
- Everything must be fully automated
Correct Mental Model
Think of GHCR like a private building:
-
docker pull= trying to enter the building -
docker login= showing your ID card
If you don’t show your ID before entering, you get:
❌ unauthorized
If you try to “type your password manually” in automation:
❌ non-TTY error
Correct Fix (Production-Ready Solution)
Step 1: Create a GitHub Token (PAT)
Go to:
GitHub → Settings → Developer Settings → Personal Access Tokens
Create a token with:
-
read:packages✅ -
repo(if private repo) ✅
Step 2: Store Secrets in GitHub Actions
Add these secrets:
-
GHCR_USERNAME→ your GitHub username -
GHCR_TOKEN→ your personal access token
Step 3: Pass Secrets into SSH Action
In your GitHub Actions workflow:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.2.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
key: ${{ secrets.SSH_KEY }}
envs: GHCR_USERNAME,GHCR_TOKEN
script: |
echo $GHCR_TOKEN | docker login ghcr.io -u $GHCR_USERNAME --password-stdin
docker compose pull
docker compose up -d
Step 4: Use Non-Interactive Docker Login (IMPORTANT)
✔️ Correct way:
echo $GHCR_TOKEN | docker login ghcr.io -u $GHCR_USERNAME --password-stdin
Wrong way (causes TTY error):
docker login ghcr.io
Step 5: Verify Login on Server
Run:
cat ~/.docker/config.json
If login worked, you should see:
"auths": {
"ghcr.io": {
"auth": "..."
}
}
Final Deployment Flow (Correct Order)
Your SSH deployment script should always follow this pattern:
set -e
echo "Logging into GHCR..."
echo $GHCR_TOKEN | docker login ghcr.io -u $GHCR_USERNAME --password-stdin
echo "Pulling latest images..."
docker compose pull
echo "Restarting containers..."
docker compose up -d
echo "Cleaning unused images..."
docker system prune -f
Common Mistakes That Cause This Issue
1. Using interactive login
docker login ghcr.io
❌ Breaks in CI/CD
2. Forgetting to pass environment variables into SSH
If you don’t include:
envs: GHCR_TOKEN,GHCR_USERNAME
Then the server receives empty values → login fails silently.
3. Using wrong image name or tag
Example:
ghcr.io/org/repo:latest
Make sure:
- Image exists
- Tag exists (
latestor versioned tag)
4. Missing package permissions
Your token must have:
read:packages
Otherwise GHCR will always return unauthorized.
Why This Only Happens in DevOps Automation
This issue is extremely common in:
- GitHub Actions
- Docker CI/CD pipelines
- SSH deployment scripts
- Kubernetes init containers
Because all of them are:
Non-interactive environments (no human input allowed)
Summary
If you remember only one thing:
❗ GHCR pull failures in CI/CD = ALWAYS authentication problem
And:
-
unauthorized→ no valid login -
non-TTY login error→ you used interactive login incorrectly
Final Working Checklist
✔ Use docker login --password-stdin
✔ Pass secrets into SSH (envs:)
✔ Ensure token has read:packages
✔ Ensure image exists in GHCR
✔ Avoid interactive commands in CI/CD
If you found this useful, I create contents about DevOps, infrastructure, and backend engineering on YouTube, Hashnode and Dev.to. Follow along if this is the kind of problem-solving you want more of.
Top comments (0)