Sunday, 22. June 2025
Makefiles 101 - How to Use Them in Web Development

A couple of months ago, I stumbled across Makefiles in web development for the first time. When I was still an engineer, I worked with a bunch of C and C++ files where Makefiles were frequently used, but I hadn't really understood their purpose — I always thought they were something specific to C/C++. But they can be used for so much more and are very useful. So today, I want to give you a short introduction to Makefiles and show you how you can use them in your web development workflow. Let's dive in.
What is a Makefile?
Let's start by defining what Makefiles are and what they are most commonly used for. To do this, we need to talk about different programming languages.
There are two different types of programming languages. The first type are interpreted languages that use a so-called interpreter, which reads and runs code line by line at runtime. They don't need to be compiled into machine code to run and are typically very easy to test. The most common languages for web development are interpreted languages, such as PHP, JavaScript, Ruby, and Python.
The second type are compiled languages. In order for them to run, they need an extra "translation" step that converts your code into machine code. Instead of an interpreter, these languages need a compiler. Once the code is compiled, you can technically discard your source code and run the executable on your machine. Common compiled languages include C, C++, Go, Rust, and Swift.
In order to compile the source code (let's say C code), one often needs to run a command with multiple options and sometimes even multiple commands (e.g. to load environment variables etc.). This process can become quite cumbersome, which is why Makefiles were invented. With Makefiles, developers can automate and simplify the compilation process by defining clear rules and commands in a structured format. This not only saves time but also reduces the potential for errors, making the development workflow more efficient and manageable.
Structure of a Makefile Command
A typical Makefile consists of a set of rules where each rule contains a target, dependencies, and a command:
- Target: The file or the action that the rule is meant to create or perform. This is usually the name of the executable or a specific action like clean.
- Dependencies: These are the files or other targets that are required for the target to be built or the action to be performed.
- Command: The command that needs to be executed to build the target using the dependencies. This is typically indented with a tab character.
Here is a simple example of a Makefile rule:
target: dependencies
command
This target can then be executed by running make target
in the terminal
History and Availability of the make Tool
The make tool was initially developed in the 1970s at Bell Labs to help manage the build process of large software projects. It was designed to automatically determine which pieces of a large program needed to be recompiled and issue the commands to recompile them.
Today, the make tool is a standard utility that comes pre-installed on most Unix-like operating systems (Linux, MacOS). It is also available for Windows.
Setting Up Your First Makefile
Now, with all the theory out of the way, let's dive into setting up our first Makefile and utilizing them for our web development projects.
Makefiles are stored in the project root and are named Makefile
(surprise surprise) - no file ending needed here.
To create one, you can go into your project root and type touch Makefile
from the command line. Voila, there is your first Makefile. But of course we want
it to do something. Let's start pretty simple and define a "Hello World" command which just echoes out the "Hello World!" string.
test:
echo "Hello World!"
That's it. Now you can open your terminal and run make test
. You should see the output Hello World!
.
The @
in front of the echo
is because by default, the command that is executed in the target (e.g. in our case echo "Hello World!
) will be printed out.
In most of the case we don't want this, so we can just put the @
in front. If you like, you can try out running make test
after deleting the @
.
You should see the following output:
> make test
echo "Hello World!"
Hello World
Don't forget to put the @
back in front of the echo
.
Now, try running just make
. As you can see, the output is the same.
This is because by default when you do not specify the target, make executes the first command in the Makefile. This turns out to be very helpful and
we come back to it at a later part of this article.
Why Use PHONY?
One thing that I think is very important when writing Makefiles is to understand how the make command interpretes what we write. When we specify a target name, make always searches for a file with this name in the current directory. If it finds a file with the name of the target, it will try to execute the file. This can be very handy, but for most web development projects we do not want this behaviour. Let's go through an example.
Create a file named test
in your route directory by running
touch test
Now, run make test
again and you will see a different output:
> make test
make: `test` is up to date
You see? The make command tried to execute the test file and not our command that we specified. In most cases, this should not be a problem since you will not name your targets the same as files (at least I assume you do not). But it's better to be safe than sorry. To prevent this, we can tell the make command that the target should not be read as a file, but as a normal action. Just insert the following line on top of the make target definition:
.PHONY: test
test:
echo "Hello World!"
With that, our make command should work as before despite having a test
file in the directory.
> make test
Hello World!
Don't forget to delete your test file from the directory again!
rm test
Using .PHONY ensures that make always executes the commands associated with a target, regardless of whether a file with the same name exists. This can help avoid confusion and ensure consistent behavior in your build process.
Creating A Help Command
Before we dive into some real world examples, I would like to introduce a nice technique that I use for all my projects.
- I always write comments above my targets with the following structure:
target: short explaination
and - The first target in all my Makefiles is always the following:
.PHONY: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
Including these two steps combined are providing a lot of value and clarify when working with Makefiles.
What the help command does is that it scans the makefile targets and extracts all the target names together with their explaination
and print them out as a list. Placing the command on top of the Makefile also ensures that when someone runs just make
- the help
command will be executed. Let's make some small changes to our Makefile and then see this technique in action.
## help: print this help message
.PHONY: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
## test: print out "Hello World!"
.PHONY: test
test:
@echo "Hello World!"
Now, running make
will give you a list of all possible commands that you can execute inside this project.
> make
Usage:
help print this help message
test print out "Hello World!"
This setup not only makes it easier for anyone using the Makefile to understand what commands are available but also encourages maintaining clear and concise documentation directly within the Makefile itself. As you add more targets, simply ensure they have a comment line above them, and they will automatically be included in the help output.
Basic Examples
To finish this article, here are a couple basic examples of how to use a Makefile in your web development project.
Starting and stopping a Laravel App
## help: print this help message
.PHONY: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
## start: Start the project
.PHONY: start
start:
php artisan serve & npm run dev
In this example, the start command runs php artisan serve and npm run dev simultaneously. This saves you from opening an additional command-line window and executing another command manually.
-
php artisan serve
is a command used in Laravel to start the development server. -
npm run dev
is typically used to start the development build process for frontend assets in a Node.js environment.
While this example is relatively simple, imagine having to start several queues and a WebSocket server on top of that. With a Makefile, you can do all this with a simple make start command, streamlining your workflow and reducing the chance of errors.
Accessing a DB Container
## help: print this help message
.PHONY: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
## db/access: access the DB container
.PHONY: db/access
db/access:
docker exec -it containername mysql -u username -p
If you have a MySQL Docker container running your MySQL database, this command allows you to log into the MySQL command-line interface within the container. The command will prompt you for the password before proceeding to the database interface.
-
docker exec -it
: This command is used to run an interactive terminal session inside a running Docker container. In this case, it connects you to the MySQL command-line client. -
Placeholders: Remember to replace
containername
andusername
with the actual name of your Docker container and your MySQL username. You will be prompted for the password when you run the command.
This db/access
command is significantly shorter and easier to use than typing out the full Docker and MySQL commands each time you need
to access your database.
Security Note:
Always ensure that your database credentials are handled securely, especially when using command-line interfaces. Avoid hardcoding sensitive information in your Makefiles or scripts.
Conclusion
In this article we dove into the basics of creating Makefiles and how you can use them to streamline your web development project. We learned why they were invented and what they were originally used for. Furthermore we digged a little into some nice techniques on how to build a clean, easily maintainable and self documented Makefile that defines targets explicitly without room for confusion. We also provided some basic examples for real world projects.
Of course there is a lot more to Makefiles and I am also still in the process of exploring new techniques on how to utilize them more efficiently. But I think this is a good introduction point for you to start using them in your projects.
As you become more comfortable with Makefiles, I encourage you to experiment with more advanced features and see how they can further enhance your development workflow. If you have any tips or experiences with Makefiles, feel free to share them and continue the conversation.
Happy coding!
Comments
Login or Register to write comments and like posts.