You closed Module 2 with the judgement to decide who accesses what. You start Module 3 with something more intimate: the place where you work. Every time you open a session on srv-tramontana, Bash builds an environment around you — a set of variables, shortcuts, search paths and memory of what you have already done — that determines how every command you run afterwards behaves. Until now you have used it exactly as it came out of the box. From here on you understand it and you shape it.
This is not an aesthetic whim. 90% of a systems administrator's "but it worked for me" moments — the script that runs from your terminal and fails in cron, the sort that orders things differently on your laptop and on the server, the command that exists for operator but not for root — are environment problems. Understanding what is inherited, what is not, and from which file each thing is loaded is what separates debugging in five minutes from losing an afternoon.
Contents
- What a process's environment is
- Shell variables and environment variables
- Inheritance by child processes
PATH: how Bash finds an executable- The other variables that matter
- Variable expansion and quoting, precisely
- Command substitution
- Aliases
- Login, interactive and non-interactive shells
- Customising the
PS1prompt - Persistent history and its security risk
- The recommended
~/.bashrcforoperator
- What a process's environment is
When the kernel starts a process, it hands it three things: the command-line arguments, the open file descriptors (which you will see in 03-04) and a vector of KEY=value strings called the environment. It is not Bash magic: it is an operating system structure, available to any program written in any language.
Since on Linux everything is a file, you can look at it directly:
operator@srv-tramontana:~$ tr '\0' '\n' < /proc/$$/environ | head -6
SHELL=/bin/bash
PWD=/home/operator
LOGNAME=operator
HOME=/home/operator
LANG=en_GB.UTF-8
USER=operator$$ is the PID of the current shell. The environ file stores the variables separated by null bytes, which is why tr is needed to read them. The important part: that content was fixed when the process started. It is a photograph, not a live link.
- Shell variables and environment variables
Bash handles two sets that look alike and are not the same thing.
| Shell variable | Environment variable | |
|---|---|---|
| Created with | VAR=value |
export VAR=value |
| Visible to the current shell | Yes | Yes |
| Visible to child processes | No | Yes |
| Listed with | set |
env or printenv |
| Typical use | temporary work within the session | configuring programs |
Assignment has one rule that causes errors on day one: there must be no spaces around the =.
operator@srv-tramontana:~$ RELEASE=3.2.1
operator@srv-tramontana:~$ RELEASE = 3.2.1
RELEASE: command not foundWith spaces, Bash reads RELEASE as a command and = and 3.2.1 as its arguments. If the value contains spaces, it has to be quoted: MESSAGE="deploy ok".
To promote an existing variable to an environment variable, export RELEASE is enough. To remove either kind, unset RELEASE.
Looking at one in particular:
printenv VARIABLE prints its value and returns 1 if the variable is not in the environment, even if it exists as a shell variable. It is the quickest way to check whether you are missing an export.
- Inheritance by child processes
This is the concept that has to be nailed down, because it explains a whole family of errors. Inheritance is one-way and happens at start-up: the child receives a copy of the parent's environment, and nothing the child does comes back to the parent.
operator@srv-tramontana:~$ export ENVIRONMENT=production
operator@srv-tramontana:~$ bash -c 'echo "the child sees: $ENVIRONMENT"; ENVIRONMENT=test'
the child sees: production
operator@srv-tramontana:~$ echo "the parent still has: $ENVIRONMENT"
the parent still has: productionThe child changed its copy and died with it. That is why a script cannot change your terminal's working directory, nor define a variable for you, unless you run it with source (or its synonym .), which does not create a child process: it reads the file in the current shell.
operator@srv-tramontana:~$ echo 'APP_VERSION=3.2.1' > /tmp/vars.sh
operator@srv-tramontana:~$ bash /tmp/vars.sh; echo "[$APP_VERSION]" # child: it is lost
[]
operator@srv-tramontana:~$ source /tmp/vars.sh; echo "[$APP_VERSION]" # current shell
[3.2.1]Parentheses create a subshell, which is just another child and behaves the same way: ( cd /opt/tramontana/app && pwd ) prints /opt/tramontana/app and leaves you where you were. That pattern is genuinely useful: you move somewhere, do something and come back on your own, with no risk of forgetting a cd -.
PATH: how Bash finds an executable
PATH: how Bash finds an executableIn Module 2 it was left as a notion. Now in detail. PATH is a list of directories separated by :, and Bash walks it from left to right, stopping at the first match.
operator@srv-tramontana:~$ echo "$PATH"
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
operator@srv-tramontana:~$ type -a python3
python3 is /usr/bin/python3type -a shows all the matches in order; if there were two versions installed, you would see which one wins. Bash also caches the paths it has resolved: if you install a binary that masks another one and the old one keeps running, hash -r clears that cache.
To add /home/operator/scripts, the correct way is to prepend or append without destroying what was already there:
Note two details. First, $PATH is reused inside the new value; writing a bare PATH=/home/operator/scripts leaves the system without ls or sudo until you close the session. Second, it is quoted.
Putting it in front, your scripts take priority over the system's, which lets you deliberately override a command; putting it behind (PATH="$PATH:$HOME/scripts") the system always wins, which is more conservative. At Tramontana we use the second order for the administrative accounts.
Why . never goes in the PATH
Including the current directory looks convenient and is a classic security hole. Imagine . is first and that Luis, while debugging, leaves a file called ls with malicious content in /srv/tramontana/backups/temp/. You go in there to have a look, you type ls, and instead of the system command you run that directory's file with your privileges. With sudo in the mix, it is a complete privilege escalation.
The rule is absolute: . does not go in the PATH, neither at the beginning nor at the end. To run something from the current directory you write ./program, explicitly. Those two characters are the difference between a conscious decision and an accident.
- The other variables that matter
| Variable | What it contains | Practical note |
|---|---|---|
HOME |
/home/operator |
It is what ~ expands to and where cd goes with no arguments |
USER / LOGNAME |
operator |
Informational; for real identity use id -un |
SHELL |
/bin/bash |
It is your login shell, not necessarily the one running now |
PWD / OLDPWD |
current and previous directory | cd - uses OLDPWD |
LANG / LC_ALL |
language and locale | Affects messages, ordering and date formats |
EDITOR / VISUAL |
default editor | Used by crontab -e, git, visudo |
TERM |
terminal type | xterm-256color locally, vt100 on old consoles |
PS1 |
the prompt string | We customise it in section 10 |
The locale deserves a paragraph of its own because it produces silent differences between machines:
operator@srv-tramontana:~$ printf 'house\nHouse\ncal-ferrer\nCan-Ventos\n' | sort | tr '\n' ' '
cal-ferrer Can-Ventos house House
operator@srv-tramontana:~$ printf 'house\nHouse\ncal-ferrer\nCan-Ventos\n' | LC_ALL=C sort | tr '\n' ' '
Can-Ventos House cal-ferrer houseWith en_GB.UTF-8, sort ignores case and hyphens and orders things "like a dictionary". With LC_ALL=C it sorts by byte value, and all the capitals come before the lower-case letters. When the ordering has to be reproducible — comparing two listings, generating a checksum — set LC_ALL=C. Note too that the assignment goes in front of the command, with no export: that defines the variable for that one run only, without touching your session. It is the cleanest way to try things out.
LC_ALL overrides all the other LC_* variables and LANG; that is why it is the one used to force behaviour. The same effect turns up in dates: date will give you "Mon 18 Aug" or "Mon Aug 18" depending on the locale, and if a script trims that output by position, it breaks when you move machines.
- Variable expansion and quoting, precisely
Module 2 covered quoting in passing. Here is the exact rule, one of the three or four most profitable things in the whole course.
| Form | Variables expand | Globbing happens | Split on spaces |
|---|---|---|---|
$VAR (unquoted) |
Yes | Yes, over the result | Yes |
"$VAR" |
Yes | No | No |
'$VAR' |
No | No | No |
The difference between the first two rows breaks scripts every single day:
operator@srv-tramontana:~$ FILEPATH="/home/operator/data/report august.txt"
operator@srv-tramontana:~$ touch "$FILEPATH"
operator@srv-tramontana:~$ ls -l $FILEPATH
ls: cannot access '/home/operator/data/report': No such file or directory
ls: cannot access 'august.txt': No such file or directory
operator@srv-tramontana:~$ ls -l "$FILEPATH"
-rw-r----- 1 operator operator 0 Aug 18 10:12 '/home/operator/data/report august.txt'Without quotes, Bash splits the value at the space and ls receives two arguments. The golden rule: always quote "$VAR", unless you deliberately want it split into words.
Braces, ${VAR}, delimit the name when what follows could be mistaken for part of it. With V=3.2.1, echo "release-$V_final" prints release- — Bash looks for a variable V_final, which does not exist — whereas echo "release-${V}_final" prints release-3.2.1_final.
Two expansions with a default value that you will use constantly:
operator@srv-tramontana:~$ echo "Destination: ${BACKUP_DIR:-/srv/tramontana/backups/temp}"
Destination: /srv/tramontana/backups/temp
operator@srv-tramontana:~$ echo "Version: ${VERSION:?the version must be specified}"
bash: VERSION: the version must be specified${VAR:-value}usesvalueifVARis empty or undefined, without assigning it. Ideal for defaults.${VAR:?message}aborts with that message if it is missing. It is the short way to demand a mandatory parameter.
There is also ${VAR:=value}, which assigns as well, and ${VAR:+value}, which uses value only if the variable is defined.
- Command substitution
$(command) runs the command and replaces the expression with its output, stripping the trailing newlines.
operator@srv-tramontana:~$ ACTIVE="$(readlink /opt/tramontana/app)"
operator@srv-tramontana:~$ echo "Active release: $ACTIVE, checked on $(date +%F)"
Active release: releases/3.2.1, checked on 2026-08-18You had already been using it in the backup convention cp app.conf app.conf.bak-$(date +%F). Now you know exactly what happens there.
There is an old form with backticks, `command`. Always use $(...): it can be nested without escaping anything and it is not visually confused with single quotes. And quote the result, "$(...)", for the same reason as in the previous section.
- Aliases
An alias is an abbreviation that Bash substitutes at the start of a command, before running it.
operator@srv-tramontana:~$ alias ll='ls -lh --group-directories-first'
operator@srv-tramontana:~$ alias logs='cd /var/log/tramontana'
operator@srv-tramontana:~$ alias | head -3
alias ll='ls -lh --group-directories-first'
alias logs='cd /var/log/tramontana'
alias ls='ls --color=auto'They are removed with unalias ll. To run the original command bypassing the alias, put a backslash in front:
command rm and the absolute path /bin/rm work too. This matters because aliases are local to your interactive session: they do not exist in a script, nor in cron, nor when another user runs the same thing. An rm='rm -i' alias gives you a false sense of a safety net that disappears in precisely the context where you would do the most damage.
Aliases are for your convenience, not for automation. If you need something to behave the same way always and everywhere, that is a script, and scripts are Module 4. In section 12 you will see the full set we use at Tramontana.
- Login, interactive and non-interactive shells
Here is the tangle that needs undoing once and for all. Bash reads different start-up files depending on how it was launched.
| Type | When it appears | What it reads |
|---|---|---|
| Interactive login | ssh operator@srv-tramontana, TTY console, su - |
/etc/profile, then the first of ~/.bash_profile, ~/.bash_login, ~/.profile that exists |
| Interactive non-login | opening a terminal tab, typing bash |
/etc/bash.bashrc and ~/.bashrc |
| Non-interactive | bash script.sh, cron, ssh server 'command' |
None of the above (only $BASH_ENV if it is defined) |
flowchart TD
S["bash starts"] --> L{"Is it a login<br/>shell?"}
L -->|Yes| P["/etc/profile"] --> U["~/.bash_profile<br/>or ~/.bash_login<br/>or ~/.profile<br/>(the first that exists)"]
U --> R["~/.profile usually includes:<br/>source ~/.bashrc"]
R --> B["~/.bashrc"]
L -->|No| I{"Is it<br/>interactive?"}
I -->|Yes| G["/etc/bash.bashrc"] --> B
I -->|No| N["It reads no start-up<br/>file at all"]
B --> W["Session ready"]
N --> W
W --> X{"Is a login<br/>shell exiting?"}
X -->|Yes| O["~/.bash_logout"]
Three consequences come out of that diagram and they resolve almost every doubt:
- Environment variables go in
~/.profile; aliases, functions andPS1go in~/.bashrc. The first are inherited by every process in the session; the second only make sense when there is somebody typing. - On Ubuntu,
~/.profileends with a block that doessource ~/.bashrc. That is why you see your aliases when you connect over SSH: they arrive through that chain, not because the login shell reads.bashrcby itself. If you create a~/.bash_profile,~/.profilestops being read and that chain breaks. - Cron reads none of this, and that is the root cause of the classic failure you will study in 03-07.
To find out which type of shell you are in: shopt -q login_shell && echo login tells you whether it is a login shell, and echo "$-" shows the active options, where i means interactive. And ~/.bash_logout runs when a login shell closes: it is used, for example, to clear the screen of a physical TTY with clear.
- Customising the
PS1 prompt
PS1 promptPS1 is the string Bash prints before each command. It accepts its own escape sequences:
| Sequence | Meaning |
|---|---|
\u |
user name |
\h / \H |
short / full host name |
\w / \W |
full path (with ~) / just the last directory |
\$ |
# if you are root, $ if not |
\t / \d |
time HH:MM:SS / date |
\n |
newline |
Colour is added with ANSI sequences, and there is one indispensable detail: anything that takes up no space on screen must go between \\[ and \\]. If you skip it, Bash miscalculates the width of the line and the history paints over the prompt when you recall long commands.
In production we deliberately use a different colour:
operator@srv-tramontana:~$ PS1='\[\e[1;37;41m\] PROD \[\e[0m\] \u@\h:\w\$ '
PROD operator@srv-tramontana:~$White text on a red background (41). At Tramontana, the prompt on srv-tramontana is red and the one on the laptop is green, for a very unaesthetic reason: nearly every catastrophic deletion begins with running a command meant for one window in the other one. A permanent visual warning that is impossible to ignore is more effective than any written rule. The same logic applies when you work as root, where \$ turns into # all by itself.
- Persistent history and its security risk
Bash keeps the commands in memory during the session and dumps them to ~/.bash_history on exit. That default behaviour has two problems: if you open several terminals, the last one to close overwrites the history of the others, and if the session dies suddenly, everything is lost.
| Setting | What it does |
|---|---|
HISTSIZE=10000 |
commands kept in memory |
HISTFILESIZE=20000 |
lines kept in the file |
HISTCONTROL=ignoreboth |
ignores consecutive duplicates and lines starting with a space |
HISTIGNORE='ls:ll:pwd:exit:history:clear' |
does not save these trivial commands |
HISTTIMEFORMAT='%F %T ' |
adds date and time to each entry |
shopt -s histappend |
appends to the file instead of overwriting it |
ignoreboth includes ignorespace, and that enables a practical trick: if you type a command preceded by a space, it is not saved. With HISTTIMEFORMAT set, history 3 shows you the last three entries with their timestamps, which turns the history into a record of what you did and when.
The security warning
~/.bash_history is a plain text file. Everything you type on the command line ends up there, credentials included:
That password is written into your history, and it is also visible in the process table to any user on the system while the command is running (you will see this with ps aux in 03-06). Two problems, not one.
What to do instead:
- Let the tool ask for it interactively (
mysql -u tramontana -p, without pasting it). - Store it in a credentials file with
600permissions and pass it the path. - If you have already typed it: delete it from the history in memory and from the file.
operator@srv-tramontana:~$ history -d 512 # deletes entry 512 from memory
operator@srv-tramontana:~$ history -w # rewrites ~/.bash_historyAnd check the permissions, which must be 600. Serious secret management is lesson 06-05; here just take away the minimum hygiene.
- The recommended
~/.bashrc for operator
~/.bashrc for operatorPutting it all together, this is the block we add at the end of ~/.bashrc on Tramontana's administrative accounts. It applies the course convention: a backup before editing and diff -u afterwards.
operator@srv-tramontana:~$ cp ~/.bashrc ~/.bashrc.bak-$(date +%F)
operator@srv-tramontana:~$ nano ~/.bashrc# --- Tramontana settings --------------------------------------------
umask 027 # nothing for "others"
HISTSIZE=10000
HISTFILESIZE=20000
HISTCONTROL=ignoreboth
HISTIGNORE='ls:ll:pwd:exit:history:clear'
HISTTIMEFORMAT='%F %T '
shopt -s histappend # do not overwrite between terminals
shopt -s checkwinsize cdspell
export EDITOR=nano
export PATH="$PATH:$HOME/scripts" # our own scripts, at the end
alias ll='ls -lh --group-directories-first'
alias la='ls -lha'
alias grep='grep --color=auto'
alias df='df -h'
alias logs='cd /var/log/tramontana'
alias rel='ls -l /opt/tramontana/releases/'
PS1='\[\e[1;37;41m\] PROD \[\e[0m\] \[\e[1;32m\]\u@\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]\$ '
# --------------------------------------------------------------------It is activated without closing the session with source ~/.bashrc, and the change is verified with diff -u ~/.bashrc.bak-2026-08-18 ~/.bashrc. Note that umask 027 goes here and not in ~/.profile because we want it for interactive work; for services it is set somewhere else, and that is material for 05-05.
Common Mistakes and Tips
- Spaces around the
=.VAR = valueis not an assignment. It is mistake number one. - Editing
~/.bashrcand expecting it to apply on its own. You have to runsource ~/.bashrcor open a new session. - Breaking the
PATH. Before touching it, save a copy:PATH_ORIG="$PATH". If you find yourself with no commands,export PATH="$PATH_ORIG"saves you without reconnecting. - Putting environment variables in
~/.bashrc. It works interactively and vanishes in non-interactive contexts. Their place is~/.profile. - Creating
~/.bash_profilewithout knowing that it cancels~/.profile. If you create it, include[ -f ~/.profile ] && . ~/.profileinside. - Trusting
alias rm='rm -i'. It does not exist in scripts or in cron. The real safety net is looking withlsbefore therm. - Forgetting
\\[ \\]in thePS1colours. The symptom is a prompt that becomes corrupted as you navigate the history. - Tip:
env -i commandstarts a program with no variables at all. It is the best way to reproduce what cron sees, and you will come back to it in 03-07. And sincesetwith no arguments lists variables and functions, always filter:set | grep '^HIST'.
Exercises
Exercise 1. Demonstrate on your VM the difference between a shell variable and an environment variable. Define DEPLOY=3.3.0 without exporting it, check that a child bash -c does not see it, export it, check that now it does, and explain why printenv failed before.
Exercise 2. Add /home/operator/scripts to the PATH in such a way that it does not take priority over the system commands and that the change survives reconnecting over SSH. Verify that it works without closing the session and explain which file you put it in and why.
Exercise 3. Generate a sorted, reproducible list of the houses in /home/operator/data/houses.txt that is identical on your laptop and on the server, even though they have different locales. Save the result in /home/operator/data/houses-sorted.txt and justify the decision.
Solutions
Solution 1.
operator@srv-tramontana:~$ DEPLOY=3.3.0
operator@srv-tramontana:~$ echo "$DEPLOY"
3.3.0
operator@srv-tramontana:~$ printenv DEPLOY; echo "exit code: $?"
exit code: 1
operator@srv-tramontana:~$ bash -c 'echo "child sees: [$DEPLOY]"'
child sees: []
operator@srv-tramontana:~$ export DEPLOY
operator@srv-tramontana:~$ bash -c 'echo "child sees: [$DEPLOY]"'
child sees: [3.3.0]The variable existed in the current shell from the start — that is why echo displayed it — but it was not in the environment vector that gets copied to the children. printenv consults exactly that vector, so it did not find it and returned 1. export does not create the variable: it marks it for export, and from then on all new children receive it. Those already running do not: inheritance happens at the moment of the fork.
Solution 2. It must go in ~/.profile, because it is an environment variable and because ~/.profile is read by login shells, which is what SSH opens. At the end, not at the beginning, so that the system has priority.
operator@srv-tramontana:~$ cp ~/.profile ~/.profile.bak-$(date +%F)
operator@srv-tramontana:~$ printf '\n# Tramontana own scripts\nexport PATH="$PATH:$HOME/scripts"\n' >> ~/.profile
operator@srv-tramontana:~$ diff -u ~/.profile.bak-$(date +%F) ~/.profile
@@ -20,3 +20,6 @@
fi
fi
+
+# Tramontana own scripts
+export PATH="$PATH:$HOME/scripts"
operator@srv-tramontana:~$ source ~/.profile
operator@srv-tramontana:~$ echo "$PATH" | tr ':' '\n' | tail -2
/usr/games
/home/operator/scriptssource applies the change to the current session without reconnecting. If instead of that we had written PATH="$HOME/scripts:$PATH", a script of ours called df or tar would mask the system one, and that kind of surprise is hard to diagnose. Putting it at the end is the conservative option. Note too that the single quotes in the printf stop $PATH and $HOME from expanding as the file is written: we want the expansion to happen at each login, not now.
Solution 3.
operator@srv-tramontana:~$ LC_ALL=C sort /home/operator/data/houses.txt \
> /home/operator/data/houses-sorted.txt
operator@srv-tramontana:~$ cat /home/operator/data/houses-sorted.txt
cal-ferrer
can-ventos
el-moli
la-solana
mas-figueresThe justification is the point of the exercise: without LC_ALL=C, the result depends on each machine's locale. With en_GB.UTF-8 the hyphen is ignored in the comparison and mas-figueres could end up in a different position relative to a hypothetical masfigueres; with C the comparison is byte by byte and the result is identical on any system. When the output is going to be compared, version-controlled or fed into another process, reproducibility matters more than the orthographic correctness of the ordering. And it goes in front of the command, with no export, so as not to disturb the rest of the session.
Conclusion
You have gone from inhabiting the shell environment to designing it.
- The environment is a
KEY=valuevector that the kernel hands to each process; you read it in/proc/<pid>/environ. - You distinguish a shell variable from an environment variable:
exportmakes the difference, and inheritance is one-way and at start-up, which is why a script cannotcdfor you except withsource. - You understand how Bash looks for an executable by walking the
PATHfrom left to right, how to extend it without destroying it, and why.never goes in thePATH. - You know the real effect of the locale on
sortand on dates, and you useLC_ALL=Cwhen you need reproducible results. - You handle
"$VAR"against$VARand'$VAR'precisely, along with braces${VAR}, the defaults${VAR:-...}and${VAR:?...}, and command substitution$(...). - You have untangled the mess of the start-up files: environment in
~/.profile, aliases andPS1in~/.bashrc, and none of it in a non-interactive shell. - You have made the production prompt red for an operational reason and configured a persistent history with
histappend, knowing that passwords typed on the command line end up in a plain text file.
The next lesson takes the natural step. You already know how to describe one file by its path; now you will learn to describe sets of files and patterns of text. In Using Wildcards and Regular Expressions you will see the distinction that almost nobody explains properly — that globbing is done by the shell before the command runs, whereas regular expressions are interpreted by the program that receives them — and that distinction, which you will now understand because you already know how Bash processes a line before launching it, is what stops grep *.log from doing something completely different from what you expected.
Linux Course: From Beginner to System Administrator
Module 1: Introduction to Linux
- What Is Linux?
- History of Linux
- Linux Distributions
- Installing Linux
- First Contact with the System
- The Linux File System Structure
Module 2: Basic Linux Commands
- Introduction to the Command Line
- Getting Help and System Documentation
- Navigating the File System
- File and Directory Operations
- Viewing and Editing Files
- Hard and Symbolic Links
- File Permissions and Ownership
Module 3: Advanced Command-Line Skills
- The Shell Environment: Variables, Aliases and History
- Using Wildcards and Regular Expressions
- Searching Files and Content: find, locate and grep
- Pipes and Redirection
- Text Processing: cut, sort, uniq, sed and awk
- Process Management
- Scheduling Tasks with Cron
- Networking Commands
Module 4: Shell Scripting
- Introduction to Shell Scripting
- Variables and Data Types
- Script Input, Output and Arguments
- Control Structures
- Functions and Libraries
- Debugging and Error Handling
- Production Scripts: Best Practices
Module 5: System Administration
- User and Group Management
- sudo and Special Permissions
- Package Management
- Disk Management
- systemd and Service Management
- System Logs: journald and syslog
- System Monitoring and Performance Tuning
- Backup and Restore
Module 6: Networking and Security
- Network Configuration
- SSH and Remote Access
- Firewalls and Perimeter Security
- Intrusion Detection Systems
- Secrets Management and TLS Certificates
- Securing Linux Systems
Module 7: Advanced Topics
- The Boot Process and System Recovery
- Advanced Diagnostics: strace, perf and eBPF
- Linux Kernel Tuning
- Virtualization with Linux
- Linux Containers and Docker
- Automation with Ansible
- High Availability and Load Balancing
