GNU Make is one of the oldest and most reliable build automation tools in software development, yet it remains deeply relevant in modern DevOps workflows. A Makefile acts as a self-documenting command runner that codifies every build, test, lint, and deploy action in a single file that any developer or CI system can invoke with a single command. In this tutorial you will install Make on RHEL 8, learn Makefile syntax from fundamentals to advanced patterns, and build a practical Makefile that works for Go, Python, and Node.js projects with full CI/CD integration.

Prerequisites

  • RHEL 8 system with sudo privileges
  • Basic shell scripting knowledge
  • A project directory to practice in (any language)
  • Docker installed if following the container build targets

Step 1 — Install Make on RHEL 8

Make is part of the make package in the RHEL 8 BaseOS repository. It is often already installed; confirm with make --version.

sudo dnf install -y make
make --version

# Also install the full development toolchain for compiled projects
sudo dnf groupinstall -y "Development Tools"
gcc --version

Step 2 — Understand Makefile Syntax: Targets, Variables, and .PHONY

A Makefile consists of rules with the form target: prerequisites followed by an indented (tab-only) recipe. Variables substitute repeated values; .PHONY prevents conflicts with files of the same name.

mkdir -p ~/make-demo && cd ~/make-demo

cat > Makefile </dev/null || echo "dev")
BUILD_DIR := ./build

# .PHONY tells make these are not file targets
.PHONY: all build test clean help

# Default target — runs when you type just "make"
all: build

## build: Compile the application
build:
	@echo "Building $(APP_NAME) version $(VERSION)..."
	mkdir -p $(BUILD_DIR)
	go build -ldflags="-X main.Version=$(VERSION)" -o $(BUILD_DIR)/$(APP_NAME) ./cmd/...

## test: Run all tests
test:
	go test ./... -v -cover

## clean: Remove build artifacts
clean:
	rm -rf $(BUILD_DIR)
	@echo "Cleaned."

## help: Show this help message
help:
	@grep -E '^## ' Makefile | sed 's/^## //'
EOF

# Display the help output
make help

Step 3 — Use Automatic Variables: $@, $<, and $^

Make’s automatic variables reduce duplication in recipes. $@ is the target name, $< is the first prerequisite, and $^ is the full list of prerequisites.

cat >> Makefile << 'EOF'

# Pattern rule: compile any .c file to a .o object file
# $< = the .c source file, $@ = the .o output file
%.o: %.c
	gcc -Wall -c $< -o $@

# Link object files into a binary
# $^ = all prerequisites (all .o files), $@ = the target binary
$(BUILD_DIR)/program: main.o utils.o
	gcc $^ -o $@
	@echo "Linked $@"

# Run lint tools across all Python files in the project
PYTHON_FILES := $(shell find . -name "*.py" -not -path "./.venv/*")

lint:
	flake8 $(PYTHON_FILES)
	@echo "Lint passed for: $^"
EOF

Step 4 — Practical Targets for Docker and Deployment

Add Docker build, push, and deploy targets to make the Makefile a complete CI/CD command interface. Using $(VERSION) for image tags ensures every image is traceable to a specific commit.

cat >> Makefile << 'EOF'

REGISTRY  := docker.io/YOUR_USERNAME
IMAGE     := $(REGISTRY)/$(APP_NAME):$(VERSION)
IMAGE_LATEST := $(REGISTRY)/$(APP_NAME):latest

.PHONY: docker-build docker-push deploy

## docker-build: Build the Docker image
docker-build:
	docker build -t $(IMAGE) -t $(IMAGE_LATEST) .
	@echo "Built image: $(IMAGE)"

## docker-push: Push the Docker image to the registry
docker-push: docker-build
	docker push $(IMAGE)
	docker push $(IMAGE_LATEST)

## deploy: Deploy to Kubernetes (requires kubectl context)
deploy:
	kubectl set image deployment/$(APP_NAME) 
	  $(APP_NAME)=$(IMAGE) 
	  --record
	kubectl rollout status deployment/$(APP_NAME)
EOF

# Now run a full pipeline
make test
make docker-build
make docker-push

Step 5 — Language-Specific Patterns: Python and Node.js

Make is language-agnostic. Here are idiomatic patterns for Python virtual environments and Node.js projects — each following the same convention so every project is operated the same way.

# Python project Makefile snippet
cat > Makefile.python < Makefile.node << 'EOF'
.PHONY: install build test lint clean

install:
	npm ci

build: install
	npm run build

test: install
	npm test -- --watchAll=false

lint: install
	npm run lint

clean:
	rm -rf node_modules dist
EOF

Step 6 — Integrate Make into a CI/CD Pipeline

Because make is ubiquitous, any CI system — GitHub Actions, GitLab CI, Jenkins, or Drone — can call Makefile targets directly. This keeps CI YAML minimal and moves logic into the repository.

# GitHub Actions example (.github/workflows/ci.yml)
cat > ci-example.yml << 'EOF'
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: make install

      - name: Lint
        run: make lint

      - name: Test
        run: make test

      - name: Build Docker image
        run: make docker-build

      - name: Push image
        if: github.ref == 'refs/heads/main'
        run: make docker-push
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
EOF

# Run the full pipeline locally before pushing
make clean install lint test docker-build

Conclusion

You have installed Make on RHEL 8, mastered core Makefile syntax including targets, .PHONY, variables, pattern rules, and automatic variables ($@, $<, $^), and built a practical Makefile covering build, test, lint, Docker image creation, registry push, and Kubernetes deployment. Adopting Make as your project’s command interface creates a consistent developer experience across Go, Python, and Node.js projects — any developer new to the codebase can run make help and immediately understand how to build, test, and ship the application. CI systems call the same Makefile targets as developers, eliminating the “works on my machine” class of CI failures.

Next steps: Combining Make with Docker Compose for Local Development Environments on RHEL 8, Caching Docker Layers in CI Pipelines with BuildKit on RHEL 8, and Managing Multi-Service Deployments with Makefile and Helm on RHEL 8.