Use three official Docker actions in one GitHub Actions workflow: log in to Docker Hub with an access token stored as a secret, set up Buildx, then build and push with tags. Keep the workflow's permissions minimal and push only from trusted branches, so a stranger's pull request can never publish an image.
What does the workflow look like?
Docker's documentation for its login action says to authenticate to Docker Hub with a personal access token and "Don't use your account password". The build action's examples show the same three step shape. A minimal version for a Node.js service:
name: publish
on:
push:
branches: [main]
tags: ["v*"]
permissions:
contents: read
jobs:
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/build-push-action@v7
with:
context: .
push: true
tags: myorg/myapp:latest,myorg/myapp:${{ github.sha }}
The Docker action versions shown match the READMEs we fetched on 18 October 2026. Check them again when you copy this, because they move.
Where do the credentials come from?
Create an access token in Docker Hub, then store it in the repository (or better, an environment) as a secret named DOCKERHUB_TOKEN. Store the username as a plain variable. GitHub's secure use reference warns that anyone with write access to a repository can read its repository secrets, so give the token only the permissions the job needs. Docker's login documentation also describes an OpenID Connect option for organizations that support it, which removes the stored secret entirely.
How do you keep the workflow itself safe?
GitHub's guidance gives several rules that apply directly here.
Set the default token to read only, and raise permissions per job. The example above sets contents to read at the top.
Pin third party actions. GitHub says pinning to a full length commit SHA "helps mitigate the risk of a bad actor adding a backdoor to the action's repository". Tags like v4 can be moved by whoever controls the action repository. Replace each tag with its SHA and leave a comment with the version.
Protect secrets with required reviewers. Environment secrets can require approval before a job can read them, which suits a production publish step.
Control who edits the workflow. Use CODEOWNERS on the workflows directory so changes to the pipeline need a designated reviewer.
Never hand untrusted input to a shell. GitHub documents script injection through values like pull request titles, and recommends passing them through an intermediate environment variable.
Why not run it on pull requests?
On a pull request from a fork, you do not want a publish step with your credentials. The trigger above only fires on pushes to main and on version tags. If you want to test the image build on pull requests, run the build with push set to false and no login step.
What tags should you push?
A moving latest tag is convenient and a poor audit trail. Add an immutable tag such as the commit SHA or a version tag, and deploy by that. Later you can tell exactly which commit a running container came from.
A constructed example
A team copies a workflow from a blog post that uses an account password and an unpinned third party action. After a review they rotate the password, create a scoped token, move it into a protected environment, pin each action to a SHA, and drop the latest only deployment. The workflow still publishes in the same two minutes.
How to check or apply this
- Create a Docker Hub access token with the narrowest scope that allows pushes.
- Store it as a secret, and the username as a variable.
- Set top level permissions to contents read.
- Pin every action to a full commit SHA, and enable Dependabot version updates for actions.
- Trigger publishing only from main and tags.
- Scan the built image before or after the push, and fail the job on policy violations.
Safeguard's CLI and CI/CD integration can run dependency scanning and policy gates in the same workflow, so a vulnerable dependency can block the publish. The free scan at gold.safeguard.sh/scan covers up to 100 dependencies.
Quick check
Can I use my Docker Hub password in the workflow?
Docker's documentation says not to. Use an access token, or OpenID Connect if your organization supports it.
Why pin actions to a commit SHA?
A tag can be moved to different code. A full commit SHA cannot be changed without creating a new commit, so you control exactly what runs.
Sources
- docker/login-action README, fetched 18 October 2026
- docker/build-push-action README, fetched 18 October 2026
- GitHub Actions secure use reference
Last reviewed: 18 October 2026.