# Ubuntu Terminal Cheatsheet

- URL: https://karandeepsingh.ca/cheatsheets/terminal/ubuntu-terminal/
- Author: Karandeep Singh
- Published: 2025-06-28
- Tags: linux, cli
- Summary: The Ubuntu server commands I reach for daily. Covers apt/dpkg/snap package management, systemd, ufw, user management, networking, and upgrades.

Most of the fleet I manage runs Ubuntu LTS, and the same twenty commands cover 90% of the day-to-day: patching with apt, poking services with systemctl, reading journalctl, opening a port in ufw. This is that reference. I run nearly all of it inside a tmux session so a dropped SSH connection doesn't kill an upgrade (see my [tmux Cheatsheet](https://karandeepsingh.ca/cheatsheets/terminal/tmux/)).

## apt - Package Management

> **Docs:** [apt(8) on Ubuntu manpages](https://manpages.ubuntu.com/manpages/noble/en/man8/apt.8.html)

```bash
sudo apt update                      # refresh package index (always first)
sudo apt upgrade                     # upgrade installed packages
sudo apt full-upgrade                # upgrade, allowing removals (kernel etc.)
apt list --upgradable                # what would upgrade
sudo apt install nginx               # install
sudo apt install nginx=1.24.0-2ubuntu7   # install a specific version
sudo apt remove nginx                # remove package, keep config
sudo apt purge nginx                 # remove package AND config files
sudo apt autoremove --purge          # remove orphaned dependencies
sudo apt install -y --no-install-recommends jq   # lean install for servers/CI
```

Inspecting packages:

```bash
apt show nginx                       # metadata: version, deps, description
apt-cache policy nginx               # installed vs candidate version, origin
apt search "reverse proxy"           # search descriptions
apt list --installed | grep nginx    # what's installed
sudo apt-mark hold linux-image-generic   # pin a package (unhold to release)
```

Repositories:

```bash
sudo add-apt-repository ppa:ondrej/php    # add a PPA (runs apt update on modern Ubuntu)
sudo add-apt-repository --remove ppa:ondrej/php
# modern keyring pattern for third-party repos:
curl -fsSL https://example.com/key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/example.gpg
echo "deb [signed-by=/etc/apt/keyrings/example.gpg] https://repo.example.com stable main" \
  | sudo tee /etc/apt/sources.list.d/example.list
```

Unattended security updates:

```bash
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
# config: /etc/apt/apt.conf.d/50unattended-upgrades
sudo unattended-upgrade --dry-run -d      # test what it would do
```

## dpkg Basics

The two questions worth answering quickly — *what installed this binary*, and
*which version am I actually on* — both have one-line answers:

```text
$ dpkg -S $(command -v jq)
jq: /usr/bin/jq
$ dpkg -l jq | tail -2
+++-==============-======================-============-====================================================
ii  jq             1.7.1-3ubuntu0.24.04.2 amd64        lightweight and flexible command-line JSON processor
$ apt-cache policy jq
jq:
  Installed: 1.7.1-3ubuntu0.24.04.2
  Candidate: 1.7.1-3ubuntu0.24.04.2
# "ii" is installed-and-configured. If Installed and Candidate differ,
# an upgrade is waiting; if Installed says (none), it is not on the box.
```

> **Docs:** [dpkg(1) on Ubuntu manpages](https://manpages.ubuntu.com/manpages/noble/en/man1/dpkg.1.html)

```bash
sudo dpkg -i package.deb             # install a local .deb
sudo apt install ./package.deb       # better: resolves dependencies too
dpkg -l | grep nginx                 # list installed packages
dpkg -L nginx                        # files a package installed
dpkg -S /usr/sbin/nginx              # which package owns this file
sudo dpkg --configure -a             # fix interrupted installs
sudo apt --fix-broken install        # fix broken dependencies
```

## snap Basics

> **Docs:** [Snap documentation on snapcraft.io](https://snapcraft.io/docs)

```bash
snap list                            # installed snaps
snap find kubectl                    # search the store
sudo snap install kubectl --classic  # classic = no sandbox confinement
sudo snap refresh                    # update all snaps
sudo snap remove --purge kubectl     # remove including snapshots
snap info kubectl                    # channels and versions
```

## systemd - Services

> **Docs:** [systemctl(1) on man7.org](https://man7.org/linux/man-pages/man1/systemctl.1.html)

```bash
systemctl status nginx               # state, PID, recent log lines
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx          # reload config without dropping connections
sudo systemctl enable --now nginx    # start now AND on boot
sudo systemctl disable nginx
systemctl is-active nginx            # scripting-friendly: active/inactive
systemctl list-units --type=service --state=failed
sudo systemctl daemon-reload         # after editing unit files
sudo systemctl edit nginx            # override unit via drop-in file
```

## journalctl - Logs

> **Docs:** [journalctl(1) on man7.org](https://man7.org/linux/man-pages/man1/journalctl.1.html)

```bash
journalctl -u nginx                  # logs for one unit
journalctl -u nginx -f               # follow (like tail -f)
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx --since "2026-07-22 09:00" --until "2026-07-22 10:00"
journalctl -u nginx -n 100 --no-pager
journalctl -p err -b                 # errors since last boot
journalctl --disk-usage              # how much space logs use
sudo journalctl --vacuum-size=500M   # trim the journal
```

## ufw - Firewall

> **Docs:** [UFW on the Ubuntu Community Help Wiki](https://help.ubuntu.com/community/UFW)

```bash
sudo ufw status verbose
sudo ufw allow OpenSSH               # ALWAYS before enabling, or you lock yourself out
sudo ufw enable
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from 10.0.0.0/8 to any port 5432 proto tcp   # scoped rule
sudo ufw limit ssh                   # rate-limit brute force
sudo ufw status numbered
sudo ufw delete 3                    # delete rule by number
sudo ufw default deny incoming
sudo ufw default allow outgoing
```

**warning:**
On a remote box, run `sudo ufw allow OpenSSH` **before** `sudo ufw enable`. The default incoming policy is deny, so enabling the firewall without an SSH rule cuts your own session's ability to reconnect.

## Users & Permissions

> **Docs:** [Ubuntu Server documentation](https://documentation.ubuntu.com/server/)

```bash
sudo adduser deploy                  # interactive: home dir, password, shell
sudo usermod -aG sudo deploy         # add to sudo group (-a is critical!)
sudo usermod -aG docker deploy       # docker without sudo (log out/in to apply)
groups deploy                        # check memberships
sudo deluser deploy --remove-home
sudo passwd -l deploy                # lock an account
sudo visudo                          # edit sudoers safely (syntax-checked)
# passwordless sudo for one command, via a drop-in:
echo 'deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp' \
  | sudo tee /etc/sudoers.d/deploy-restart
```

chmod / chown recap:

| Command | Effect |
|---------|--------|
| `chmod 644 file` | rw-r--r-- (configs, regular files) |
| `chmod 755 dir` | rwxr-xr-x (directories, scripts) |
| `chmod 600 key.pem` | rw------- (private keys; SSH refuses looser) |
| `chmod +x script.sh` | Add execute bit |
| `chmod -R g+w shared/` | Recursive group write |
| `chown deploy:deploy file` | Change owner and group |
| `chown -R www-data: /var/www` | Recursive; empty group = user's group |

## Networking

> **Docs:** [Netplan documentation](https://netplan.readthedocs.io/en/stable/)

```bash
ip a                                 # interfaces and addresses
ip r                                 # routing table (default gateway)
ss -tulpn                            # listening sockets + owning process
ss -tn state established             # current TCP connections
resolvectl status                    # DNS config (systemd-resolved)
ping -c 4 1.1.1.1
curl -sI https://karandeepsingh.ca | head -5
```

Netplan (Ubuntu's network config since 18.04): YAML in `/etc/netplan/*.yaml`, applied with `sudo netplan apply`. On cloud instances the file is usually generated by cloud-init, so override via `/etc/cloud/cloud.cfg.d/` rather than editing the generated file, or your changes vanish on reboot. `sudo netplan try` applies with an auto-rollback timer, which is the safe option over SSH.

## Release & Version Info

> **Docs:** [Ubuntu Server docs on ubuntu.com](https://ubuntu.com/server/docs)

```bash
lsb_release -a                       # Ubuntu version and codename
cat /etc/os-release                  # same info, script-friendly
uname -r                             # kernel version
sudo do-release-upgrade              # upgrade to next LTS (take a snapshot first)
sudo do-release-upgrade -d           # upgrade to development release
# /etc/update-manager/release-upgrades: Prompt=lts|normal|never
```

## update-alternatives

> **Docs:** [update-alternatives(1) on Ubuntu manpages](https://manpages.ubuntu.com/manpages/noble/en/man1/update-alternatives.1.html)

Manage multiple versions of the same tool:

```bash
update-alternatives --list editor
sudo update-alternatives --config editor      # interactive pick
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1
sudo update-alternatives --set editor /usr/bin/vim.basic
```

## Common Paths

| Path | What lives there |
|------|------------------|
| `/etc/apt/sources.list.d/` | Third-party apt repo definitions |
| `/etc/apt/apt.conf.d/` | apt behaviour incl. unattended-upgrades config |
| `/etc/apt/keyrings/` | Repo signing keys (modern location) |
| `/etc/netplan/` | Network configuration YAML |
| `/etc/systemd/system/` | Custom and overridden unit files |
| `/var/log/syslog` | General system log |
| `/var/log/auth.log` | Logins, sudo usage, SSH attempts |
| `/var/log/unattended-upgrades/` | Automatic update history |
| `/var/log/nginx/` | nginx access and error logs |
| `/etc/sudoers.d/` | Drop-in sudo rules (use with visudo) |
| `/etc/cron.d/`, `/etc/cron.daily/` | System cron jobs |

If your servers are EC2 instances running Amazon Linux instead, most of this maps over with yum in place of apt. The differences are in my [Amazon Linux 2 Terminal Cheatsheet](https://karandeepsingh.ca/cheatsheets/terminal/al2-terminal/).

**Question:** A binary on a server behaves differently from your laptop. Can you tell, in one command, which package and version put it there?

## Related Cheatsheets

- [Amazon Linux 2 Terminal](https://karandeepsingh.ca/cheatsheets/terminal/al2-terminal/): the yum/EC2 counterpart, covering extras repos, SSM Session Manager, IMDSv2, and cloud-init logs
- [tmux](https://karandeepsingh.ca/cheatsheets/terminal/tmux/): how I keep `do-release-upgrade` and other long jobs alive through dropped SSH sessions
- [Jenkins](https://karandeepsingh.ca/cheatsheets/devops/jenkins/): for when you stop running these commands by hand and codify them as pipeline stages

More terminal sheets in the [Terminal group](https://karandeepsingh.ca/cheatsheets/terminal/); the full [cheatsheet library](https://karandeepsingh.ca/cheatsheets/) has everything else.
