Linux for the cloud · Lesson 8 of 15 · about 30 minutes

Installing software

The bookshop's server gets the rest of its setup: nginx in front of the app, and PostgreSQL behind it, installed with apt. What packages and repositories are, how to find, install, remove and update software, and where it all ends up. Along the way: the 502 Bad Gateway the practice files predicted, why Apache wouldn't start next to nginx, and what remove leaves behind that purge doesn't.

By the end you'll know

  • What packages and repositories are, and why servers install software this way
  • How to find, install, remove and update software with apt
  • Where a package puts its program, its settings and its service
  • How to set up a site in nginx, and check its settings before using them
  • How to install PostgreSQL and make your first database

The problem: a server with nothing on it

The practice files describe a server with three parts: a web server in front, the books-api app in the middle, and a database behind it. Lesson 7 has the app running. This lesson installs the other two.

You could download each program from its website. But then you'd have to check it's genuine, find and install everything it depends on, put its files in the right places, set up its service, and repeat all of that for every security fix. On Linux, almost nobody does. Instead, the distribution packages the software for you.

On your own computer, in your practice folder, start the server and log in, as in Lesson 7:

BASH
cd linux-practice
vagrant up
vagrant ssh

Everything else in this lesson runs on the server.

Packages and repositories

A package is a program ready to install: its files, a list of the other packages it needs (its dependencies), and small scripts that set it up. On Ubuntu and Debian, packages are .deb files. A repository is a big, signed collection of packages that Ubuntu builds and tests together. Your server already knows where Ubuntu's are:

BASH
head -12 /etc/apt/sources.list.d/ubuntu.sources
OUTPUT
Types: deb
URIs: http://ports.ubuntu.com/ubuntu-ports/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

Types: deb
URIs: http://ports.ubuntu.com/ubuntu-ports/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
  • URIs is where to download from. ports.ubuntu.com is for processors other than Intel and AMD, like the arm64 in an Apple Silicon Mac; an Intel or AMD server uses archive.ubuntu.com.
  • Suites are the release, noble (Ubuntu 24.04's name), plus its updates, newer extras (backports), and security fixes.
  • Components sort packages by who supports them: main is supported by Ubuntu, universe by the community.
  • Signed-By is the key every package list must be signed with. A tampered list is refused, which is why this is safer than downloading programs by hand.

apt is the tool you'll use. Underneath it, dpkg does the actual installing of .deb files; apt adds downloading, dependencies and searching on top.

Update the list first

apt works from a local copy of each repository's list of packages. apt update downloads a fresh copy. It installs nothing:

BASH
sudo apt update
OUTPUT
Hit:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease
Get:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease [126 kB]
Get:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease [126 kB]
Get:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease [126 kB]
...
Fetched 14.3 MB in 2s (8,805 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
238 packages can be upgraded. Run 'apt list --upgradable' to see them.

(Most of the Get: lines are left out.) Hit means that list hadn't changed; Get means it downloaded a new one. The last line says 238 installed packages have newer versions waiting: this server's image was built weeks ago. Updating them is apt upgrade, at the end of this lesson. Run apt update before installing anything, every time, so you get current versions.

Find it

BASH
apt search --names-only '^nginx$' 2>/dev/null
apt show nginx 2>/dev/null | grep -E '^(Package|Version|Depends|Description)'
apt-cache policy nginx
OUTPUT
Sorting... Done
Full Text Search... Done
nginx/noble-updates,noble-security 1.24.0-2ubuntu7.19 arm64
  small, powerful, scalable web/proxy server

Package: nginx
Version: 1.24.0-2ubuntu7.19
Depends: libc6 (>= 2.34), libcrypt1 (>= 1:4.1.0), libpcre2-8-0 (>= 10.22), libssl3t64 (>= 3.0.0), zlib1g (>= 1:1.1.4), iproute2, nginx-common (= 1.24.0-2ubuntu7.19)
Description: small, powerful, scalable web/proxy server
nginx:
  Installed: (none)
  Candidate: 1.24.0-2ubuntu7.19
  Version table:
     1.24.0-2ubuntu7.19 500
        500 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 Packages
        500 http://ports.ubuntu.com/ubuntu-ports noble-security/main arm64 Packages
     1.24.0-2ubuntu7 500
        500 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 Packages
  • apt search looks through names and descriptions. --names-only '^nginx$' asks for exactly nginx; without it, you'd get dozens of nginx add-ons. 2>/dev/null hides a warning (Lesson 4) that apt prints when its output goes into a pipe.
  • apt show describes a package. Depends lists what it needs, including nginx-common, which apt will install for you.
  • apt-cache policy says what's installed (nothing yet), which version would be installed (the candidate), and where each version comes from. The newest is in noble-security: it includes security fixes made since Ubuntu 24.04 came out. The -2ubuntu7.19 part is Ubuntu's own revision of nginx 1.24.0.

Install nginx

nginx (said "engine x") is a web server: it answers web requests, serves files, and passes requests on to apps behind it, which makes it a reverse proxy.

BASH
sudo apt install -y nginx
OUTPUT
The following additional packages will be installed:
  nginx-common
...
The following NEW packages will be installed:
  nginx nginx-common
0 upgraded, 2 newly installed, 0 to remove and 238 not upgraded.
...
Setting up nginx-common (1.24.0-2ubuntu7.19) ...
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service.
Setting up nginx (1.24.0-2ubuntu7.19) ...
...
No services need to be restarted.

(Some lines are left out.) -y answers "yes" to "Do you want to continue?". The Created symlink line is Lesson 7's enable: the package enabled nginx's service as part of installing it. On Ubuntu, a service package is usually started and enabled straight away. Its unit file is in /usr/lib/systemd/system/, the folder for packaged units, not the /etc/systemd/system/ you used for your own.

BASH
systemctl status nginx --no-pager | head -5
curl -sI http://localhost | head -3
OUTPUT
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Fri 2026-10-30 00:13:39 UTC; 2s ago
       Docs: man:nginx(8)
    Process: 3253 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Fri, 30 Oct 2026 00:13:42 GMT

Running, enabled, and answering: curl -sI asks for just the headers of the reply, and 200 OK is nginx's welcome page. Notice ExecStartPre=: a command systemd runs before starting the service. Here it's nginx -t, which checks nginx's settings, so nginx with broken settings won't even try to start. You'll use nginx -t yourself in a moment.

Where it all went

BASH
which nginx
dpkg -S "$(which nginx)"
ls /etc/nginx /etc/nginx/sites-enabled
apt list --installed 2>/dev/null | wc -l
OUTPUT
/usr/sbin/nginx
nginx: /usr/sbin/nginx
/etc/nginx:
conf.d        fastcgi_params  koi-win     modules-available  nginx.conf    scgi_params      sites-enabled  uwsgi_params
fastcgi.conf  koi-utf         mime.types  modules-enabled    proxy_params  sites-available  snippets       win-utf

/etc/nginx/sites-enabled:
default
617

Lesson 2's layout, used exactly as intended: the program in /usr/sbin (sbin is for programs that administrators run), its settings in /etc/nginx, its logs in /var/log/nginx. dpkg -S answers "which package does this file belong to?", useful when you find a file and wonder where it came from. dpkg -L nginx lists every file a package installed. And 617 packages are installed on this server in total.

The bookshop's site

nginx's settings for each website go in /etc/nginx/sites-available/, and a site is switched on by a symbolic link to it in sites-enabled/, the same idea as systemd's .wants folder in Lesson 7. Use the practice files' own site.conf:

BASH
cat /practice/config/web/site.conf
OUTPUT
server {
    listen 80;
    server_name books.example.com;

    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_read_timeout 30s;
    }

    location /static/ {
        root /srv/books;
    }
}

In plain words: listen on port 80, for the site books.example.com. Pass anything under /api/ to the app on this machine's port 8080, and give up after 30 seconds. Serve anything under /static/ straight from files in /srv/books. Every setting ends in ;. Switch it on, and the default welcome site off:

BASH
sudo cp /practice/config/web/site.conf /etc/nginx/sites-available/books
sudo ln -s /etc/nginx/sites-available/books /etc/nginx/sites-enabled/books
sudo rm /etc/nginx/sites-enabled/default
ls -l /etc/nginx/sites-enabled/
sudo mkdir -p /srv/books/static
echo "body { font-family: sans-serif; }" | sudo tee /srv/books/static/style.css
OUTPUT
total 0
lrwxrwxrwx 1 root root 32 Oct 30 00:13 books -> /etc/nginx/sites-available/books
body { font-family: sans-serif; }

ln -s TARGET LINK makes a symbolic link. Removing default only removed the link; the welcome site's settings are still in sites-available, ready to switch back on. With it gone, the bookshop is the only site, so it answers every request, whatever name they use.

Check before you load. nginx -t tests the settings without touching the running server. Then reload applies them without dropping a single connection, unlike restart:

BASH
sudo nginx -t
sudo systemctl reload nginx
curl -s http://localhost/static/style.css
curl -si http://localhost/api/books | head -1
OUTPUT
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
body { font-family: sans-serif; }
HTTP/1.1 502 Bad Gateway

The static file works. The API gives 502 Bad Gateway, exactly what the practice files' README.txt warned: "If the app listens somewhere else, requests fail with 502 Bad Gateway." (A request sent the instant after a reload can still reach the old settings, and get the old site's answer. Give it a second.) nginx's logs say why:

BASH
sudo tail -1 /var/log/nginx/error.log
sudo tail -2 /var/log/nginx/access.log
OUTPUT
2026/10/30 00:16:50 [error] 3399#3399: *5 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: books.example.com, request: "GET /api/books HTTP/1.1", upstream: "http://127.0.0.1:8080/api/books", host: "localhost"
127.0.0.1 - - [30/Oct/2026:00:16:50 +0000] "GET /static/style.css HTTP/1.1" 200 34 "-" "curl/8.5.0"
127.0.0.1 - - [30/Oct/2026:00:16:50 +0000] "GET /api/books HTTP/1.1" 502 166 "-" "curl/8.5.0"

Connection refused while connecting to the upstream, the app behind nginx, at 127.0.0.1:8080. Nothing is listening there: your books-api writes log lines, but doesn't answer web requests at all. The access log is the same format as the practice files' access.log from Lessons 3 and 4, now written by your own server. The 502s in those practice logs, between 14:00 and 14:20, looked exactly like this. Lesson 9 is about ports, and makes the app answer.

Install PostgreSQL

PostgreSQL is the database the app's settings point at: database_port = 5432 in app.conf.

BASH
sudo apt install -y postgresql
OUTPUT
The following additional packages will be installed:
  libcommon-sense-perl libjson-perl libjson-xs-perl libllvm17t64 libpq5 libtypes-serialiser-perl postgresql-16 postgresql-client-16
  postgresql-client-common postgresql-common ssl-cert
...
0 upgraded, 12 newly installed, 0 to remove and 238 not upgraded.
Need to get 42.3 MB of archives.
After this operation, 176 MB of additional disk space will be used.
...
Creating new PostgreSQL cluster 16/main ...
/usr/lib/postgresql/16/bin/initdb -D /var/lib/postgresql/16/main --auth-local peer --auth-host scram-sha-256 --no-instructions
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
...
Setting up postgresql (16+257build1.1) ...

One command, 12 packages: postgresql itself is a small package that depends on the real one, postgresql-16, which depends on the rest. That's dependencies doing their job. The package also made its first database storage (a cluster) in /var/lib/postgresql, Lesson 2's place for data that changes, and a system user to own it, like your bookshop in Lesson 5:

BASH
systemctl list-units 'postgresql*' --no-pager
id postgres
OUTPUT
  UNIT                       LOAD   ACTIVE SUB     DESCRIPTION               
  postgresql.service         loaded active exited  PostgreSQL RDBMS
  postgresql@16-main.service loaded active running PostgreSQL Cluster 16-main

Legend: LOAD   → Reflects whether the unit definition was properly loaded.
        ACTIVE → The high-level unit activation state, i.e. generalization of SUB.
        SUB    → The low-level unit activation state, values depend on unit type.

2 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.
uid=108(postgres) gid=110(postgres) groups=110(postgres),109(ssl-cert)

Two units: postgresql.service just starts the others and exits, and postgresql@16-main.service is the real database, running. The @ means one unit file used for many: one per cluster, here version 16's main. RDBMS stands for relational database management system.

Talk to it. psql is PostgreSQL's command-line client, and sudo -u postgres runs it as the postgres user:

BASH
sudo -u postgres psql -c 'SELECT version();'
sudo -u postgres createdb books
sudo -u postgres psql -l | head -8
OUTPUT
                                                                    version                                                                     
------------------------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 16.15 (Ubuntu 16.15-0ubuntu0.24.04.1) on aarch64-unknown-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit
(1 row)

                                                       List of databases
   Name    |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    | ICU Locale | ICU Rules |   Access privileges   
-----------+----------+----------+-----------------+-------------+-------------+------------+-----------+-----------------------
 books     | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |            |           | 
 postgres  | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |            |           | 
 template0 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |            |           | =c/postgres          +

The bookshop has an empty books database. Why sudo -u postgres and no password? The install line said --auth-local peer: on this machine, PostgreSQL trusts that the Linux user postgres is the database user postgres, so Lesson 5's users and permissions decide who gets in. The SQL track picks up from here, with tables and queries.

What's listening? A preview of Lesson 9:

BASH
sudo ss -tlnp | grep -E 'nginx|postgres'
OUTPUT
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=3399,fd=5),("nginx",pid=3398,fd=5),("nginx",pid=3284,fd=5))
LISTEN 0      200        127.0.0.1:5432      0.0.0.0:*    users:(("postgres",pid=5562,fd=6))

nginx listens on port 80, on every address (0.0.0.0). PostgreSQL listens on 5432, but only on 127.0.0.1, this machine, which is the safe default for a database. The app's settings say database_host = db.internal, a different machine's name. Both things are Lesson 9's to untangle.

Three tiers

What you've built has a name. Splitting an application into a front, a middle and a back is called a three-tier architecture, and it's one of the most common designs on the web:

The bookshop's three tiersA visitor sends a request to the web tier, nginx, listening on port 80. nginx serves static files itself, and passes requests under /api/ to the application tier, books-api, on port 8080. Only the application tier talks to the data tier, PostgreSQL, on port 5432, which listens only inside the server. All three tiers run on the one server called bookshop.Visitora browserthe server: bookshopWeb tiernginxport 80/api/Application tierbooks-apiport 8080Data tierPostgreSQLport 5432serves /static/ files itselfonly reachable from insideEach tier only talks to the one next to it. The database never faces the internet.
The three tiers on the bookshop's one server. On a bigger setup, each box becomes its own set of machines, and each arrow crosses a network.
TierIts jobOn this serverIn the cloud, often
Web, or presentationTakes requests from visitors, serves files, passes the rest onnginx, port 80A load balancer, then web servers
ApplicationDoes the work: the bookshop's own codebooks-api, port 8080 (soon)Several app servers, or containers
DataKeeps everything that has to lastPostgreSQL, port 5432A managed database, like Amazon RDS

Each tier only talks to the one next to it: visitors reach nginx, nginx reaches the app, and only the app reaches the database. That's why the split is worth having. Each tier can be grown on its own (more app servers on a busy day, a bigger database as data grows), and each can be locked down on its own: the database never needs to be reachable from the internet at all, which is exactly why PostgreSQL listens only on 127.0.0.1.

Here, all three tiers share one machine, which is normal for a small site and for learning. On a bigger setup, each tier is its own set of machines, and the connections between them cross a network. That's where the app's database_host = db.internal comes from: a database on a different machine, found by name.

Larger organisations often go a step further and split the application tier itself. Instead of one books-api doing everything, there might be a catalog service, an orders service and a payments service, each a small app with its own team, its own releases, and sometimes its own database. These are microservices, and they talk to each other over the network, usually as containers managed by Kubernetes. One app doing everything is called a monolith. Neither is better by default: microservices let big teams work and scale separately, at the cost of many more moving parts to deploy, connect and debug. Plenty of busy sites run happily on a monolith. Either way, the three tiers are still there; the middle one just has more boxes in it.

A monolith and microservicesLeft, a monolith: nginx passes requests to one books-api app, which contains the catalog, orders and payments code, and uses one database. Right, microservices: nginx passes requests to three separate services, catalog, orders and payments. Orders also calls payments over the network. Each service has its own database. Both sides still have a web tier, an application tier and a data tier.A monolithMicroserviceswebapplicationdatanginxbooks-apicatalog codeorders codepayments codeone databaseeverythingnginxcatalogserviceordersservicepaymentsserviceorders calls paymentsdatabasecatalogdatabaseordersdatabasepaymentsone app to build, deploy and debugeach part released and scaled on its ownSame three tiers. The middle one is either one app, or several that talk over the network.
A monolith keeps all the application's code in one app with one database. Microservices split it into small services, each often with its own database, which call each other over the network.

Lesson 9 is about the connections between them, and the AWS track builds the same three tiers as separate cloud services.

Remove and purge

Install a package, then take it away again. Try the commands in any order here first:

Try it: what each apt command changes

The package listold, or empty
apache2 itselfnot installed
Its servicenot running
Its settings, /etc/apache2gone
The 8 packages it pulled ingone

A new server. Run the commands in any order.

Removing comes in three strengths, and the difference matters on a real server. You'll see them for real in "Break it on purpose", below:

CommandRemoves the programRemoves its settings in /etcRemoves what it pulled in
apt remove NAMEYesNo: kept, in case you reinstallNo
apt purge NAMEYesYesNo
apt autoremoveNoNoYes: packages nothing needs any more

None of them touch your own data, like a database or a website's files. That's on purpose.

Keeping it up to date

BASH
apt list --upgradable 2>/dev/null | wc -l
systemctl is-enabled unattended-upgrades
OUTPUT
239
not-found

238 updates waiting (the 239th line is the Listing... header). sudo apt upgrade installs them all; on a real server, you'd read the list first, then run it in a quiet moment, since services restart as their packages update. Run it now if you like: it takes a few minutes, and nothing in the lessons depends on it.

Security fixes shouldn't wait for you to remember, though. Ubuntu's answer is unattended-upgrades, a service that installs security updates by itself, every day. Ubuntu's cloud servers come with it, but not-found says this Vagrant box doesn't. That's worth knowing: never assume a server has something because "Ubuntu always does". Check, and install what's missing:

BASH
sudo apt install -y unattended-upgrades
systemctl is-enabled unattended-upgrades
systemctl is-active unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
OUTPUT
The following NEW packages will be installed:
  python3-distro-info unattended-upgrades
...
Creating config file /etc/apt/apt.conf.d/20auto-upgrades with new version

Creating config file /etc/apt/apt.conf.d/50unattended-upgrades with new version
Created symlink /etc/systemd/system/multi-user.target.wants/unattended-upgrades.service → /usr/lib/systemd/system/unattended-upgrades.service.
...
enabled
active
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

(Some lines are left out.) Enabled and running, like every service package so far. 20auto-upgrades switches it on: refresh the package list every 1 day, and install updates every 1 day. Which updates is set in 50unattended-upgrades; by default, only security fixes, the ones that are safest to install without a person watching. Bigger updates stay your decision, with apt upgrade.

Break it on purpose

A name that doesn't exist.

BASH
sudo apt install nginxx
OUTPUT
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
E: Unable to locate package nginxx

E: means error. A typo, or a package with a different name than you'd guess. apt search finds the real one. The same error on a brand-new server, for a name that's definitely right, usually means apt update hasn't been run yet.

A broken setting, and the safety net. Take the ; off listen 80:

BASH
sudo sed -i 's/listen 80;/listen 80/' /etc/nginx/sites-available/books
sudo nginx -t
sudo systemctl reload nginx
systemctl is-active nginx
OUTPUT
2026/10/30 00:14:10 [emerg] 5678#5678: invalid parameter "server_name" in /etc/nginx/sites-enabled/books:3
nginx: configuration file /etc/nginx/nginx.conf test failed
Job for nginx.service failed.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.
active

Without the ;, nginx read listen 80 server_name books.example.com as one setting, so the error points at line 3, the line after the real mistake. Missing ; errors often do that: look at the line before. The reload failed, but nginx is still active, still serving the old, working settings. A failed reload never takes a site down; a failed restart would, because it stops first. That's the case for nginx -t, then reload, every time. Put it back:

BASH
sudo sed -i 's/listen 80$/listen 80;/' /etc/nginx/sites-available/books
sudo nginx -t
OUTPUT
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Two web servers, one port. Apache (apache2) is the other big web server; it did most of the web's work before nginx, and still runs a lot of it. Install it next to nginx:

BASH
sudo apt install -y apache2
systemctl status apache2 --no-pager | head -6
sudo journalctl -u apache2 --no-pager -n 6
OUTPUT
× apache2.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/apache2.service; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Fri 2026-10-30 00:14:13 UTC; 1s ago
       Docs: https://httpd.apache.org/docs/2.4/
        CPU: 8ms

Oct 30 00:14:13 bookshop apachectl[6197]: (98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:80
Oct 30 00:14:13 bookshop apachectl[6197]: no listening sockets available, shutting down
Oct 30 00:14:13 bookshop apachectl[6197]: AH00015: Unable to open logs
Oct 30 00:14:13 bookshop systemd[1]: apache2.service: Control process exited, code=exited, status=1/FAILURE
Oct 30 00:14:13 bookshop systemd[1]: apache2.service: Failed with result 'exit-code'.
Oct 30 00:14:13 bookshop systemd[1]: Failed to start apache2.service - The Apache HTTP Server.

The package installed fine; the service didn't start. Address already in use ... 0.0.0.0:80: only one program can listen on a port, and nginx got there first. Lesson 9 covers finding who holds a port. Now take Apache away, in stages:

BASH
sudo apt remove -y apache2
dpkg -l apache2 | tail -1
sudo apt purge -y apache2
sudo apt autoremove -y
OUTPUT
The following packages were automatically installed and are no longer required:
  apache2-bin apache2-data apache2-utils libapr1t64 libaprutil1-dbd-sqlite3 libaprutil1-ldap libaprutil1t64 liblua5.4-0
Use 'sudo apt autoremove' to remove them.
The following packages will be REMOVED:
  apache2
...
rc  apache2        2.4.58-1ubuntu8.17 arm64        Apache HTTP Server
...
Purging configuration files for apache2 (2.4.58-1ubuntu8.17) ...
dpkg: warning: while removing apache2, directory '/var/www/html' not empty so not removed
...
The following packages will be REMOVED:
  apache2-bin apache2-data apache2-utils libapr1t64 libaprutil1-dbd-sqlite3 libaprutil1-ldap libaprutil1t64 liblua5.4-0
0 upgraded, 0 newly installed, 8 to remove and 238 not upgraded.

(Some lines are left out.) After remove, dpkg -l shows rc: removed, but its settings are still there. remove even says which 8 packages are now unneeded. purge deleted the settings, and left /var/www/html alone because it wasn't empty: nginx's welcome page lives there too, and apt never deletes files it didn't create. autoremove cleared the 8 extra packages.

On a real server

  • apt update, then read what apt upgrade will change, then upgrade. Many teams do this weekly, and let unattended-upgrades handle security fixes in between.
  • Could not get lock /var/lib/dpkg/lock-frontend means another apt is already running, often unattended-upgrades on a new server's first boot. Wait a few minutes and try again. Never delete the lock file.
  • Does it need a reboot? A kernel update does. If /var/run/reboot-required exists, the server is waiting for one. The Scanning processes... lines after an install are a tool called needrestart, checking which services need restarting.
  • Hold a version with sudo apt-mark hold NAME, so upgrades leave it alone, for example a database you'll upgrade by hand. apt-mark unhold NAME releases it.
  • Software that isn't in Ubuntu's repositories comes from its maker's own repository (Docker and PostgreSQL both offer one), a .deb file, or a snap, Ubuntu's other package format. Be wary of instructions that say curl ... | sudo bash: that runs someone's script as root, unchecked.

Spot the problem

Work out what's wrong before you read the explanation.

It worked on the old server. A setup script runs sudo apt install -y nginx on a brand-new cloud server, and fails with E: Unable to locate package nginx. The same script works on an older server. nginx definitely exists. What's missing? The new server's package list is empty, or out of date, because apt update has never run on it. Scripts should always run sudo apt update before apt install.

Keep this

ToRun
Refresh the package list (do this first)sudo apt update
Find a packageapt search NAME, exactly: apt search --names-only '^NAME$'
Describe itapt show NAME
Installed and available versionsapt-cache policy NAME
Installsudo apt install -y NAME
Remove, keeping settingssudo apt remove NAME
Remove with its settingssudo apt purge NAME
Clear packages nothing needssudo apt autoremove
What can be updatedapt list --upgradable
Update everythingsudo apt upgrade
Which package owns a filedpkg -S /usr/sbin/nginx
Every file in a packagedpkg -L NAME
Installed, or only settings left (rc)dpkg -l NAME
Check nginx's settingssudo nginx -t
Apply them without dropping connectionssudo systemctl reload nginx
Switch an nginx site onsudo ln -s /etc/nginx/sites-available/NAME /etc/nginx/sites-enabled/NAME
nginx's logs/var/log/nginx/access.log, error.log
Run a PostgreSQL commandsudo -u postgres psql -c 'SELECT version();'
Make a database, list themsudo -u postgres createdb NAME, sudo -u postgres psql -l

Where you are now

The server has all three parts: nginx in front, serving files and passing /api/ on; the app in the middle; PostgreSQL behind, with a books database. But they're not connected. nginx gets Connection refused from port 8080, and the database only listens to itself. Lesson 9 is networking from the shell: ports, who's listening on them, and getting the three to talk, including the trap from Lesson 3, the port that's set twice.

Check yourself

  1. What does sudo apt update do?

  2. One apt install postgresql command installed 12 packages. Why?

  3. Why run sudo nginx -t before reloading nginx?

  4. nginx answers 502 Bad Gateway for /api/. What does that mean?

  5. Apache failed to start with Address already in use for port 80. Why?

  6. After sudo apt remove apache2, dpkg -l shows rc. What does that mean?

Key terms

  • Package: a program ready to install, with its dependencies and setup scripts. A .deb file on Ubuntu.
  • Repository: a signed collection of packages to install from.
  • Dependency: another package a package needs to work.
  • apt and dpkg: the everyday package tool, and the lower-level tool it uses to install .deb files.
  • Web server and reverse proxy: a program that answers web requests, and one that passes them on to an app behind it. nginx is both.
  • Upstream: the app a reverse proxy passes requests to.
  • Database cluster: in PostgreSQL, one set of databases managed by one running server.

Go deeper

  • dpkg -L nginx-common | grep etc lists every settings file the nginx package gave you.
  • apt-cache rdepends --installed libpq5 shows which installed packages depend on libpq5, the other way round from apt show.
  • Read /etc/nginx/nginx.conf: it's short, and the include lines show how sites-enabled gets loaded.
  • Lesson 7, Services and logs with systemd, for systemctl and journalctl, which every service here uses.