Building from Source
Building from source means taking an upstream tarball or git checkout, compiling it on the machine, and copying the result into place — the classic ./configure && make && make install sequence. You reach for it when no repository ships the software, when the packaged version is too old for a feature or a CVE fix you need, or when you have to enable a build flag the distribution chose to leave off. It is the oldest way to install software on Unix, and on a modern server it should be the last one you try.
The reason is one fact that everything else on this page follows from: a source build is invisible to dpkg and rpm. The package database has no row for it, so apt upgrade will never patch it, apt remove can never clean it up, and a security scanner reading the package list will not see it at all. Every file make install writes is yours to track, update, and eventually delete by hand — and on a host you manage for years, "by hand" is exactly the failure mode you are trying to avoid.
The Build Toolchain
Compiling C or C++ needs a compiler, the make driver, and the development headers for every library the program links against. On Debian and Ubuntu the compiler and friends come from the build-essential metapackage, which pulls in gcc, g++, make, and libc6-dev. The library headers are the catch: a runtime package like libssl3 ships the shared object, but the matching libssl-dev package ships the .h files and the .pc metadata the build actually compiles against. Install the runtime and forget the -dev and configure fails with a message about a missing header, not a missing package.
# Debian / Ubuntu — compiler, make, and a library's dev headers sudo apt install build-essential pkg-config libssl-dev # Red Hat / Fedora — same idea, different names sudo dnf install "@Development Tools" pkgconf-pkg-config openssl-devel
The naming split is worth memorizing because it never changes: Debian calls the headers lib<name>-dev, Red Hat calls them <name>-devel. pkg-config (or its modern drop-in pkgconf) is the glue — configure scripts call pkg-config --cflags --libs openssl to discover where a library's headers and link flags live, which is why a missing -dev package surfaces as a pkg-config error rather than a compiler one.
The configure, make, install Sequence
The three commands do three distinct jobs. ./configure is a script that probes your system — which compiler, which libraries, which features are available — and writes a Makefile tailored to that machine; its single most important argument is --prefix, which sets the root directory everything will be installed under. make reads that Makefile and compiles the source into binaries, running entirely in the build directory and touching nothing system-wide. make install is the only step that writes outside the source tree, copying the compiled files under the prefix — which is why it is the only step that needs root.
# Configure for /usr/local, build with all cores, then install ./configure --prefix=/usr/local make -j$(nproc) sudo make install
make -j$(nproc) parallelizes the compile across every CPU the box has, which on a 16-core server turns a ten-minute build into well under two. Note that only the final line runs as root: configure and make should run as your normal user, because nothing in those steps needs to write to a privileged path and a build script running as root is a build script that can damage the system if it misbehaves. Not every project uses Autotools — many now ship a CMakeLists.txt driven by cmake -DCMAKE_INSTALL_PREFIX=/usr/local, or a meson.build driven by meson and ninja — but the prefix-build-install shape is identical across all of them.
Staying Out of the Package Manager's Way
The filesystem hierarchy reserves a place for exactly this situation. /usr belongs to the distribution — every file under it is owned by some .deb or .rpm, and a source build that writes there can overwrite a packaged binary that apt will then silently restore on the next upgrade, or worse, refuse to manage. /usr/local and /opt exist precisely so that locally built software has somewhere to live that no package will ever touch. The default --prefix for Autotools is already /usr/local; the mistake is overriding it to /usr because that is where the distro's copy lived.
--prefix=/usr/local scatters the install across the standard subdirectories — binaries into /usr/local/bin, libraries into /usr/local/lib, config into /usr/local/etc — which integrates with $PATH cleanly but mixes one program's files in with every other local install. --prefix=/opt/<name> does the opposite: it gives the program one self-contained tree you can delete in a single rm -rf, at the cost of having to add /opt/<name>/bin to $PATH yourself. For a single tricky package, /opt is the cleaner choice precisely because uninstalling is one command instead of a hunt.
Making a Source Build Trackable
The right fix for the tracking problem is to not install with make install at all — wrap it so the result lands in the package database. checkinstall runs the install step inside a watcher, records every file written, and packages those files into a real .deb (or .rpm) that it then installs through dpkg. From that point the build is a first-class package: dpkg -l lists it, dpkg -L shows its files, and apt remove takes it away cleanly. The cost is that checkinstall is unmaintained and chokes on some modern build systems, so it is a tool you try rather than rely on.
# Replace `make install` with a tracked .deb on Debian/Ubuntu ./configure --prefix=/usr/local make -j$(nproc) sudo checkinstall --pkgname=myapp --pkgversion=1.4.2
When checkinstall will not cooperate, GNU Stow is the other clean answer. Install into a per-program prefix such as /usr/local/stow/myapp-1.4.2, then run stow to symlink that tree into /usr/local; uninstalling is stow -D myapp-1.4.2, which removes only that program's links. It does not register in the package database, but it makes a manual install reversible with one command and lets two versions coexist — which is the practical half of what package tracking buys you.
When Not to Build
Most source builds are a mistake made before checking the alternatives. If the packaged version is merely old, Debian and Ubuntu both run a backports repository that ships newer builds of selected packages against the stable release, installable with apt install -t bookworm-backports <pkg> and patched by apt like anything else. Many upstreams — Docker, PostgreSQL, NodeSource, the official nginx team — publish their own signed apt repositories, which give you the latest version while keeping it fully managed. On Ubuntu a PPA fills the same role, at the cost of trusting that one maintainer's key.
Build from source only when none of those exist, and even then prefer a container or a self-built package over a raw make install. The honest trade-off: a source build gets you the exact version and flags you want today, and in exchange you sign up to manually rebuild it for every future CVE in that software or any library it statically pulled in. For one-off tools that rarely change, that is a fair deal; for anything network-facing or long-lived, the maintenance debt usually outweighs the convenience.
- Running
./configure --prefix=/usrso the build lands where the distribution's copy lived — it overwrites package-owned files, and the nextapt upgradeeither reverts your binary or breaks on the conflict. - Installing with plain
make installand keeping no record — the files are orphaned forever, invisible todpkg -L, and the only way to remove them later is to read theMakefileand delete each path by hand. - Installing the runtime library but not its
-dev/-develheaders —configurefails with a cryptic "cannot find header" or apkg-configerror that looks like the library is missing when only the headers are. - Running the entire build as
rootwithsudo make— a malicious or buggy build script then has full write access to the system; onlymake installneeds privilege, and only because it writes under the prefix. - Forgetting the source build exists at patch time — because it is not in the package list, it never appears in
apt list --upgradableor a vulnerability scan, so a known-CVE binary sits patched everywhere except the one you compiled. - Compiling against headers from one release and deploying the binary onto another — a glibc or library ABI mismatch produces a binary that links on the build host and fails to start on the target.
- Exhaust
backports, the upstream's signedaptrepo, and a PPA before compiling — a managed package gets patched automatically and a source build never does. - Always pass
--prefix=/usr/localor--prefix=/opt/<name>, never/usr, so the build can never collide with a distribution-owned file. - Wrap the install with
checkinstallto produce a real.deb, or useGNU Stowwith a per-version prefix, so removal is one command instead of a manual file hunt. - Install build dependencies explicitly up front —
apt build-dep <pkg>pulls the exact-devpackages an upstream's Debian build needs — rather than chasing one missing header perconfigurerun. - Run
configureandmakeas an unprivileged user and reservesudoformake installalone, so no build script ever executes asroot. - Record the exact version,
configureflags, and dependency list with the binary — pin them in a script or Dockerfile so the build is reproducible when the next CVE forces a rebuild.
makepkg: a build recipe (PKGBUILD) compiles from source but produces a tracked package the manager installs and removesGentoo — Portage compiles every package from source by design, with USE flags as the supported way to choose build optionsLanguage build tools — cargo install and go install compile from source but install into a per-user path, sidestepping the system package database entirelyKnowledge Check
Why does a plain make install create a long-term maintenance problem on a server?
- The files it writes are invisible to
dpkg/rpm, soaptnever patches them and a scanner never sees them — patching and removal stay manual - It always installs into
/usr, which the kernel remounts strictly read-only once the system has finished booting, so every later update silently fails to write - It strips the binary of its debug symbols during the copy step, so any later crash leaves an unreadable core dump and makes diagnosing the failure on that server impossible
- It overwrites the system
$PATHfor every shell so previously installed commands stop resolving correctly until each user repairs their login profile by hand
A ./configure run fails reporting a missing header for a library you can see is already installed. What is the usual cause?
- The runtime package is installed but the matching
-dev/-develpackage with the headers and.pcfile is not - The compiler shipped in
build-essentialis too new to compile that library's older source, so the configure step rejects the toolchain version before it ever looks for headers configuremust always be run asrootbefore it can read the system headers, and a normal user is denied access to the include directory where they live- The library came from a backport repository, and its development headers ship only in the older stable release version that you would have to enable separately
Why install a source build under /usr/local or /opt rather than /usr?
- Every file under
/usris package-owned; writing there can clobber a managed binaryaptlater reverts, whereas/usr/localand/optare reserved for local software /usris mounted strictly read-only on Debian and Ubuntu by default, so the install would simply fail outright the momentmake installtried to copy its first file into the protected tree- Binaries placed under
/usrare never added to the default$PATHon Debian-family systems, so the freshly built program would never resolve from the shell without a manual export every session - Building under
/usr/localcompiles binaries faster because it always sits on a separate, faster filesystem that the linker can write to without contending with the system partition
What does wrapping the install step with checkinstall buy you over make install?
- It records the installed files into a real
.deband installs it throughdpkg, so the build lands in the package database andapt removecleans it up - It recompiles the source with full optimization and link-time flags that plain
makeleaves disabled by default, producing a noticeably faster binary on the target host - It cryptographically signs the resulting binary with a local machine-owner key so the kernel will allow it to run on a host that has Secure Boot enforcement turned on
- It automatically subscribes the new package to upstream security updates fetched over the network, so the manager re-pulls and rebuilds a fresh binary whenever a CVE is published
The packaged version of a tool is too old. What should you try before compiling it yourself?
- A backports repository, the upstream's own signed
aptrepo, or a PPA — all keep the newer version patched by the manager - Force the install with
dpkg --force-overwriteto pull a newer file into place over the old one, letting the local archive's package win the file-ownership conflict - Run
apt upgrade --allow-downgradesto jump the package straight to the upstream release that the project published on its own download page - Edit the version string in
/var/lib/dpkg/statusby hand so the manager believes an older release is installed and fetches the newer build to replace it
You got correct