GNU Make is one of the oldest and most widely used build automation tools in the Unix ecosystem, yet it remains just as relevant today as it was when first released in 1976. Unlike language-specific build tools such as Maven, Cargo, or npm scripts, Make is completely language-agnostic — it works equally well for C projects, Python applications, Docker workflows, and Terraform deployments. A Makefile at the root of a project gives every contributor a common, self-documenting interface: make build, make test, make deploy. This guide covers installing Make on RHEL 9 and writing practical Makefiles for real-world projects.

Prerequisites

  • RHEL 9 server or workstation with sudo/root access
  • A basic understanding of shell commands
  • A project directory to experiment in (any language or stack)

Step 1 — Install GNU Make

# Make is usually available in the base RHEL 9 repos
dnf install -y make

# Verify the installation
make --version
# GNU Make 4.3
# Built for x86_64-redhat-linux-gnu

# Also install gcc if you plan to compile C code
dnf install -y gcc

Step 2 — Understand Makefile Syntax

# Makefile anatomy — save this as Makefile in your project root
# IMPORTANT: recipe lines MUST start with a TAB, not spaces

# --- Variables ---
CC      := gcc
CFLAGS  := -Wall -O2
APP     := myapp
SRC     := main.c utils.c

# --- .PHONY declares targets that are not real files ---
# Without .PHONY, Make skips a target if a file with that name exists
.PHONY: all build test clean

# --- Default target (first target Make runs when invoked with no args) ---
all: build

# --- A target with a file dependency ---
# Syntax: target: dependencies
#         recipe
build: $(SRC)
	$(CC) $(CFLAGS) -o $(APP) $(SRC)
	@echo "Build complete: $(APP)"

test:
	@echo "Running tests..."
	./run_tests.sh

clean:
	rm -f $(APP) *.o
	@echo "Cleaned build artefacts"

Step 3 — Practical Makefile for a Containerised Project

# Makefile for a typical web application project
IMAGE   := myapp
TAG     := $(shell git rev-parse --short HEAD)
REGISTRY := harbor.example.com/myproject

.PHONY: build test docker-build docker-push deploy clean

build:
	npm ci
	npm run build

test:
	npm test -- --coverage

docker-build:
	docker build -t $(IMAGE):$(TAG) .
	docker tag $(IMAGE):$(TAG) $(IMAGE):latest

docker-push:
	docker tag $(IMAGE):$(TAG) $(REGISTRY)/$(IMAGE):$(TAG)
	docker push $(REGISTRY)/$(IMAGE):$(TAG)

deploy:
	kubectl set image deployment/$(IMAGE) $(IMAGE)=$(REGISTRY)/$(IMAGE):$(TAG)
	kubectl rollout status deployment/$(IMAGE)

clean:
	rm -rf dist/ node_modules/ coverage/

Step 4 — Use Automatic Variables

# Automatic variables reduce repetition in recipes
# $@  — the target name
# $<  — the first prerequisite
# $^  — all prerequisites

# Example: compile every .c file to a .o file
build/%.o: src/%.c
	@mkdir -p build
	$(CC) $(CFLAGS) -c $< -o $@

# Link all object files into the final binary
$(APP): build/main.o build/utils.o
	$(CC) -o $@ $^

# Pattern rules with wildcards
%.pdf: %.md
	pandoc $< -o $@

Step 5 — Integrate Makefiles into CI/CD Pipelines

# .drone.yml — calling Make targets in a Drone CI pipeline
kind: pipeline
type: docker
name: default

steps:
  - name: build
    image: node:20-alpine
    commands:
      - make build

  - name: test
    image: node:20-alpine
    commands:
      - make test

  - name: docker-build
    image: docker:24-dind
    privileged: true
    commands:
      - make docker-build
      - make docker-push

# Because the pipeline calls make targets rather than raw commands,
# developers can reproduce any CI step locally with the same make command.

Conclusion

Makefiles provide a simple, universal interface that abstracts the complexity of build, test, and deployment commands behind memorable targets. By declaring .PHONY targets, using variables for configurable values, and leveraging automatic variables to reduce duplication, you create a build system that is easy to read, easy to maintain, and easy to call from CI/CD pipelines. Because make build behaves identically on a developer’s laptop and inside a CI runner, Makefiles eliminate the classic “it works on my machine” problem for build processes. Keep recipes short, delegate heavy logic to shell scripts when needed, and always document your targets with a help target that prints available commands.

Next steps: How to Install and Configure Drone CI on RHEL 9, How to Write Shell Scripts for System Administration on RHEL 9, and How to Build and Push Docker Images in CI/CD Pipelines on RHEL 9.