Every time you push code, someone has to build it, test it and ship it. Doing that by hand is slow and easy to get wrong. GitHub Actions lets you automate the whole job inside GitHub itself, with no separate server to maintain. In this tutorial you will build a real pipeline: every push to your main branch will build a Docker image and publish it to Docker Hub automatically.
This is the same flow used by many teams to release containerised applications, and it is a great first CI/CD project for your portfolio. If you are new to containers, read our Docker for Beginners guide first.
How GitHub Actions Works
GitHub Actions has only a few concepts, and once you know them every workflow file becomes easy to read:
- Workflow — an automated process defined in a YAML file inside
.github/workflows/in your repository. - Event — what starts the workflow, such as a push, a pull request, a schedule or a manual click.
- Job — a group of steps that runs on one machine. Jobs can run in parallel or one after another.
- Runner — the machine that runs a job. GitHub provides hosted Linux, Windows and macOS runners, or you can use your own.
- Step — a single task, either a shell command (
run) or a reusable action (uses). - Secret — an encrypted value, such as a password or token, that your workflow can read but nobody can see in the logs.
What You Need Before Starting
- A GitHub account and a repository with a small application.
- A free Docker Hub account.
- A
Dockerfilein the root of the repository.
If you do not have an application yet, create a tiny one. This example serves a static page with nginx:
Add a simple index.html next to it, commit both files and push them to GitHub.
Step 1: Create a Docker Hub Access Token
Never put your Docker Hub password in a workflow. Create an access token instead, which you can revoke at any time without changing your password.
- Sign in to Docker Hub and open Account settings → Personal access tokens (the exact menu name can change, so look under Security if you do not see it).
- Generate a new token with a clear name such as github-actions.
- Give it read and write permission for repositories, then copy the token. You will not be able to see it again.
Step 2: Add Secrets to Your GitHub Repository
In your GitHub repository, open Settings → Secrets and variables → Actions and create two repository secrets:
DOCKERHUB_USERNAME— your Docker Hub username.DOCKERHUB_TOKEN— the access token you just created.
Why secrets matter
Anything you commit to a public repository is visible to everyone, including automated scanners that look for leaked passwords. Secrets are stored encrypted and GitHub hides their values in workflow logs.
Step 3: Write the Workflow File
Create the file .github/workflows/docker-publish.yml with the following content:
Here is what each part does:
onstarts the workflow on every push tomain.workflow_dispatchadds a manual Run workflow button.runs-on: ubuntu-latestasks GitHub for a fresh Linux machine for this job.actions/checkoutdownloads your repository code onto the runner.docker/login-actionsigns in to Docker Hub using your secrets.docker/build-push-actionbuilds the image from your Dockerfile and pushes it. We publish two tags:latestand the commit ID (github.sha), so you can always trace an image back to the exact code that built it.
The action versions shown here were current when we wrote this guide. Check the Marketplace page of each action for the latest major version before you copy the file into a real project.
Step 4: Run It and Watch the Pipeline
Commit the workflow file and push it to main. Then open the Actions tab in your repository. You will see a new run with a yellow dot while it works and a green tick when it succeeds. Click the run to see every step and its logs. When it finishes, open your repository on Docker Hub and you will find the new image with both tags.
Test it on your own machine:
Add a Test Step Before Publishing
A real pipeline should never publish an image that has not been checked. Add a job that runs your tests and make the Docker job wait for it with needs:
If the test job fails, the Docker job is skipped and nothing is published. This is the core idea of CI/CD: automatic checks first, automatic delivery second.
Common Problems and Fixes
- "denied: requested access to the resource is denied" — the token has no write permission, the username secret is wrong, or the image name does not start with your Docker Hub username.
- "Dockerfile: no such file or directory" — the
contextis wrong. If your Dockerfile is inside a folder, setcontext: ./folder-name. - Workflow does not start — the file must be inside
.github/workflows/, end in.yml, and be on the branch listed underon. - Secrets show as empty — secret names are case-sensitive, and secrets are not passed to workflows triggered from forks of your repository.
Best Practices
- Use access tokens with the minimum permission needed, and rotate them regularly.
- Tag images with the commit ID or a version, and avoid relying only on
latestin production. - Keep each workflow small and readable. Split long pipelines into separate jobs.
- Use caching to speed up builds once your pipeline works.
- Protect the
mainbranch so code is reviewed before it is merged and deployed.
What to Learn Next
Now that you can build and publish an image automatically, the next step is deploying it. You can run the image on a server, on AWS, or on Kubernetes. Read our Kubernetes for Beginners guide to see where your container goes next, and compare tools in Jenkins vs GitHub Actions. You can also follow the step-by-step video versions in our free DevOps tutorials.
Build Real CI/CD Pipelines with Mentors
Our DevOps program covers GitHub Actions, Docker, Kubernetes, Terraform and AWS through live projects.
Apply for the Program →