Updating multiple / hundreds of VM's running Ubuntu would be a full time job luckily you can use unattended upgrades.

Install the package

sudo apt install unattended-upgrades

Configure automatic updates, this is completely customisable.

Edit the configuration file (here with nano – replace with any other text editor):

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

The beginning of the file should look like this:

// Automatically upgrade packages from these (origin:archive) pairs
//
// Note that in Ubuntu security updates may pull in new dependencies
// from non-security sources (e.g. chromium). By allowing the release
// pocket these get automatically pulled in.
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
        // Extended Security Maintenance; doesn't necessarily exist for
        // every release and this system may not have it installed, but if
        // available, the policy for updates is such that unattended-upgrades
        // should also install from here by default.
        "${distro_id}ESM:${distro_codename}";
//      "${distro_id}:${distro_codename}-updates";
//      "${distro_id}:${distro_codename}-proposed";
//      "${distro_id}:${distro_codename}-backports";
};

Anything after a double slash “//” is a comments and has no effect. To “enable” a line, remove the double slash at the beginning of the line (replace with nothing or with spaces to keep alignment).

The most important: uncomment the “updates” line by deleting the two slashes at the beginning of it:

"${distro_id}:${distro_codename}-updates";

Optional: You should uncomment and adapt the following lines to ensure you’ll be notified if an error happens:

Unattended-Upgrade::Mail "user@example.com";
Unattended-Upgrade::MailOnlyOnError "true";

Recommended: remove unused kernel packages and dependencies and make sure the system automatically reboots if needed by uncommenting and adapting the following lines:

Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";

↑ You may have to add a semicolon at the end of this line. ↑

Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:30";

Enable automatic updates and set up update intervals by running:

sudo nano /etc/apt/apt.conf.d/20auto-upgrades

In most cases, the file will be empty. Copy and paste the following lines:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";

The time interval are specified in days, feel free to change the values. Save changes and exit.

Check it!

You can see if the auto-upgrades work by launching a dry run:

sudo unattended-upgrades --dry-run --debug

lsinclair@rebeladmin:~$ sudo unattended-upgrades --dry-run --debug
[sudo] password for lsinclair:
Initial blacklisted packages:
Initial whitelisted packages:
Starting unattended upgrades script
Allowed origins are: o=Ubuntu,a=bionic, o=Ubuntu,a=bionic-security, o=UbuntuESM,a=bionic
Using (^linux-image-[0-9]+\.[0-9\.]+-.*|^linux-headers-[0-9]+\.[0-9\.]+-.*|^linux-image-extra-[0-9]+\.[0-9\.]+-.*|^linux-modules-[0-9]+\.[0-9\.]+-.*|^linux-modules-extra-[0-9]+\.[0-9\.]+-.*|^linux-signed-image-[0-9]+\.[0-9\.]+-.*|^linux-image-unsigned-[0-9]+\.[0-9\.]+-.*|^kfreebsd-image-[0-9]+\.[0-9\.]+-.*|^kfreebsd-headers-[0-9]+\.[0-9\.]+-.*|^gnumach-image-[0-9]+\.[0-9\.]+-.*|^.*-modules-[0-9]+\.[0-9\.]+-.*|^.*-kernel-[0-9]+\.[0-9\.]+-.*|^linux-backports-modules-.*-[0-9]+\.[0-9\.]+-.*|^linux-modules-.*-[0-9]+\.[0-9\.]+-.*|^linux-tools-[0-9]+\.[0-9\.]+-.*|^linux-cloud-tools-[0-9]+\.[0-9\.]+-.*|^linux-buildinfo-[0-9]+\.[0-9\.]+-.*|^linux-source-[0-9]+\.[0-9\.]+-.*|^linux-image-[0-9]+\.[0-9\.]+-.*|^linux-headers-[0-9]+\.[0-9\.]+-.*|^linux-image-extra-[0-9]+\.[0-9\.]+-.*|^linux-modules-[0-9]+\.[0-9\.]+-.*|^linux-modules-extra-[0-9]+\.[0-9\.]+-.*|^linux-signed-image-[0-9]+\.[0-9\.]+-.*|^linux-image-unsigned-[0-9]+\.[0-9\.]+-.*|^kfreebsd-image-[0-9]+\.[0-9\.]+-.*|^kfreebsd-headers-[0-9]+\.[0-9\.]+-.*|^gnumach-image-[0-9]+\.[0-9\.]+-.*|^.*-modules-[0-9]+\.[0-9\.]+-.*|^.*-kernel-[0-9]+\.[0-9\.]+-.*|^linux-backports-modules-.*-[0-9]+\.[0-9\.]+-.*|^linux-modules-.*-[0-9]+\.[0-9\.]+-.*|^linux-tools-[0-9]+\.[0-9\.]+-.*|^linux-cloud-tools-[0-9]+\.[0-9\.]+-.*|^linux-buildinfo-[0-9]+\.[0-9\.]+-.*|^linux-source-[0-9]+\.[0-9\.]+-.*) regexp to find kernel packages
Using (^linux-image-4\.15\.0\-76\-generic$|^linux-headers-4\.15\.0\-76\-generic$|^linux-image-extra-4\.15\.0\-76\-generic$|^linux-modules-4\.15\.0\-76\-generic$|^linux-modules-extra-4\.15\.0\-76\-generic$|^linux-signed-image-4\.15\.0\-76\-generic$|^linux-image-unsigned-4\.15\.0\-76\-generic$|^kfreebsd-image-4\.15\.0\-76\-generic$|^kfreebsd-headers-4\.15\.0\-76\-generic$|^gnumach-image-4\.15\.0\-76\-generic$|^.*-modules-4\.15\.0\-76\-generic$|^.*-kernel-4\.15\.0\-76\-generic$|^linux-backports-modules-.*-4\.15\.0\-76\-generic$|^linux-modules-.*-4\.15\.0\-76\-generic$|^linux-tools-4\.15\.0\-76\-generic$|^linux-cloud-tools-4\.15\.0\-76\-generic$|^linux-buildinfo-4\.15\.0\-76\-generic$|^linux-source-4\.15\.0\-76\-generic$|^linux-image-4\.15\.0\-76\-generic$|^linux-headers-4\.15\.0\-76\-generic$|^linux-image-extra-4\.15\.0\-76\-generic$|^linux-modules-4\.15\.0\-76\-generic$|^linux-modules-extra-4\.15\.0\-76\-generic$|^linux-signed-image-4\.15\.0\-76\-generic$|^linux-image-unsigned-4\.15\.0\-76\-generic$|^kfreebsd-image-4\.15\.0\-76\-generic$|^kfreebsd-headers-4\.15\.0\-76\-generic$|^gnumach-image-4\.15\.0\-76\-generic$|^.*-modules-4\.15\.0\-76\-generic$|^.*-kernel-4\.15\.0\-76\-generic$|^linux-backports-modules-.*-4\.15\.0\-76\-generic$|^linux-modules-.*-4\.15\.0\-76\-generic$|^linux-tools-4\.15\.0\-76\-generic$|^linux-cloud-tools-4\.15\.0\-76\-generic$|^linux-buildinfo-4\.15\.0\-76\-generic$|^linux-source-4\.15\.0\-76\-generic$) regexp to find running kernel packages
Checking: dmidecode ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: dmidecode=3.1-1
Checking: libnss-systemd ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: libnss-systemd=237-3ubuntu10.38
Checking: libpam-systemd ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: libpam-systemd=237-3ubuntu10.38
Checking: libsystemd0 ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: libsystemd0=237-3ubuntu10.38
Checking: libudev1 ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: libudev1=237-3ubuntu10.38
Checking: linux-generic ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: linux-generic=4.15.0.76.78
Checking: linux-headers-generic ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: linux-headers-generic=4.15.0.76.78
Checking: linux-image-generic ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: linux-image-generic=4.15.0.76.78
Checking: systemd ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: systemd=237-3ubuntu10.38
Checking: systemd-sysv ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: systemd-sysv=237-3ubuntu10.38
Checking: udev ([<Origin component:'main' archive:'bionic-updates' origin:'Ubuntu' label:'Ubuntu' site:'gb.archive.ubuntu.com' isTrusted:True>])
adjusting candidate version: udev=237-3ubuntu10.38
pkgs that look like they should be upgraded:
Fetched 0 B in 0s (0 B/s)
fetch.run() result: 0
blacklist: []
whitelist: []
No packages found that can be upgraded unattended and no pending auto-removals

Another way to check if automatic updates work is waiting a few days and checking the unattended upgrades logs:

cat /var/log/unattended-upgrades/unattended-upgrades.log

Done! Ubuntu Server 18.04 should now update itself once a day.