DockerHub #
DockerHub Access Token #
Create a DockerHub access token, that is used with the GitHub Actions pipeline to push the Docker image to Dockerhub:
-
Open the DockerHub settings: https://hub.docker.com/settings/security
-
Go to: “Security” > “Personal access tokens”
-
Define a access token description like
GitHub
-
Select access permissions: “Read, Write Delete”
-
Click “Generate”
# User name
jueklu
# Access token
dckr_pat_some-token...
GitHub Actions Example #
Overview #
The following GitHub Action Workflow runs the following Jobs:
-
Run static website tests (HTML, CSS, JavaScript)
-
Build a Docker image with Apache based on Alpine
-
Pushes the image into the DockerHub registry
File and Folder Structure #
The file and folder structure of the GitHub repository looks like this:
├── Dockerfiles
│ └── Dockerfile
├── eslint.config.js # JavaScript test configuration
├── .github
│ └── workflows
│ └── build.yaml # CI pipeline manifest
├── public
│ ├── css
│ │ └── styles.css
│ ├── index.html
│ └── js
│ └── scripts.js
├── README.md
└── .stylelintrc.json # CSS test configuration
GitHub: https://github.com/jueklu/github-actions-workflow
Create Repository #
Create a new private repository, mine is named:
github-actions-static-website
.
Add Repository Secrets #
Store the DockerHub access token and user name as GitHub repository secrets:
-
Go to: “Settings”
-
Select (Security) “Secrets and variables” > “Actions”
-
Click “New repository secret”
-
Add the following secrets:
# Name
DOCKERHUB_USERNAME
# Secret
jueklu
# Name
DOCKERHUB_TOKEN
# Secret
dckr_pat_some-token...
The secrets should look like this:

GitHub Actions Workflow Manifest #
- .github/workflows/main.yaml
name: Test, Build, and Push Docker Image
on:
push:
branches:
- main
paths:
- "public/**" # Changes of the website files trigger the workflow
jobs:
# Test job
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 'latest'
- name: Install HTMLHint
run: npm install -g htmlhint
- name: Run HTML linting
run: htmlhint "public/**/*.html"
- name: Install Stylelint
run: npm install -g stylelint stylelint-config-standard
- name: Run CSS linting
run: stylelint "public/css/**/*.css"
- name: Install ESLint
run: npm install -g eslint
- name: Run JavaScript linting
run: eslint "public/js/**/*.js" --config eslint.config.js
# Deploy and push job
build-and-push:
runs-on: ubuntu-latest
needs: test # Waits for the test job
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU # Enables multi-architecture builds to build Docker images for different CPU architectures
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx # Enables advanced Buildx Docker BuildKit build engine
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub # Login to Dockerhub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4 # Requires Buildx
with:
push: true
context: . # Include the Dockerfile directory
file: Dockerfiles/Dockerfile # Path to the Dockerfile
tags: |
jueklu/github-actions-static-website:latest
jueklu/github-actions-static-website:${{ github.sha }}
Dockerfile #
- Dockerfiles/Dockerfile
# Use the Alpine base image
FROM alpine:latest
# Install Apache2
RUN apk update && apk add apache2 && rm -rf /var/cache/apk/*
# Copy website files to the document root
COPY public/ /var/www/localhost/htdocs/
# Set ownership and permissions for Apache directories
RUN chown -R apache:apache /var/www && \
chown -R apache:apache /run/apache2 && \
chown -R apache:apache /var/log/apache2 && \
chmod -R 770 /var/run/apache2 && \
chmod -R 770 /var/log/apache2 && \
chown -R apache:apache /etc/apache2
# Start Apache2 using non-root user
USER apache
# Expose the default Apache port
EXPOSE 80
# Start Apache
ENTRYPOINT ["/usr/sbin/httpd", "-D", "FOREGROUND"]
Static Website Example #
HTML #
- public/index.html
<!DOCTYPE html>
<html>
<head>
<title>jklug.work</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
</head>
<body>
<h1>Some HTML</h1>
<p>Hi there, click me.</p>
<!-- Add a button -->
<button id="myButton">Click Me</button>
<!-- Link to the JavaScript file -->
<script src="js/scripts.js"></script>
</body>
</html>
CSS #
- public/css/styles.css
/* General styling for the body */
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 0;
/* Flexbox centering */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh; /* Full viewport height */
}
/* Header styling */
h1 {
color: #333;
margin: 0 0 10px; /* Remove default margin and add spacing */
}
/* Paragraph styling */
p {
color: #666;
font-size: 18px;
margin: 10px 0; /* Add some spacing */
}
/* Button styling */
button {
background-color: #d2691e;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
margin-top: 20px; /* Add space above the button */
}
/* Button hover effect */
button:hover {
background-color: #8b4513;
}
JavaScript #
- public/js/scripts.js
// Function to handle button clicks
function handleButtonClick() {
const paragraph = document.querySelector("p");
// Toggle text between text
if (paragraph.textContent === "Hi there, click me.") {
paragraph.textContent = "Button was clicked.";
} else {
paragraph.textContent = "Hi there, click me.";
}
}
// Attach event listener when the DOM is ready
document.addEventListener("DOMContentLoaded", () => {
const button = document.querySelector("#myButton");
button.addEventListener("click", handleButtonClick);
});
Testing Configuration #
- eslint.config.js
export default [
{
ignores: ["node_modules"],
},
{
files: ["public/js/**/*.js"],
languageOptions: {
sourceType: "module",
ecmaVersion: "latest",
},
rules: {
"no-unused-vars": "warn",
"no-console": "off",
"indent": ["warn", 4],
"quotes": ["error", "double"],
"semi": ["error", "always"]
},
},
];
- .stylelintrc.json
{
"extends": "stylelint-config-standard"
}
Verify the Workflow #
Workflow Run #
Verify the GitHub Actions Workflow:



Pull the Image & Run Container #
# Pull & run the image from DockerHub: "latest" tag
docker run -d --name example-app -p 8080:80 jueklu/github-actions-static-website:latest
# Pull & run the image from DockerHub: commit SHA
docker run -d --name example-app -p 8080:80 jueklu/github-actions-static-website:3bbe36a4c010ac57b3630b561ca874f5f15bef06
# Test the container
curl localhost:8080
DockerHub #
Repository Settings #
Change the repository image to private:
-
Open the “Repositories” tab
-
Select the “github-actions-workflow-example” repository
-
Go to “Settings”
-
Click “Make private”


Links #
# GitHub Repository
https://github.com/jueklu/github-actions-workflow