Operating System

Linux

Linux runs the vast majority of servers, containers and cloud machines. The handful of ideas behind it — everything is a file, processes are cheap, permissions are explicit — unlock every machine you will ever SSH into.

beginner15 min readUpdated Sep 16, 2026
server.sh
bash
# Update packages, then inspect and follow a service
sudo apt update && sudo apt upgrade -y
systemctl status nginx --no-pager
journalctl -u nginx -f
ss -tulpn | grep ':443'
Created
1991
Creator
Linus Torvalds
License
GPLv2
Kernel type
Monolithic
Default shells
bash, zsh, sh
Runs on
Servers, containers, phones, routers

Why it matters

Why every server you touch runs Linux

One interface, everywhere

The same commands work on your laptop, a CI runner, a container and a cloud VM. Learn the shell once and you can inspect any machine you can reach.

Permissions are explicit

Every file has an owner, a group and a mode. Nothing runs with more access than you grant it, which is what makes shared servers safe.

Software is one command away

apt, dnf and apk install, upgrade and remove software with dependency resolution, so a fresh machine becomes useful in minutes.

The big picture

Three ideas that explain the whole system

A kernel mediates access to hardware, a shell turns text into commands, and processes are cheap enough to run thousands at once.

The kernel

Mediate

It schedules processes, manages memory and presents devices as files. Everything above it talks to hardware through it.

The shell

Command

A read-eval loop over text. You type a program name and arguments, and the shell finds the binary on your PATH and runs it.

Processes

Run

Every running program is a process with an id, an owner and a parent. Services are just long-lived processes the init system supervises.

Linux at a glance

The tools you will actually use

The filesystem

A single tree rooted at / with conventional homes for config, logs and programs.

The shell

bash or zsh with history, tab completion, pipes and redirection.

Permissions

rwx bits for owner, group and others, plus chmod and chown.

Processes

ps, top, kill and signals to inspect and control running work.

systemd

systemctl and journalctl to run and debug long-lived services.

Networking

ss, curl and dig to see ports, fetch URLs and resolve DNS.

A short history

From a hobby kernel to the cloud's default

  1. 1991

    The kernel appears

    Linus Torvalds releases a free Unix-like kernel and invites contributors.

    91
  2. 1992

    GNU meets Linux

    Combining the GNU tools with the kernel produces a complete free operating system.

    92
  3. 1993

    Distributions form

    Debian and Red Hat package the kernel with tools, giving users an installable system.

    93
  4. 2004

    Ubuntu makes it friendly

    A predictable release cadence and desktop focus bring Linux to a much wider audience.

    04
  5. 2013

    Containers run on it

    Docker uses Linux namespaces and cgroups to isolate processes, starting the container era.

    13
  6. Today

    The default server OS

    Most public cloud instances, containers and the mobile devices in your pocket run a Linux kernel.

    Today

The complete guide

Linux: Everything you need to know

Why Linux runs the server world

Walk into any data centre, open any cloud console, or docker exec into any container and you are almost certainly on a Linux kernel. It powers the majority of web servers, every Android phone, most routers and the vast majority of cloud instances. The reasons are old and durable: it is free, it runs on almost any hardware, it is stable enough to stay up for years, and it is scriptable from top to bottom.

For a developer the practical point is narrower. The moment you deploy, you stop being a person who edits files in an editor and become a person who types commands into a shell, often over SSH, on a machine you cannot see. The skills in this guide are the ones that make that machine legible instead of frightening.

You do not need to become a system administrator. You need enough fluency to move around, read logs, fix permissions, restart a service and understand what is listening on a port.

WSL and macOS: you already know more than you think

If you use macOS, you are already on a Unix system. The terminal, the filesystem layout and most commands — ls, cd, grep, chmod, ssh — behave the same way. The differences are mostly in the package manager and in a few BSD-versus-GNU flags.

On Windows, WSL 2 gives you a real Linux kernel inside a lightweight virtual machine. wsl --install gets you Ubuntu with a shell, and your files are reachable from both sides. This is the recommended path because it is the same environment your code will run in.

Even if you never install Linux directly, the mental model transfers completely to containers, which are Linux userland processes wearing a very convincing costume.

The filesystem is one tree

Unlike Windows, where drives are separate roots, Linux has a single tree rooted at /. Every disk, network share and device is mounted somewhere inside it. A handful of directories have conventional meanings, and knowing them means you always know where to look.

/           the root of everything
├── bin     essential user binaries (ls, cp, bash)
├── etc     system-wide configuration files
├── home    per-user home directories
├── opt     optional third-party software
├── proc    kernel and process state, as files
├── root    the root user's home
├── tmp     temporary files, cleared on reboot
├── usr     installed programs and libraries
│   ├── bin local binaries
│   └── lib shared libraries
└── var     variable data: logs, caches, databases
    ├── log system and application logs
    └── www web content on Debian systems

The two you will visit most are /etc for configuration and /var/log for logs. When something is wrong, those are the first places to look. When you install an application yourself, /opt or /usr/local keeps it out of the way of the package manager.

Paths are either absolute (start with /, like /etc/hosts) or relative (resolved from your current directory). ~ is a shortcut for your home directory, and . is the current directory, .. its parent. Those three symbols appear in nearly every command you type.

Everything is a file

The deepest Unix idea is that almost everything is represented as a file. A hard disk is /dev/sda. A terminal is /dev/pts/0. Running process information lives under /proc/<pid>/. Random numbers come from /dev/urandom. A Unix domain socket is a file too.

This uniformity is what makes the shell so powerful. The same cat, grep, less and redirection operators work on a log file, a process’s memory map and a device, because they are all just streams of bytes behind a path. When you understand that, /proc and /sys stop looking like magic and become inspectable.

cat /proc/cpuinfo | grep 'model name' | head -1
ls -l /dev/null          # the bit bucket

The shell is a loop: it reads a line, expands it, finds the program and runs it. Six commands cover most navigation.

pwd                     # print working directory
ls -lah                 # list, long format, human sizes, hidden files
cd /var/log             # change directory
cd -                    # back to the previous directory
cd ~                    # home
tree -L 2               # a readable tree, two levels deep

Creating, copying, moving and removing files is just as small a vocabulary. mkdir -p creates parents as needed. cp -r copies directories recursively. mv renames within a filesystem and moves across them. rm -rf deletes a tree, which is why it deserves respect: there is no trash can on the command line.

mkdir -p app/{src,test,docs}
cp -r app app.bak
mv notes.txt docs/notes.txt
rm -rf build/

Two habits keep you safe. Use ls or find to confirm what a glob matches before you run rm with it, and prefer mv to a backup directory over deleting something you are unsure about. Shell history is not a safety net when you press Enter.

Reading files without opening an editor

You rarely need a full editor to inspect something on a server. These tools print just the part you need.

cat /etc/os-release          # print the whole file
less /var/log/syslog         # page through, / to search, q to quit
head -n 20 access.log        # first 20 lines
tail -n 50 access.log        # last 50 lines
tail -f app.log              # follow a live log
wc -l access.log             # count lines

tail -f is the single most useful command during an incident: it streams new lines as they are written. Press Ctrl+C to stop. less is faster than an editor and never accidentally modifies the file, which matters on production.

Searching with find and grep

find walks the filesystem and matches on metadata; grep searches inside file contents. Together they answer most “where is it?” questions.

find /etc -name '*.conf'                 # by name
find . -type d -name node_modules -prune -o -type f -name '*.ts' -print
find /var/log -type f -mtime -1          # modified in the last day
find . -type f -size +50M                # larger than 50 MB
find . -type f -name '*.tmp' -delete     # delete matches
grep -RIn 'TODO' src/                    # recursive, line numbers, ignore binary
grep -c 'ERROR' app.log                  # count matches
grep -E 'timeout|refused' app.log        # extended regex
grep -v '^#' config.ini                  # invert: drop comments

grep returning nothing is a success, not an error, but it exits with status 1. That trips up scripts under set -e; append || true when an empty result is acceptable.

Permissions: rwx and the octal shorthand

Every file has an owner, a group and three permission bits for each: read (r), write (w) and execute (x). ls -l shows them as a ten-character string.

-rwxr-xr--  1 deploy www-data  512 Sep 16 10:00 deploy.sh
The ten-character permission string
-rwxr-xr--
type- file, d directory, l symlink
ownerread, write and execute for the owner
groupread and execute for the group
othersread only for everyone else

Octal is the shorthand. Each triple is the sum of 4 (read), 2 (write) and 1 (execute), so rwx is 7, r-x is 5 and r-- is 4. That makes 755 mean rwxr-xr-x and 644 mean rw-r--r--.

chmod 755 deploy.sh          # scripts and directories
chmod 644 index.html         # regular files
chmod 600 .env               # secrets only the owner can read
chmod u+x,g-w,o-rwx script   # symbolic form, easier to review

Directories need the execute bit to be entered, which is why removing x from a directory hides its contents even if read is still set. This is a subtle source of “permission denied” when read looks fine.

Ownership, groups and umask

chown changes the owner and group; chgrp changes only the group. Prefix with sudo because only root can give files away.

sudo chown deploy:www-data /var/www/app
sudo chgrp -R www-data /var/www/app/uploads

New files get their permissions from the umask, a mask subtracted from the default. A umask of 022 yields 644 for files and 755 for directories; 027 yields 640 and 750. Set it in your shell profile or a service’s unit file when a stricter default is warranted.

umask            # show current, e.g. 0022
umask 027        # stricter for this shell

sudo and the least-privilege habit

sudo runs one command as another user, root by default, after checking /etc/sudoers. It is deliberately narrow: grant the specific commands a person or service needs rather than handing out a root shell. Prefer sudo -u www-data <cmd> over editing files as root and leaving them owned by root.

sudo systemctl restart my-api
sudo -u postgres psql -c '\l'
sudo -l                      # what am I allowed to run?

A rule of thumb: root owns system configuration, a dedicated service user owns application files, and your app never runs as root. If a compromise of your app would give an attacker root on the host, permissions are doing nothing for you.

Users and groups

Users are identified by a name and a numeric uid; groups by a name and a gid. The mapping lives in /etc/passwd and /etc/group, while hashed passwords live in /etc/shadow, readable only by root.

whoami                       # current user
id                           # uid, gid and groups
groups deploy                # groups a user belongs to
sudo adduser appuser         # create a user (Debian/Ubuntu)
sudo usermod -aG docker appuser   # add to a group
sudo userdel -r appuser      # remove the user and home

Service accounts usually have no password and no login shell. That is intentional: they exist to own files and run one process, not to be logged into.

Processes: ps, top and signals

Every running program is a process with a pid, an owner and a parent. ps takes a snapshot; top and htop update live.

ps aux | grep node           # find a process
ps -eo pid,ppid,user,%cpu,%mem,cmd --sort=-%cpu | head
top -o %MEM                  # interactive, sort by memory
pgrep -af node               # pids and full command lines

Processes communicate through signals. SIGTERM (15) asks a process to exit and is the default for kill; SIGINT (2) is Ctrl+C; SIGHUP (1) traditionally means reload; SIGKILL (9) cannot be caught and should be a last resort.

kill 1234            # SIGTERM, graceful
kill -HUP 1234       # reload config
kill -9 1234         # force, no cleanup
pkill -f 'node server.js'

Send SIGTERM first and give the process a few seconds to close connections and flush state. Escalate to -9 only when it is genuinely stuck, because it skips every cleanup path your app has.

Background jobs, & and nohup

Appending & runs a command in the background; jobs, fg and bg manage it from the current shell. But a background job still dies when you close the terminal unless you detach it from the hangup signal with nohup or a terminal multiplexer.

long-task &              # background in this shell
jobs -l                  # list background jobs
fg %1                    # bring job 1 to the foreground
nohup ./worker.sh > worker.log 2>&1 &
disown                   # detach from the shell's job table

For anything long-lived, use tmux or screen. They keep a session alive across disconnects, which is invaluable when a deploy takes longer than your SSH connection.

tmux new -s deploy
# Ctrl+B, then D to detach
tmux attach -t deploy

systemd services with systemctl

On modern distributions, systemd is the init system that starts services at boot and supervises them. You define a service in a unit file under /etc/systemd/system/.

# /etc/systemd/system/my-api.service
[Unit]
Description=My API
After=network.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/my-api
Environment=NODE_ENV=production
EnvironmentFile=/opt/my-api/.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target

After creating or editing a unit, reload the manager and enable the service so it starts on boot.

sudo systemctl daemon-reload
sudo systemctl enable --now my-api
systemctl status my-api --no-pager
sudo systemctl restart my-api
sudo systemctl reload my-api      # if the service supports it

Restart=on-failure is what turns a crashed process into a self-healing service. EnvironmentFile keeps secrets out of the unit, which is often world-readable. Use Type=notify only if the program actually supports the notification protocol; simple is the safe default.

Logs with journalctl

systemd captures a service’s stdout and stderr into the journal. journalctl queries it, and you will use it constantly.

journalctl -u my-api                 # all logs for one unit
journalctl -u my-api -n 100          # last 100 lines
journalctl -u my-api -f              # follow live
journalctl -u my-api --since today
journalctl -u my-api --since '10 min ago' -p err
journalctl -b -p warning             # this boot, warnings and worse
journalctl --disk-usage
sudo journalctl --vacuum-time=7d     # keep a week

The -p flag filters by priority, from emerg down to debug. Following a unit with -f while you hit an endpoint is the fastest way to see a 500 explained.

Environment variables and PATH

Environment variables are key-value strings a process inherits from its parent. PATH is the important one: it lists the directories the shell searches for a command name, in order.

echo "$PATH" | tr ':' '\n'
export NODE_ENV=production
export PATH="$HOME/.local/bin:$PATH"
printenv DATABASE_URL

Because a service does not inherit your interactive shell, set variables in its unit file or an EnvironmentFile. A variable that works in your terminal and not under systemd is almost always this. To persist your own settings, add the export lines to ~/.bashrc or ~/.profile.

Package managers: apt, dnf, apk

Each distribution family has its own package manager, but the verbs are similar: update the index, install, upgrade, remove, search.

# Debian / Ubuntu
sudo apt update && sudo apt upgrade -y
sudo apt install nginx
sudo apt remove --purge nginx

# Fedora / RHEL
sudo dnf install nginx
sudo dnf upgrade --refresh

# Alpine (common in containers)
sudo apk add nginx
sudo apk upgrade

Install only from official repositories where possible; they are signed and patched. Reach for a PPA or third-party repo deliberately, and pin versions in production images so a rebuild does not silently change what runs.

Pipes, redirection and small scripts

A pipe sends one program’s output to another’s input. Redirection sends it to a file or reads it from one. Together they turn small tools into pipelines.

ls -l | wc -l                     # count files
grep ' 500 ' access.log | wc -l   # count 500s
cmd > out.txt                     # stdout to file, overwrite
cmd >> out.txt                    # append
cmd 2> err.txt                    # stderr to file
cmd > all.txt 2>&1                # both streams together
cmd < input.txt                   # read stdin from a file

Every process starts with three file descriptors: stdin (0), stdout (1) and stderr (2). 2>&1 means “send stderr where stdout is going”, and the order matters — it must come after the stdout redirection.

Wrap a sequence in a script and you have automation. Two lines at the top make scripts far safer:

#!/usr/bin/env bash
set -euo pipefail

-e exits on the first failed command, -u errors on an undefined variable, and -o pipefail makes a pipeline fail if any stage fails. Together they turn silent partial failures into loud ones, which is exactly what you want in a deploy script.

SSH and keys

SSH is how you reach a remote machine. Password logins work, but keys are stronger and scriptable. You keep a private key secret and put the matching public key on the server.

ssh-keygen -t ed25519 -C "you@laptop"      # creates ~/.ssh/id_ed25519(.pub)
ssh-copy-id [email protected]            # install the public key
ssh [email protected]                    # log in

A host alias in ~/.ssh/config saves typing and pins useful options:

Host prod
  HostName 203.0.113.10
  User deploy
  IdentityFile ~/.ssh/id_ed25519
  ServerAliveInterval 30

Then ssh prod connects. Once you trust keys, disable password authentication in /etc/ssh/sshd_config (PasswordAuthentication no) and reload sshd. scp and rsync reuse the same keys for copying files; rsync -avz --delete is the usual way to sync a directory.

rsync -avz --exclude node_modules ./app/ prod:/opt/app/
ssh prod 'sudo systemctl restart my-api'

Disk and memory: df, du, free

Servers rarely fail because of CPU; they fail because a disk fills up or memory runs out. These commands answer “how much is left?” and “what is using it?”.

df -h                       # free space per filesystem
df -i                       # inode usage — a disk can be full of inodes
du -sh /var/log/*           # size per entry
du -h --max-depth=1 / | sort -h
free -h                     # memory and swap
ls -lhS /var/log | head     # biggest log files

When /var is full, the cause is almost always logs, a package cache or a runaway database. Clean up deliberately — journalctl --vacuum-time, apt clean, log rotation — rather than deleting files an application expects.

Networking: ss, curl, dig

ss shows sockets and listening ports, replacing the older netstat. It answers “is anything listening, and who is connected?”.

ss -tulpn                   # TCP, UDP, listening, process names
curl -I https://example.com # response headers only
curl -sS localhost:3000/health
curl -X POST -H 'content-type: application/json' \
  -d '{"name":"ada"}' localhost:3000/users
dig example.com +short      # DNS resolution
dig @1.1.1.1 example.com MX

curl -I is the quickest way to confirm a server is responding and what status it returns. If ss shows nothing on a port but the app says it started, it may be bound to 127.0.0.1 instead of 0.0.0.0, which is invisible from outside the host.

Scheduling with cron

cron runs commands on a schedule. Each user has a crontab with five time fields followed by the command.

# ┌ min (0-59)
# │ ┌ hour (0-23)
# │ │ ┌ day of month (1-31)
# │ │ │ ┌ month (1-12)
# │ │ │ │ ┌ day of week (0-6, Sun=0)
# * * * * * command
crontab -e                    # edit your crontab
crontab -l                    # list it
0 3 * * * /opt/app/backup.sh >> /var/log/backup.log 2>&1
*/5 * * * * curl -fsS localhost:3000/health > /dev/null
0 7 * * 1 /opt/app/weekly-report.sh

Cron’s environment is minimal: it has a different PATH, no shell profile and no interactive variables. Use absolute paths, redirect output to a log so failures are visible, and remember that cron does not retry a failed run. For anything more complex, schedule a systemd timer or enqueue a job instead.

Best practices

  • Use absolute paths in scripts and unit files; never rely on the interactive environment.
  • Put set -euo pipefail at the top of every non-trivial shell script.
  • Grant least privilege: dedicated service users, sudo for specific commands, no app running as root.
  • Prefer SIGTERM and let services shut down gracefully before reaching for kill -9.
  • Configure Restart=on-failure and read journalctl -u <unit> when debugging.
  • Keep configuration in version control and change one thing at a time on a live host.
  • Rotate logs and watch df -h and df -i; disks fill quietly.
  • Use SSH keys, not passwords, and keep private keys off servers.
  • Learn find, grep, awk and pipes — they replace a lot of one-off tooling.
  • Test a config before reloading the service that reads it.

Common mistakes

  • Running applications as root and turning any bug into a full compromise.
  • Using chmod -R 777 to “fix” permissions and removing every protection between users.
  • Deleting files with an unexamined wildcard and no backup.
  • Sending kill -9 first and corrupting state that a graceful shutdown would have flushed.
  • Setting environment variables in your shell and expecting systemd to see them.
  • Assuming a service is down when it is bound only to 127.0.0.1.
  • Forgetting 2>&1 in cron entries and losing the error that explains a failed job.
  • Editing config directly on a server with no version control and no way back.
  • Ignoring inode exhaustion because df -h still shows free space.
  • Leaving password authentication enabled on an internet-facing SSH port.

Where to go next

Linux is the substrate under everything else you deploy. Once you can move around a box, read its logs and restart its services, the Nginx guide shows the web server you will configure in front of your app, and Docker & Deployment turns these primitives into repeatable, isolated images. To automate the same commands on every push, read CI/CD, and to understand the process your service actually runs, revisit Node.js Basics.

In practice

The shell commands that cover most days

Four groups worth committing to memory before you touch a production box.

files.sh
pwd                          # where am I?
ls -lah /var/log             # long, human sizes, hidden files
cd /etc/nginx && cd -        # go there, then back
mkdir -p app/{src,test}      # create nested dirs in one shot
cp -r src backup/            # copy a tree
mv old.txt new.txt           # rename or move
rm -rf node_modules          # delete a tree (be careful)

# find files by name, size or age
find /var/log -name '*.log' -mtime -7
find . -type f -size +100M -exec ls -lh {} \;

chmod 755 vs chmod 777

777 removes the protection between users. Almost nothing needs it, and a web-writable directory is a common way a site gets compromised.

Prefer
chmod 755 script.sh   # rwxr-xr-x
chmod 644 index.html  # rw-r--r--
chmod 600 .env        # rw-------
chmod 700 ~/.ssh      # rwx------
Avoid
chmod -R 777 /var/www
# anyone on the box can edit
# or replace your code

kill vs kill -9

SIGTERM asks a process to shut down so it can close connections and flush state. SIGKILL cannot be caught and gives it no chance.

Prefer
kill 1234        # SIGTERM: graceful
# wait, then escalate only
# if it is truly stuck
Avoid
kill -9 1234     # SIGKILL: no cleanup
# can corrupt files and
# drop in-flight requests

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Linux Basics?

Our interactive tutorial walks you through Linux Basics step by step — with quizzes and real code you can run in the browser.