Having a custom image allows applications to be scaled on-demand via orchestration tools (Kubernetes, Rancher, Kasm, etc.) because containers can be spun up in milliseconds straight from an image.


MAIN STEPS

  • Create a new directory.
  • Create a new file in that directory called Dockerfile.
  • Add the following instructions to the file:
    • FROM – specifies the base image to build from.
    • RUN – executes commands during the image build process.
    • COPY / ADD – copies files or directories into the image.
    • CMD – specifies the command to run when a container is started from the image.
  • Build the image using docker build with the -t flag to assign it a name.

Additional instructions can be added as needed:

  • ENV – sets environment variables in the image.
  • EXPOSE – declares which ports the container will listen on at runtime.
  • USER – sets the UID and/or GID that CMD will run as.
  • WORKDIR – sets the working directory for subsequent instructions like RUN and CMD.
  • VOLUME – creates a persistent volume that survives container restarts.
  • and more.

Note: COPY and ADD are not the same. COPY only copies local files and directories from the build context into the image. ADD also supports extracting compressed archives (such as tar or zip files) and copying files from a remote URL.


INSTALL LATEST DOCKER FROM OFFICIAL REPOSITORY

sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo nano /etc/apt/sources.list.d/docker.sources
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
sudo systemctl status docker

EXAMPLES

Create a file named Dockerfile and add the build instructions. See examples below:

  • Building the latest Alpine.
FROM alpine:latest
COPY ./hello.sh /usr/local/bin/hello
RUN chmod +x /usr/local/bin/hello
CMD ["/usr/local/bin/hello"]
  • Building Python 3.9 on top of Alpine, from its official repository on Docker Hub.
FROM python:3.9-alpine
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
EXPOSE 80:5000/tcp
CMD ["flask", "run", "--host=0.0.0.0"]
  • Building the latest Uptime Kuma with some additional tools.
FROM louislam/uptime-kuma
RUN apt update 
RUN apt install nano htop curl -y
EXPOSE 3001/tcp
  • Building PostgreSQL 17.7 on top of Alpine, from its official repository on Docker Hub.
FROM postgres:17.7
ENV POSTGRES_DB dbHost
ENV POSTGRES_USER dbUser
ENV POSTGRES_PASSWORD dbPassword
COPY init.sql /docker-entrypoint-initdb.d/
EXPOSE 5432
  • Building a generic Red Hat 10 image using the Universal Base Image from Red Hat’s public repo.
FROM registry.access.redhat.com/ubi10/ubi:latest
RUN dnf install -y httpd && \
    dnf clean all && \
    rm -rf /var/cache/dnf
ENV CACHE=/tmp/cache
RUN mkdir -p /devenv
ADD . /devenv
WORKDIR /devenv
RUN /devenv/script.sh
EXPOSE 8080:80
CMD ["/dev/entry-point.sh"]
  • Building a Red Hat 10 image using the Universal Base Image with Init from Red Hat’s public repo.
FROM registry.access.redhat.com/ubi10/ubi-init
RUN dnf install -y --nodocs --setopt install_weak_deps=false http mariadb && \
    dnf clean all
EXPOSE 80 3306
RUN systemctl enable httpd mariadb
  • Building a Fedora image and adding an additional repository.
FROM fedora:latest
ENV LIBGUESTFS_BACKEND=direct
RUN dnf -y install virt-v2v dnf-plugins-core wget
RUN wget https://fedorapeople.org/groups/virt/virtio-win/virtio-win.repo -O /etc/yum.repos.d/virtio-win.repo && \
    dnf -y install virtio-win
RUN dnf clean all && \
    rm -rf /var/cache/dnf
  • Building a container to establish a Tunnel (also known as Argo or Connector) with Cloudflare Zero Trust.
FROM debian:12
RUN apt update
RUN apt install nano htop curl wget -y
RUN wget -O /tmp/cloudflared.deb -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
RUN dpkg -i /tmp/cloudflared.deb && rm /tmp/cloudflared.deb
CMD ["sh", "-c", "/usr/bin/cloudflared --no-autoupdate tunnel run --token "]

BUILD

sudo docker build -t customImageName:v1.0 .

RUN

sudo docker run --name containerName customImageName:v1.0
  • Building a Debian 13 image with Apache and PHP 8, using a base target and a development target with debugging tools.
FROM php:8-apache-trixie AS base
RUN a2enmod rewrite
WORKDIR /var/www/
EXPOSE 80/tcp
CMD ["apache2-foreground"]

FROM base AS dev
RUN apt update && apt install -y curl nano htop
FROM base AS final

RUN STAGE

sudo docker run --target base customImageName

PUBLISHING IMAGES

Images (also referred to as Artifacts) can be published to a remote registry for public or private use, with access control support.

The two most popular container registries are Docker Hub and GitHub Container Registry. See the examples below for pushing images to both using GitHub Actions.

  • Docker Hub [Link]
    • Pros: The most widely used registry and the default for Docker tooling and many third-party tools.
    • Cons: Pull rate limits can break CI/CD pipelines and automated deployments.
name: Build and Push Multi-Arch Docker Image

on:
  push:
    branches:
      - main

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Log in to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    - name: Build and push multi-arch image
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}:latest
  • GitHub Container Registry [Link]
    • Pros: Tight integration with GitHub repositories, permissions, and GitHub Actions.
    • Cons: Not the default registry in many tools, which can lead to accidentally pulling images from Docker Hub instead of GHCR.
name: Build and Push Multi-Arch to GHCR

on:
  workflow_dispatch:

permissions:
  contents: read
  packages: write

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to GHCR
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push multi-arch image
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest

Note: Git repositories allow uppercase letters in their names, but container registries do not. If your repository name contains uppercase characters, image pushes may fail rather than being automatically converted to lowercase. Keep this in mind when naming your repositories.

See more examples at Docker Awesome Compose [Link].


EXTRACT IMAGE CONTENT

To extract the contents of a Docker Image for analysis:

sudo docker save USER/IMAGE > USER_IMAGE.tar
tar xvf USER_IMAGE.tar

To extract from a Docker Container:

sudo docker export CONTAINER > CONTAINER.tar
tar xvf USER_IMAGE.tar