Docker Publishing with GitHub Actions: A Practical End-to-End Setup
I wanted a small Docker implementation that covered the complete lifecycle rather than stopping at docker build.
The result is a deliberately simple static application packaged with Docker, published to Docker Hub, and automatically rebuilt through GitHub Actions when the container source changes.
The application itself is intentionally basic. The value is in the workflow around it.
What the implementation covers
The project demonstrates:
- building a Docker image from a Dockerfile
- using a lightweight
nginx:alpinebase image - copying application content into the image
- exposing the application through a container port
- tagging and publishing images to Docker Hub
- authenticating to Docker Hub using GitHub repository secrets
- automatically building and publishing through GitHub Actions
- separating source control from the container registry
The public repository is:
https://github.com/rakeshranderia/docker-engineering
The published image is:
https://hub.docker.com/r/rakeshranderia/docker-hello
The Dockerfile
The image is intentionally small:
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
There are only three concerns here:
- start with a minimal Nginx image;
- copy the static application into the web root;
- document that the container serves HTTP on port 80.
Locally, I map that container port to 8080 on the host. That keeps the container internally consistent while allowing the host port to be changed without rebuilding the image.
For example:
docker run --rm -p 8080:80 rakeshranderia/docker-hello:latest
That separation between the container port and host port is one of the small Docker concepts that becomes much more important once multiple services are involved.
Publishing manually
Before automating anything, I validated the lifecycle manually:
docker build -t rakeshranderia/docker-hello:latest ./docker-hello
docker push rakeshranderia/docker-hello:latest
docker pull rakeshranderia/docker-hello:latest
docker run --rm -p 8080:80 rakeshranderia/docker-hello:latest
Doing the manual path first made the later CI/CD automation easier to troubleshoot because the image, registry credentials and runtime behaviour were already known to work.
Automating the Docker Hub publish
The GitHub Actions workflow runs when container-related files change on the main branch, and it can also be triggered manually.
The important part of the workflow is:
name: Publish Docker image
on:
push:
branches:
- main
paths:
- "docker-hello/**"
- ".github/workflows/docker-publish.yml"
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ./docker-hello
push: true
tags: |
rakeshranderia/docker-hello:latest
rakeshranderia/docker-hello:v1
The workflow keeps the Docker Hub credentials out of the repository by storing them as GitHub Actions secrets.
I used a dedicated Docker Hub personal access token rather than an account password. This is both cleaner operationally and easier to revoke or rotate independently.
Why path filtering matters
The workflow only runs when either the Docker application or the workflow itself changes:
paths:
- "docker-hello/**"
- ".github/workflows/docker-publish.yml"
For a small lab this is mostly about learning the pattern. In a larger repository it prevents unrelated documentation or application changes from needlessly rebuilding images.
What this clarified for me
A few concepts became much clearer once the whole lifecycle was working:
Docker is not a small VM
The image packages the application and its runtime requirements without carrying an entire guest operating system.
That makes the unit of deployment much smaller and more focused than a traditional VM.
The registry is part of the delivery chain
Building an image locally is only one step.
Once the image is pushed to Docker Hub, it becomes a portable versioned artefact that can be pulled and run elsewhere.
CI/CD removes the manual publishing step
Once the GitHub Actions workflow is in place, the repository becomes the source of truth.
A relevant change to main can produce the next published image without manually rebuilding and pushing from a workstation.
Secrets should stay outside the repository
The Docker Hub username and token are referenced by the workflow but never committed with the source.
That sounds obvious, but implementing the end-to-end flow makes the boundary much more concrete.
Where this goes next
The next useful extensions are not more Docker commands for their own sake.
They are applying the same container workflow to something with an application runtime, health checks and API behaviour, then using orchestration only when the application actually warrants it.
For now, this project provides a compact working reference for:
source → image → registry → automated publish → runtime
Project links:
Top comments (1)
Validating the lifecycle manually before automating it is the right order, the Actions workflow then encodes something already proven instead of debugging CI blind. The container port vs host port separation looks small here but pays off once several services share a host.