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:alpine base 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:

  1. start with a minimal Nginx image;
  2. copy the static application into the web root;
  3. 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: