Blog entry
DevOps journey - #1 - Linux Basics
I'm working through a self-paced bootcamp, and this module covered the foundational module: operating systems, virtualisation, and the Linux fundamentals that any DevOps engineer relies on daily.
Introduction
This is the first proper entry of my DevOps learning journey. I'm working through a self-paced bootcamp, and this foundational module covered: operating systems, virtualisation, and the Linux fundamentals that any DevOps engineer relies on daily.
If you come from a development background where most of the work happens inside an IDE, this is the area that tends to be under-explored. Servers don't have a desktop, they have a terminal, and before you can deploy anything, you need to be comfortable working in that environment.
The big picture
An operating system sits between hardware and software, managing the CPU, memory, storage, I/O devices, and security. Linux runs the majority of servers in production today, and most modern DevOps tooling, including Docker, Kubernetes, and most cloud-native services, was built on Linux first and ported elsewhere afterwards.
For that reason, Linux fluency is one of the core competencies for the role. The goal of this module was to move beyond basic navigation with cd and ls and become comfortable with piping commands, debugging a permissions issue, writing a bash script, and connecting to a remote machine over SSH.
Key concepts
Virtualisation and VMs
A virtual machine runs one operating system on top of another, managed by a hypervisor. Type 2 hypervisors, such as VirtualBox, sit on top of a host OS and are well suited to learning. Type 1 hypervisors, such as VMware ESXi, run directly on hardware and underpin cloud platforms like AWS.
I skipped the VirtualBox install because I work on Windows with WSL2, which provides a full Ubuntu environment without the overhead of a traditional VM. It uses the same Linux kernel, consumes far less memory, and integrates cleanly with VS Code. I strongly suggest exploring this option, and even some customisation with Zsh and Oh My Zsh (https://ohmyz.sh/)
The Linux file system
There is a single root ( / ) and a single tree. Each top-level directory has a defined purpose:
/home/<user>: the user's personal folder./etc: system configuration files./var: logs and cache./usr/binand/usr/sbin: most of the system commands./usr/local: software installed manually at the system level./opt: self-contained third-party applications such as IDEs and browsers./tmp: temporary files./dev,/boot,/media,/mnt: devices, boot files, and mounted media.
It's worth getting to know these dirs, as with time you'll often access them to work with specific files.
CLI essentials
A small set of commands covers most everyday usage:
pwd # print working directory
ls -la # list all files, including hidden
cd /path/to/folder # change directory
mkdir folder # create a new folder
touch file.txt # create a new empty file
rm file.txt # delete a file
rm -r folder # delete a folder recursively
mv old new # rename or move
cp -r src dst # copy (use -r for folders)
cat file # print file contents
less file # paginated view
history # show previous commands
Tab completion and reverse history search ( Ctrl+R ) saved more time than any individual command.
Package management with APT
On Ubuntu, software is installed via APT rather than by downloading installers from arbitrary websites.
sudo apt update # refresh the package index
sudo apt search <name> # check whether a package is available
sudo apt install -y <package> # install (the -y flag skips prompts)
sudo apt remove <package> # uninstall
The general order of preference is APT first, Snap where appropriate, and third-party PPAs only when the source is trusted.
Users, groups, and permissions
There are three categories of users: root (the super user, ID 0), regular users, and service users, which are dedicated to individual services for isolation.
Every file has a user owner, a group owner, and three permission blocks (user, group, other). Each block contains read (r), write (w), and execute (x) flags.
ls -l # show ownership and permissions
sudo chown user:group file # change ownership
chmod u+x script.sh # add execute permission for the user owner
chmod 740 file # rwx for user, r for group, none for other
The numeric form becomes intuitive once you recognise that each digit is the sum of 4 (read), 2 (write), and 1 (execute).
Pipes, redirects, and grep
Combining commands is where the terminal becomes genuinely productive.
history | grep sudo # filter
cat /var/log/syslog | less # paginate
history | grep sudo > sudo-cmds.txt # redirect to file (overwrite)
history | grep sudo >> sudo-cmds.txt # append
Commands can be chained with |, and output can be redirected with > or >>. Those primitives cover most day-to-day shell work.
Bash scripting
A bash script is a plain file of commands with a shebang on the first line:
#!/bin/bash
echo "hello"
From there, the language provides variables, conditionals, loops, functions, and parameters. Two parameter forms come up constantly:
$1,$2, and so on for individual arguments passed to the script.$#for the number of arguments, and$*for the full list.
The if syntax takes a little getting used to:
if [ -d "$1" ]; then
echo "$1 is a directory"
elif [ -f "$1" ]; then
echo "$1 is a file"
else
echo "$1 is neither a file nor a directory"
fi
Environment variables
Environment variables are key-value pairs available to the current shell session and any process launched from it. They are set with export:
export DB_PASSWORD=supersecret
By default they exist only for the current session. Assuming you are using bash, to persist them for the user, add them to ~/.bashrc and reload with source ~/.bashrc. System-wide variables belong in /etc/environment.
PATH is particularly worth knowing well. It defines the list of directories the shell searches when a command is invoked. Adding a directory to PATH allows custom scripts to be called as first-class commands.
After that, I went over some networking basics, simple concepts from DNS, LAN, WAN, etc.
SSH
Secure Shell is the standard way to connect to remote servers. Authentication can use a password (acceptable) or an SSH key pair (preferred).
ssh-keygen -t rsa # generate a key pair
ssh user@server-ip # connect
ssh -i ~/.ssh/custom_key user@server # connect using a specific key
scp local-file user@server-ip:/path/ # copy a file to the server
The private key (~/.ssh/id_rsa) stays on the local machine, while the public key (~/.ssh/id_rsa.pub) is added to the server's ~/.ssh/authorized_keys. SSH listens on port 22 by default and should be firewalled to a small allow-list of trusted IPs.
Hands-on: what I built
The end-of-module exercise was to write a bunch of different scripts, from simpler to more complex ones. I want to share a couple of examples below:
First. A bash script that installs Java and then checks three conditions: whether Java is installed at all, whether the installed version is older than 11, and whether it is version 11 or newer.
Nothing fancy or polished, just a simple example to start familiarising with writing scripts.
#!/bin/bash
sudo apt update
sudo apt install -y openjdk-17-jre-headless
if command -v java
then
echo "java is installed"
else
echo "java not installed yet"
fi
version=$(java -version 2>&1 | awk '{print $3}' | head -n 1 | tr -d '"')
major=$(echo $version | cut -d. -f1)
if [ "$major" -lt 11 ]
then
echo "Java version is older than 11 ($version)"
else
echo "Java version is 11 or newer ($version)"
fi
A few details worth noting:
java -versionwrites to stderr rather than stdout, which is why the output is redirected with2>&1before being piped toawk.awk '{print $3}'extracts the third whitespace-separated field, which is where the version string sits.tr -d '"'strips the surrounding double quotes from the version string. Without it, the numeric comparison fails.cut -d. -f1splits on the dot character and returns the first segment, which is the major version number.
A second exercise was to write an interactive script that reports the current user's running processes, sorted by either CPU or memory usage, with the number of lines to display chosen at runtime.
#!/bin/bash
cuser=$USER
echo "current user is $cuser"
echo ""
echo -n "Input cpu or mem to order accordingly: "
read order
echo -n "input number of lines to display: "
read number
if [ "$order" == "cpu" ]
then
proc=$(ps aux --sort=-%cpu | grep "$cuser" | head -n "$number")
else
proc=$(ps aux --sort=-%mem | grep "$cuser" | head -n "$number")
fi
echo "current running processes for $cuser are:"
echo "$proc"
A few details worth noting:
$USERis an environment variable the shell sets automatically, so the script can identify the current user without prompting for it.readpauses execution and stores the user's keyboard input in the named variable (order, thennumber), which is what makes the script interactive rather than argument-driven.ps aux --sort=-%cpusorts the process list by CPU usage, and the leading-reverses the order so the heaviest consumers appear first. Switching to--sort=-%memorders by memory instead.- Piping through
grep "$cuser"narrows the full process list down to just the current user's processes. head -n "$number"then trims the result to the requested number of lines.- Quoting the variable in
[ "$order" == "cpu" ]keeps the test reliable if the input is ever empty, for the same reason quoting mattered in the previous script.
Again, the script can be improved a lot, but I wanted to share simpler examples. The key concept here is to practice simple scripts first and build on them incrementally to learn new concepts a step at a time.
What tripped me up
Environment variables and PATH. Setting a variable with export only lasts for the current shell session. To make a variable persist for my user I had to add it to ~/.zshrc (and reload with source ~/.zshrc), while system-wide values belong in /etc/environment. The same lesson applied to PATH: rather than overwriting it, the safe pattern is to append to it, for example export PATH="$PATH:/new/dir". Forgetting the existing $PATH on the right-hand side wipes out the directories the shell relies on and breaks ordinary commands until the session is restarted. Which I did and panicked, thinking I messed up big time ๐
Bash syntax is picky, especially around $. The shell is far less forgiving than the languages I am used to, and most of my early errors came down to small details with the $ symbol. $var reads a variable, ${var} is needed when the name sits next to other characters, and $(command) captures command output, but they are easy to mix up. Spacing inside test brackets is mandatory ([ "$x" -lt 11 ], never ["$x"-lt 11]), and quoting variables is not optional: [ "$var" -lt 11 ] works reliably, whereas [ $var -lt 11 ] can fail without any feedback when the variable is empty or contains spaces. Getting the whitespace and quoting right turned out to matter as much as the logic itself.
Takeaways
- Linux is an environment to become fluent in rather than a product to memorise. Comfort in the terminal makes the rest of the DevOps stack significantly easier to learn.
- Most productivity comes from a small set of commands combined with the primitives that join them: pipes, redirects,
grep, andless. - Bash scripting is the first real step into automation. Anything you find yourself doing twice is a candidate for a script.
- Practising building actual scripts is the right way to actually memorise and understand how commands work and interact with pipes.
- Permissions and SSH keys are not optional knowledge. They are the entry point to every server you will work on.
What's next
Next come two refreshers rather than new ground: Git (branches, merges, rebases, pull requests and team workflows), followed by a quick pass over databases and how a deployed application actually connects to one.
After that, build tools and package managers: artifacts and artifact repositories, Maven and Gradle for Java, npm and Yarn for JavaScript, and Webpack for bundling front-end code. The interesting part is how much of it Docker ends up replacing with a single universal artifact.
Which leads straight into Docker and CI/CD, where this course starts getting properly interesting.
See you on the next one ๐
Resources
- WSL2 installation guide - the route I took instead of a VirtualBox VM.
- Ubuntu downloads - if you would rather run a full VM or dual boot.
- Oh My Zsh - well worth the ten minutes it takes to set up a nicer shell.
- Bash scripting cheat sheet (devhints) - the page I kept open while writing the exercises.
- ShellCheck - catches the quoting and spacing mistakes described above before you run the script.
- explainshell - paste a command and it breaks down every flag.
- DigitalOcean (used for the SSH demo) - cheap throwaway servers for practising remote access.