DEV Community

Cover image for Fixing GHCR “Unauthorized” + Docker “Cannot perform interactive login from non-TTY” in GitHub Actions + SSH Deployments
FOLASAYO SAMUEL OLAYEMI
FOLASAYO SAMUEL OLAYEMI

Posted on

Fixing GHCR “Unauthorized” + Docker “Cannot perform interactive login from non-TTY” in GitHub Actions + SSH Deployments

When deploying applications using GitHub Actions + SSH (appleboy/ssh-action) and pulling images from GitHub Container Registry (GHCR), two common errors often appear together:

  • unauthorized when 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This means Docker tried to run:

docker login ghcr.io
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 4: Use Non-Interactive Docker Login (IMPORTANT)

✔️ Correct way:

echo $GHCR_TOKEN | docker login ghcr.io -u $GHCR_USERNAME --password-stdin
Enter fullscreen mode Exit fullscreen mode

Wrong way (causes TTY error):

docker login ghcr.io
Enter fullscreen mode Exit fullscreen mode

Step 5: Verify Login on Server

Run:

cat ~/.docker/config.json
Enter fullscreen mode Exit fullscreen mode

If login worked, you should see:

"auths": {
  "ghcr.io": {
    "auth": "..."
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Common Mistakes That Cause This Issue

1. Using interactive login

docker login ghcr.io
Enter fullscreen mode Exit fullscreen mode

❌ Breaks in CI/CD

2. Forgetting to pass environment variables into SSH

If you don’t include:

envs: GHCR_TOKEN,GHCR_USERNAME
Enter fullscreen mode Exit fullscreen mode

Then the server receives empty values → login fails silently.

3. Using wrong image name or tag

Example:

ghcr.io/org/repo:latest
Enter fullscreen mode Exit fullscreen mode

Make sure:

  • Image exists
  • Tag exists (latest or 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)