
September 11, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Makefiles for build automation turn a pile of shell one-liners into a stable contract your whole team can run. You type make deploy instead of remembering five commands in order. On real client projects I maintain with Laravel Envoy and GitLab CI, a Makefile sits at the repo root as the single entry point before CI takes over. This guide covers GNU Make on Linux and macOS, with patterns that fit PHP 8.3+, Laravel 12/13, Node.js 26 LTS, and Vite 8.x frontends.
-j.What are Makefiles for build automation and how do they work?
A Makefile is a rule file read by GNU Make. Each rule has a target, optional prerequisites, and a recipe. Make compares timestamps. It rebuilds only what changed. That incremental behaviour is why Make still beats ad-hoc scripts for many web stacks.
Think of Make as a thin orchestration layer. It does not replace Composer, npm, or Deployer. It wires them together with consistent names. Your README says make setup and make test. New developers skip tribal knowledge.
Core concepts you must get right
Targets are the names you invoke. Prerequisites are files or other targets that must be up to date first. Recipes are shell commands, and each line must start with a tab character. Spaces will fail silently or loudly depending on your editor.
- .PHONY — marks targets that are not files, like
testordeploy. - Variables — centralise PHP binary, paths, and environment flags.
- Includes — split large Makefiles into
make/*.mkfragments. - Pattern rules — compile many similar outputs from one template.
The official GNU Make manual remains the authoritative reference. Read the sections on automatic variables $@, $<, and $^. They cut duplication fast.
How do you write a Makefile for a Laravel or PHP project?
Start with the commands you already run by hand. Map each to a phony target. Keep recipes idempotent. Running make setup twice should not break the tree.
Below is a production-style Makefile for Laravel 12 on PHP 8.3 with a Vite 8.x frontend. It mirrors patterns from build automation workflows I use on Deployer 7 pipelines.
SHELL := /bin/bash
.PHONY: help setup install build test lint deploy clean
PHP ?= php
COMPOSER ?= composer
NPM ?= npm
APP_ENV ?= local
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
setup: install build ## First-time project bootstrap
$(PHP) artisan key:generate --ansi
$(PHP) artisan migrate --graceful
install: vendor node_modules ## Install PHP and JS dependencies
vendor: composer.lock
$(COMPOSER) install --no-interaction --prefer-dist
@touch vendor
node_modules: package-lock.json
$(NPM) ci
public/build: node_modules resources/js resources/css
$(NPM) run build
build: public/build ## Compile frontend assets
test: vendor ## Run PHPUnit and static analysis
$(PHP) artisan test --parallel
$(PHP) vendor/bin/phpstan analyse --memory-limit=1G
lint: vendor
$(PHP) vendor/bin/pint --test
deploy: test build ## Production deploy via Deployer
dep deploy production
clean: ## Remove generated artifacts
rm -rf vendor node_modules public/build bootstrap/cache/*.php
Variables and environment safety
Never hard-code secrets in a Makefile. Read from .env in recipes or export variables in CI. For staging versus production, pass APP_ENV=staging make deploy or use separate targets.
On Ubuntu servers I manage, I pin the PHP binary: PHP=/usr/bin/php8.3 make test. That avoids the wrong FPM version during cron or CI drift. This pairs well with Linux system administration practices on shared hosts.
Frontend builds without Node on the server
Many Laravel deployments commit compiled assets. The Makefile builds locally or in CI, then Deployer rsyncs public/build. The server never needs Node.js 26 LTS installed. That reduces attack surface and disk use.
If you compile on the server instead, add a build-prod target that runs npm ci && npm run build after git pull. Document which path your project uses. Mixed approaches cause the classic "works in CI, blank CSS in prod" bug.
What is the difference between Make, npm scripts, and CI pipelines?
These tools overlap but solve different layers. Make orchestrates any shell command across languages. npm scripts live inside package.json and know JavaScript tooling well. CI pipelines schedule jobs on remote runners with secrets and artefacts.
| Tool | Best for | Weak at | Typical invoke |
|---|---|---|---|
| GNU Make | Cross-language tasks, file-based incrementality, local + CI parity | Windows without WSL; complex string logic | make test |
| npm scripts | Node/Vite/webpack tasks, JS lint and test | PHP/Composer orchestration alone | npm run build |
| GitLab CI / GitHub Actions | Remote runners, secrets, artefacts, gates | Fast local iteration loops | git push trigger |
| Gradle / MSBuild | JVM and .NET native builds | Lightweight PHP/Laravel stacks | ./gradlew build |
The winning pattern is layered. npm owns JS. Composer owns PHP. Make exposes setup, test, and build to humans. CI calls the same Make targets. Read the companion piece on npm scripts for build automation if your frontend team lives in package.json.
How do you speed up builds with parallel jobs and caching?
GNU Make runs independent prerequisites in parallel when you pass -j. Use make -j$(nproc) test on Linux CI runners. Locally, make -j4 build is usually enough for laptop fans.
File-based incrementality helps frontend work. If public/build/manifest.json is newer than your JS sources, skip Vite. That behaviour aligns with incremental and parallel builds theory. For deeper CI cache strategy, see build caching in CI and Docker layer caching.
Order-only prerequisites
Sometimes you need a directory to exist but its timestamp should not force rebuilds. Order-only prerequisites use a pipe syntax:
public/build/manifest.json: | node_modules
$(NPM) run build
The pipe before node_modules means "ensure it exists" without comparing mtime. That prevents endless Vite rebuilds when npm touches node_modules.
.NOTPARALLEL and critical sections
Database migrations must not run in parallel across two deploys. Mark sensitive targets serial:
.NOTPARALLEL: migrate deploy
migrate: vendor
$(PHP) artisan migrate --force
I have seen race conditions when two CI jobs hit the same staging database. Serial targets are cheap insurance.
How do you integrate Makefiles with GitLab CI and deployment?
CI should call Make, not reimplement it. Your .gitlab-ci.yml stays thin. All logic lives in the Makefile where developers can reproduce failures locally.
stages: [test, build, deploy]
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
test:
stage: test
image: php:8.3-cli
cache:
key: ${CI_COMMIT_REF_SLUG}
paths: [vendor/, node_modules/, .composer-cache/]
before_script:
- apt-get update && apt-get install -y git unzip nodejs npm make
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
script:
- make install
- make -j$(nproc) lint test
build_assets:
stage: build
script:
- make build
artifacts:
paths: [public/build/]
expire_in: 1 week
deploy_production:
stage: deploy
only: [main]
script:
- make deploy
This mirrors the Deployer 7 + GitLab CI setup on sister sites like notarykathmandu.com and translationnepal.com. After symlink swap, reload PHP-FPM so opcache picks up new code. The Makefile can wrap that:
reload-fpm: ## Reload PHP-FPM after deploy
sudo systemctl reload php8.3-fpm
For quality gates before deploy, align with build verification patterns. Failed make test should block make deploy through prerequisite chaining.
WordPress and WooCommerce projects
Make fits CMS workflows too. A WooCommerce 11.1 site might define:
sync-uploads: ## Rsync wp-content/uploads from staging
rsync -avz staging:/var/www/html/wp-content/uploads/ ./wp-content/uploads/
wp-lint:
wp plugin list --status=active
I use similar targets on florist eCommerce builds like the Petals Qatar WooCommerce project. Make does not care whether the backend is Laravel or WordPress 7.1.
Symfony and polyglot repos
Symfony 8.1 requires PHP 8.4.1 minimum. A Makefile can gate version checks:
check-php:
@$(PHP) -r 'version_compare(PHP_VERSION, "8.4.1", ">=") or exit(1);'
@echo "PHP version OK"
test: check-php vendor
$(PHP) bin/phpunit
Monorepos with API and frontend subfolders benefit from includes:
include make/php.mk make/js.mk make/deploy.mk
That structure scales better than a 400-line root file. It also helps onboarding on enterprise application development engagements where multiple teams touch the same repo.
What Makefile mistakes break production builds?
Most failures are boring. Wrong tab indentation. Missing .PHONY so an file named test shadows your target. Recipes that assume cwd instead of $(CURDIR).
- Silent failures — prefix destructive commands with checks; use
set -euo pipefailin bash recipes. - Stale
vendorstamp files — prefer real outputs likevendor/autoload.phpas prerequisites. - Platform drift — document required Make version; GNU Make 4.x ships on Ubuntu 22/24.
- Overloaded targets — split
deployintodeploy-staginganddeploy-production. - Ignoring dry-run — use
make -n deployto preview commands before production.
Reproducibility matters for compliance and debugging. Pair your Makefile with the practices in reproducible builds and pipeline automation best practices. When frontend tooling shifts, compare Vite versus webpack and update the build recipe once.
For infrastructure-heavy workflows, Ansible roles handle server state while Make handles repo state. The split is healthy. See Ansible roles for reusable automation for the server side. Make stays the developer's daily interface.
Need to validate JSON configs your Makefile generates? Use the JSON formatter tool during local debugging. Small utilities reduce context switching during build troubleshooting.
Key Takeaways
- Define phony targets for
setup,test,build, anddeployso every developer and CI job runs identical commands. - Use file prerequisites on lockfiles and build outputs so Make skips work when nothing changed.
- Call
make -j$(nproc)in CI for parallel lint and test jobs that do not share mutable state. - Keep CI YAML thin — one
make testline beats duplicated shell blocks that drift over time. - Pin
PHP,COMPOSER, andNPMvariables per environment to avoid version mismatch after deploy. - Split large Makefiles with
include make/*.mkand ahelptarget that documents every recipe.
People Also Ask
Is GNU Make still relevant in 2026?
Yes. Make is installed by default on most Linux CI images and macOS dev machines. It orchestrates polyglot stacks without forcing everything into JavaScript or YAML. Teams that outgrow Make often move to Bazel or Nx, but for typical Laravel, WordPress, and Symfony projects, Make remains the pragmatic choice.
Can Make replace GitLab CI or GitHub Actions?
No. Make runs tasks; CI schedules them on remote infrastructure with secrets, caches, and branch policies. The correct split is local make test plus a CI job that invokes the same target. CI adds gates Make cannot provide alone.
How is a Makefile different from a shell script?
A shell script runs top to bottom every time. Make tracks dependencies and timestamps, skips up-to-date steps, and runs independent prerequisites in parallel. Make also standardises target names across projects, which shell scripts rarely enforce.
Does Make work on Windows?
GNU Make runs well under WSL2 or Git Bash. Native Windows cmd.exe is awkward. Most PHP and Laravel teams standardise on WSL or Linux CI anyway, so this rarely blocks web projects.
Ship consistent builds starting today
Makefiles for build automation cost little to add and pay off every time someone new clones the repo or CI catches a bug you never saw locally. Start with four targets: help, setup, test, and build. Wire your pipeline to call them. Expand only when pain appears.
If you want help standardising build and deploy workflows across Laravel, WooCommerce, or custom PHP applications, review the web development services page or browse the Adventure Third Pole Trek portfolio case for a Livewire booking system shipped with automated deploys. Ongoing reliability work lives under support and maintenance.
Ready to audit your current pipeline? Contact us with your repo layout and CI config. We will map a Makefile structure that matches how your team actually ships. You can also read more from about me or explore the full build automation guide on the blog.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

