Having a custom image would allow one to scale applications on-demand via orchestration (Kubernetes, Rancher, Kasm, etc) because they 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 blocks to the new file:
    • FROM to use a specific basic image.
    • RUN to execute commands during the image build process.
    • COPY / ADD to copy files or directories to the image.
    • CMD to specify the command that will be executed when a container is run from the image.
  • Build the new image (with docker build and -t to give it a name).

There are additional instructions that can be added as needed, such as:

  • ENV to set environment variables in the image.
  • EXPOSE to declare what ports the container will listen to at runtime.
  • USER is used to set the UID and/or GID that the command, run by CMD, should be executed as.
  • WORKDIR defines the working directory for subsequent instructions, such as RUN and CMD.
  • VOLUME creates a persistent volume in the image (to survive container restarts).
  • and more.

Note: COPY and ADD are not the same. Essentially, COPY only copies local files and directories from the build context to the image. While ADD also supports automatically extracting files from a compressed archive (such as a tar or zip file) 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 with the name Dockerfile and add the build instructions. See examples:

  • Building 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, specifying version 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 and adding some additional tools.
FROM louislam/uptime-kuma
RUN apt update 
RUN apt install nano htop curl -y
EXPOSE 3001/tcp
  • Building PostgreSQL, specifying version 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 RedHat 10 image using the Universal Base Image from RedHat’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 RedHat 10 image using the Universal Base Image with Init from RedHat’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 installing an additional repository into it.
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 (aka Argo or Connector) with Cloudflare’s ZeroTrust.
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 <CLOUDFLARE>"]

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 set up with 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, aka Artifacts, can be published to a remote Registry for public or private (with access control) use.

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

  • Docker Hub [Link]
    • Pros: Most widely used registry and the default for Docker tooling and many third-party tools.
    • Cons: Recent 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 assumed 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 you use uppercase characters, image pushes may fail instead of being automatically converted to lowercase. Keep this in mind.

See more examples at Docker Awesome Compose [Link].


EXTRACT IMAGE CONTENT

Sometimes it is needed to extract the content of a Docker Image for analysis.

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

Or from a Docker Container:

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