Skip to main content

GitHub Actions Workflow: Build a Docker Container Image and Push the Image to DockerHub; Code Tests for HTML, CSS and JavaScript

918 words·
GitHub GitHub Actions CI Pipeline DockerHub Docker Linting
Table of Contents

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