Docker & DevOps

Push images to Docker Hub or GitHub Container Registry

Publish your own Docker images to Docker Hub or GitHub Container Registry. Log in with access tokens, tag images properly, keep them private and pull them on your VPS.

BeginnerLinux server5 min read

Once you build an image, you need somewhere to store it so your VPS, a second server or a CI pipeline can pull it. Docker Hub and GitHub Container Registry (GHCR) are the two most common hosted registries. This guide shows how to log in safely with tokens, tag images in a way you will understand later, push them and pull private images on your server.

Before you start

Free plan limits, private repository counts and pull rate limits change from time to time. Check the current terms on Docker's and GitHub's own pricing pages.

Create an access token

Use a token instead of your account password. Tokens can be limited in scope and revoked without changing your password.

Docker Hub

  1. Sign in at Docker Hub and open your Account settings.
  2. Find Personal access tokens and generate a new token.
  3. Give it a description such as vps-pull and choose the narrowest access that works: read-only for servers that only pull, read and write for the machine that pushes.
  4. Copy the token. Docker Hub shows it only once.

GitHub Container Registry

  1. In GitHub, open Settings → Developer settings → Personal access tokens.
  2. Create a classic token with the write:packages scope for pushing, or only read:packages for pulling. Classic tokens are the documented option for GHCR from the command line; check GitHub's docs in case fine-grained tokens support your case.
  3. Set an expiry date and copy the token.

Tip: In GitHub Actions you do not need a personal token. The built-in GITHUB_TOKEN can push to GHCR when the workflow has packages: write permission.

Log in from the command line

Pass the token on standard input so it does not end up in your shell history:

Bash
echo "$DOCKERHUB_TOKEN" | docker login -u yourname --password-stdin

For GHCR:

Bash
echo "$GHCR_TOKEN" | docker login ghcr.io -u your-github-username --password-stdin

Set the variable first with read -s DOCKERHUB_TOKEN, which reads it without echoing to the screen. Docker stores the credentials in ~/.docker/config.json.

Warning: Without a credential helper, config.json holds the token in a reversible encoded form. Keep the file readable only by your user and use read-only tokens on servers.

Tag the image

A registry needs the full name: registry host, namespace and repository.

Bash
docker tag myapp:1.0 yourname/myapp:1.0
docker tag myapp:1.0 ghcr.io/your-github-username/myapp:1.0

Docker Hub is the default, so it needs no host prefix. GHCR names must be lowercase.

Good tagging habits:

  • Use an exact version for every release, such as 1.4.2, and never overwrite it.
  • Optionally also push a moving tag such as 1.4 or latest for convenience.
  • In CI, add the short Git commit hash, for example sha-3f2c1ab, so you can trace any running image back to its code.

Add several tags to the same image:

Bash
docker tag myapp:1.0 yourname/myapp:latest

Push the image

Bash
docker push yourname/myapp:1.0
docker push yourname/myapp:latest

For GHCR:

Bash
docker push ghcr.io/your-github-username/myapp:1.0

Docker uploads only the layers the registry does not already have, so pushing a new version is usually quick.

Make the image private or public

  • Docker Hub: set the visibility when you create the repository on the website, or change it later in the repository's Settings.
  • GHCR: a new package is private by default when pushed from the command line. Change it under the package's Package settings. You can also link the package to a repository so it inherits that repository's access rules.

Pull private images on your VPS

On the server, log in once with a read-only token, then pull as normal:

Bash
echo "$GHCR_TOKEN" | docker login ghcr.io -u your-github-username --password-stdin
docker pull ghcr.io/your-github-username/myapp:1.0

Compose uses the same stored credentials:

YAML
services:
  app:
    image: ghcr.io/your-github-username/myapp:1.0
    restart: unless-stopped
Bash
docker compose pull && docker compose up -d

If you run Compose with sudo, log in with sudo too, because credentials are stored per user.

Check it works

Remove the local copy and pull it back from the registry:

Bash
docker image rm yourname/myapp:1.0
docker pull yourname/myapp:1.0

The pull should succeed. On GHCR, the package also appears under the Packages tab of your GitHub profile or organisation.

Troubleshooting

  • denied: requested access to the resource is denied: the image name does not match your username or organisation, or you are not logged in to that registry. Check docker tag and run docker login again.
  • unauthorized on GHCR: the token lacks write:packages or read:packages, or it has expired.
  • toomanyrequests on Docker Hub: you hit the pull rate limit. Log in on the server so pulls count against your account, and avoid pulling on every deploy when the tag has not changed.
  • invalid reference format: repository name must be lowercase: use lowercase letters in GHCR and Docker Hub names.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.