DEV Community

Rakesh Randeria
Rakesh Randeria

Posted on

Docker Publishing with GitHub Actions: A Practical End-to-End Setup

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
abhinavs1920 profile image
Abhinav Sharma •

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.