After a fresh Debian installation, there are usually several packages that I install every time. Instead of installing them one by one manually, I keep them in a simple Bash script and install everything with a single command.
For me, this is a lightweight and efficient solution for basic package installation. There is no need to use any automation platform just to install a list of packages.
So here's basically what I do after installing Debian:
- Run a simple post-install Bash script,
- Install all required packages.
My script is quite minimal and contains two types of packages:
- Basic tools that are useful on almost any desktop
- Personal desktop packages that I specifically want
If the setup later grows beyond package installation into system configuration, then I move to Ansible.
Please note that this guide is specifically a package-focused post-install guide, not a complete/general Debian or Ubuntu post-installation guide. If you want a complete essential post-installation guide, go to the following links:
- Debian Server Setup: Essential Post-Installation Steps
- DebPostInstall: Debian And Ubuntu Server Post Install Script
- An Automated Way To Install Essential Applications On Ubuntu
Table of Contents
1. Create a Simple Bash Post-Install Script for Debian / Ubuntu
It is convenient to keep all scripts in one directory. So let me create a scripts directory:
mkdir -p ~/scripts
Create the script:
nano ~/scripts/install-deb-essential-packages.sh
Add:
#!/usr/bin/env bash
set -euo pipefail
PACKAGES=(
# Basic tools
git
curl
wget
ca-certificates
unzip
build-essential
# Personal desktop packages
firmware-cirrus
papirus-icon-theme
timeshift
gnome-shell-extension-manager
vim
gnome-shell-extension-dashtodock
gnome-themes-extra
flatpak
gnome-software-plugin-flatpak
steam-installer
)
echo "Updating package lists..."
sudo apt update
echo "Installing packages..."
sudo apt install -y "${PACKAGES[@]}"
echo "Done."
Please note that some packages are only available for a specific platform. So you may need to edit this script and add or remove packages based on your Linux distribution. After adding all essential packages to install, save the script and close it.
Make it executable:
chmod +x ~/scripts/install-deb-essential-packages.sh
Run the script to install all essential packages at once in your Debian/Ubuntu system:
~/scripts/install-deb-essential-packages.sh
You can use the same script for other Debian-based systems like Ubuntu and Ubuntu-derivatives such as Linux Mint, Pop!_OS, ElementaryOS and Zorin OS.
As stated already, some packages may available only for a specific edition. Just make sure the packages in the script are available for your Linux distribution. You can check package availability as described in the link below:
2. Why These Basic Packages?
The first group is intentionally small.
git
Useful for cloning repositories and managing configuration files.
git clone ...
curl
A general-purpose command-line tool for transferring data over network protocols. Many installation instructions and scripts use it.
wget
Another useful command-line downloader.
ca-certificates
Provides trusted CA certificates used by software for HTTPS/TLS connections.
unzip
Useful because downloaded software and projects frequently come as .zip archives.
build-essential
Provides the standard basic compilation toolchain, including tools such as gcc and make. Debian specifically identifies it as the key package for a basic build environment.
build-essentialisn't strictly necessary for every desktop user. If I know I won't compile software, I can remove it. It's included here because it's a useful baseline for a development-oriented personal machine.
3. Why Use a Bash Array?
Instead of manually running:
sudo apt install git
sudo apt install curl
sudo apt install wget
sudo apt install unzip
I maintain one list:
PACKAGES=(
git
curl
wget
unzip
)
Then install everything with:
sudo apt install -y "${PACKAGES[@]}"This is:
- Faster
- Easier to maintain
- Easy to modify
- Easy to reuse after reinstalling Debian
Adding a package is as simple as adding another line:
PACKAGES=(
git
curl
wget
neovim
)
4. Why set -euo pipefail?
As you may noticed, the script starts with:
set -euo pipefail
This makes the script safer.
In particular, -e causes the script to stop when a command fails instead of blindly continuing.
For a small personal setup script, this is a good default.
5. Keep Personal Packages Separate
As the list grows, it can be useful to distinguish between general-purpose tools and things specific to my setup.
For example:
PACKAGES=(
# Basic tools
git
curl
wget
ca-certificates
unzip
build-essential
# Desktop
firmware-cirrus
papirus-icon-theme
gnome-shell-extension-manager
gnome-shell-extension-dashtodock
gnome-themes-extra
# Applications
timeshift
flatpak
gnome-software-plugin-flatpak
steam-installer
# Editor
vim
)
This makes the script easier to understand later.
6. When I Use Ansible?
Some of you wonder why not just use automation platforms like Ansible and call it a day. I don't use Ansible just for a package list. Ansible becomes useful when I start doing things like:
- Install packages
- Configure Git
- Create config files
- Configure Vim
- Enable services
- Configure GNOME
- Apply system settings
Or when I want the same configuration on multiple machines.
For simple package install purpose, I use Bash. For Repeatable system configuration, I go to Ansible.
7. Install Ansible
Ansible is an open-source, command-line IT automation tool written in Python. It can configure systems, deploy software, and orchestrate advanced workflows to support application deployment, system updates, and more.
It is known for being "agentless," meaning it manages remote machines over standard connection protocols without installing persistent software on them.
How it Works
You write human-readable instructions in YAML (called "playbooks") that declare the desired state of your servers. Ansible connects via SSH (for Linux/Unix) or WinRM (for Windows), checks the current state, and only makes changes if necessary to match your instructions. No extra agents or daemons required on the managed nodes.
Key Insights
- Agentless Architecture: Unlike tools requiring a persistent agent on every server, Ansible uses your existing SSH daemon (or WinRM for Windows) for transport. This reduces maintenance overhead and eliminates the need to manage agent software updates or compatibility.
- Idempotence: This is a core principle. Ansible modules are designed to be idempotent when possible, meaning they only make changes to a system when the desired state has not been achieved. Running the same playbook multiple times produces the same result. Note: Some modules (like
commandandshell) do not have this behavior natively and require manual enforcement. - Simplicity via YAML: Playbooks are written in YAML, a human-readable language. This means your automation code often reads like documentation, making it accessible even to those who aren't dedicated programmers.
- Push-Based Model: You execute Ansible from a central "Control Node," and it pushes configuration changes out to "Managed Nodes" over SSH (or WinRM for Windows).
One Interesting Fact about Ansible
While Ansible is famous for being "agentless," it actually does use a tiny, ephemeral agent—but only temporarily. For each task, Ansible connects to managed nodes and pushes out small programs, called Ansible modules, to them.
These programs are written to be resource models of the desired state of the system. Ansible then executes these modules (over SSH by default) and removes them when finished. This leaves the server exactly as it was before the automation, with no persistent Ansible software installed.
To install Ansible on Debian / Ubuntu, run:
sudo apt update
sudo apt install ansible
Check its version:
ansible --version
Sample Output:
ansible [core 2.16.3]
config file = None
configured module search path = ['/home/ostechnix/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3/dist-packages/ansible
ansible collection location = /home/ostechnix/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.12.3 (main, Apr 10 2024, 05:33:47) [GCC 13.2.0] (/usr/bin/python3)
jinja version = 3.1.2
libyaml = True
8. Create an Ansible Project
For a personal desktop:
mkdir -p ~/ansible/debian-desktop
cd ~/ansible/debian-desktop
Initialize Git:
git init
Initial structure:
debian-desktop/
├── inventory.ini
└── site.yml
9. Create the Inventory
Create inventory.ini file:
inventory.ini
Add:
[desktop]
localhost ansible_connection=local
This tells Ansible to manage my current computer locally.
Test it using command:
ansible -i inventory.ini desktop -m ping
Sample Output:
localhost | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}10. Create the Playbook
Create site.yml file:
nano site.yml
Add the following lines:
---
- name: Configure my Debian desktop
hosts: desktop
become: true
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
- name: Install desktop packages
ansible.builtin.apt:
name:
- git
- curl
- wget
- ca-certificates
- unzip
- build-essential
- firmware-cirrus
- papirus-icon-theme
- timeshift
- gnome-shell-extension-manager
- vim
- gnome-shell-extension-dashtodock
- gnome-themes-extra
- flatpak
- gnome-software-plugin-flatpak
- steam-installer
state: present
Again, add the packages that your platform supports in the above playbook. Save and close it.
Run the playbook:
ansible-playbook -i inventory.ini site.yml --ask-become-pass
Sample Output:
BECOME password:
PLAY [Configure my Debian desktop] ********************************************************************************************************************************************************************************************************
TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Update apt cache] *******************************************************************************************************************************************************************************************************************
changed: [localhost]
TASK [Install desktop packages] ***********************************************************************************************************************************************************************************************************
changed: [localhost]
PLAY RECAP ********************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
11. Difference Between Bash Script and Ansible
Bash generally says: Run these commands.
Ansible generally says: Make the machine look like this.
For example:
- name: Install Vim
ansible.builtin.apt:
name: vim
state: present
If Vim isn't installed, Ansible instructs the system to Install it. If Vim is already installed, it simply says Do nothing.
This is called idempotency.
12. Adding Configuration
Once package installation works, Ansible can manage configuration too.
For example:
- name: Create Vim configuration
ansible.builtin.copy:
content: |
set number
set relativenumber
syntax on
dest: "{{ ansible_facts.user_dir }}/.vimrc"
mode: "0644"
Now Ansible isn't just installing software. It's configuring the machine.
13. Split Things Up When the Playbook Gets Large
Don't worry about Ansible roles immediately.
If site.yml gets too large, you can split the tasks into separate files:
debian-desktop/
├── inventory.ini
├── site.yml
├── packages.yml
├── git.yml
└── desktop.yml
site.yml is the main playbook. It imports and runs the tasks from the other files:
---
- name: Configure my Debian desktop
hosts: desktop
become: true
tasks:
- ansible.builtin.import_tasks: packages.yml
- ansible.builtin.import_tasks: git.yml
- ansible.builtin.import_tasks: desktop.yml
The individual files have specific purposes:
packages.yml- contains tasks for installing and managing APT packages.git.yml- contains tasks for configuring Git.desktop.yml- contains tasks for configuring GNOME and other desktop settings.
For example, packages.yml might contain:
---
- name: Install required packages
ansible.builtin.apt:
name:
- git
- curl
- wget
- vim
- flatpak
state: present
This keeps site.yml small and makes the configuration easier to understand and maintain.
14. Eventually Use Ansible Roles for Complex Setup
If the project becomes much larger, you can introduce Ansible roles.
The structure might eventually look like:
debian-desktop/
├── inventory.ini
├── site.yml
└── roles/
├── packages/
├── git/
├── vim/
└── gnome/
Each role groups all the tasks, files, templates, variables, and configuration related to one particular area.
For example:
packages- manages installed packages.git- manages Git configuration.vim- manages Vim configuration.gnome- manages GNOME and desktop configuration.
Don't start with roles. For a personal machine, start with a single site.yml. If it becomes large, split it into files such as packages.yml, git.yml, and desktop.yml. Only introduce roles when the project has grown enough that they provide a real benefit.
15. Useful Ansible Commands
Test Ansible:
ansible -i inventory.ini desktop -m ping
Run the playbook:
ansible-playbook -i inventory.ini site.yml --ask-become-pass
Dry run:
ansible-playbook -i inventory.ini site.yml --check --ask-become-pass
More output:
ansible-playbook -i inventory.ini site.yml --ask-become-pass -v
16. Keep the Setup in Git
For the Bash script:
cd ~/scripts
git init
For the Ansible project:
cd ~/ansible/debian-desktop
git init
Eventually, the Ansible project can become the canonical description of my desktop setup.
17. What About Fedora or RHEL-Based Distributions?
The Bash structure of the script can be reused on Fedora and other RHEL-based distributions, but you should not assume that simply replacing apt with dnf is enough.
On Debian:
sudo apt update
sudo apt install -y "${PACKAGES[@]}"
On Fedora/RHEL-based systems, package installation generally uses:
sudo dnf install -y "${PACKAGES[@]}"However, package names can differ between distributions.
For example:
Debian Fedora/RHEL
------- ----------
git git
curl curl
wget wget
ca-certificates ca-certificates
unzip unzip
build-essential gcc gcc-c++ make
Some packages may have completely different names, may be provided by different repositories, or may not exist in the same form.
Therefore, for a personal setup, it is usually cleaner to keep separate scripts:
install-debian-essential-packages.sh
install-fedora-essential-packages.sh
The Debian script can use:
sudo apt update
sudo apt install -y "${PACKAGES[@]}"
while the Fedora script can use:
sudo dnf install -y "${PACKAGES[@]}"with a Fedora-specific package list.
Why not make one script for everything?
It is possible to detect the distribution:
if command -v apt >/dev/null 2>&1; then
sudo apt update
sudo apt install -y "${PACKAGES[@]}"
elif command -v dnf >/dev/null 2>&1; then
sudo dnf install -y "${PACKAGES[@]}"
else
echo "Unsupported package manager."
exit 1
fi
But this becomes more complicated once the package names differ.
For a simple personal setup, separate scripts are easier to understand and maintain.
If the configuration eventually needs to support multiple distributions and becomes much more complicated, that is a good point to consider Ansible.
18. Conclusion
As you can see, we can use a simple post-install Bash script to avoid manually installing the same packages one by one. If you want to automate and reproduce the configuration of the whole system, you can then use Ansible.
Don't introduce Ansible until the problem is big enough to need it.
Don't complicate things. Start simple. Automate what becomes repetitive. Introduce Ansible when the configuration is what needs managing.
