If you’d like to enable unattended updates in Ubuntu and also configure a time for reboots to occur then the following should help you.
Now, manual steps can be found on the Ubuntu website but below I’ve drafted up some Ansible code for those of us that want to manage things using a Configuration as Code (CaS) model.

Our playbook consists of a small number of components:

  • Inventory file (usually found at: /etc/ansible/hosts )
  • Variables file (/vars/main.yml)
  • Playbook (unattended_upgrades.yml)
directory structure
├── unattended_upgrades.yml
├── vars
   └── main.yml


First off, you’ll want to build out your Ansible inventory so that you have three groups:

  • patch_group_a
  • patch_group_b
  • patch_group_c

For example:





The playbook assumes that you have a file under the ‘vars’ directory

You can change the group_names and timings by editing this file, which comes next:


reboot_offset: 15m

install_time: "{%- if 'patch_group_a' in group_names -%}
            {%- elif 'patch_group_b' in group_names -%}
            {%- elif 'patch_group_c' in group_names -%}
            {% else %}
            {%- endif -%}"

reboot_time: "{%- if 'patch_group_a' in group_names -%}
            {%- elif 'patch_group_b' in group_names -%}
            {%- elif 'patch_group_c' in group_names -%}
            {% else %}
            {%- endif -%}"

You can easily add or remove groups in the template or change the respective values for reboot_time.


The main playbook does the following:

  • Installs: update-notifier-common
  • Creates: unattended upgrades configuration file
  • Enables: automated reboots

Note that the goal here is configuring an already existing service, namely, ‘unattended-upgrades‘. Specifically, we’re controlling the reboots.

What does the actual work of installing the updates is the unattended-upgrades script which gets invoked when the ‘apt-daily-upgrade.service‘ runs.

The timing for the execution of ‘apt-daily-upgrade.service‘ is controlled via a systemd timer named ‘apt-daily-upgrade.timer‘.

See the reference section at the end for full documentation.


While this is sufficient to enable automatic reboots, it might not be exactly what you have in mind because the schedule by which the updates are installed is controlled separately via the apt-daily-upgrade.timer.

The configuration file can be found at:

Description=Daily apt upgrade and clean activities

OnCalendar=*-*-* 6:00


As one can see there’s quite a bit of scope for customising when it will run and, ideally, you’ll probably want it to run as close to your specified reboot window as possible.
To achieve this using Ansible the vars file contains some jinja2 templating which allows us to dynamically set the value of variables based on whether a host is a member of an inventory group or not.

In our implementation here we’re reducing the randomized delay from 60 minutes down to 15 minutes, and setting a gap between install and reboot of 30 minutes. Given the offset value this means a worst-case scenario of a host having 15 minutes to install all updates before the reboot (and a best-case of 45 minutes). That’s fine for me but you may want to increase both values based on the needs of your environment.

Our final playbook will now look like this:

- name: Configure automated updates (Ubuntu)
  hosts: all
  become: true
  gather_facts: false
    - vars/main.yml

## https://help.ubuntu.com/community/AutomaticSecurityUpdates

    - name: Install apt prerequisites for automated reboots
        name: update-notifier-common
        state: present

    - name: Create unattended upgrades configuration file
        dest: /etc/apt/apt.conf.d/20auto-upgrades
        block: |
          APT::Periodic::Update-Package-Lists "1";
          APT::Periodic::Unattended-Upgrade "1";
        marker: "// {mark} ANSIBLE MANAGED BLOCK - unattended_upgrades settings"
        create: true
        mode: "0644"
        owner: root
        group: root
      register: unattended_upgrades_config_set

    - name: Enable automated reboots
        dest: /etc/apt/apt.conf.d/50unattended-upgrades
        block: |
          Unattended-Upgrade::Automatic-Reboot "true";
          Unattended-Upgrade::Automatic-Reboot-Time "{{ reboot_time }}";
        marker: "// {mark} ANSIBLE MANAGED BLOCK - unattended_upgrades settings"
        create: true
        mode: "0644"
        owner: root
        group: root
      register: unattended_upgrades_settings_set

    - name: Dpkg reconfigure
        cmd: dpkg-reconfigure -f noninteractive unattended-upgrades
      register: dpkg_reconfigure_unattended_upgrades
        - unattended_upgrades_config_set.changed or

    - name: Configure updates installation timing
        path: /lib/systemd/system/apt-daily-upgrade.timer
        regexp: '^OnCalendar'
        line: OnCalendar=*-*-* {{ install_time }}

    - name: Configure updates installation timing offset
        path: '/lib/systemd/system/apt-daily-upgrade.timer'
        regexp: '^RandomizedDelaySec'
        line: 'RandomizedDelaySec={{ reboot_offset}}'


Naturally, the best way to validate changes aside from checking the idempotency of the Ansible playbook is to see whether things work as expected. However, there are also some additional checks that you can run.

Firstly, you can verify whether the timer that triggers the service has picked up the correct schedule.

Let’s verify the config contents:

 grep -E OnCalendar /lib/systemd/system/apt-daily-upgrade.timer

Which should hopefully match what the playbook has configured. For example:

OnCalendar=*-*-*  01:30

Then we’ll check the actual value being used by the Systemd timer unit:

systemctl status apt-daily-upgrade.timer
 apt-daily-upgrade.timer - Daily apt upgrade and clean activities
     Loaded: loaded (/lib/systemd/system/apt-daily-upgrade.timer; enabled; preset: enabled)
     Active: active (waiting) since Fri 2023-10-20 22:22:53 BST; 17h ago
      Until: Fri 2023-10-20 22:22:53 BST; 17h ago
    Trigger: Sun 2023-10-22 01:37:11 BST; 9h left
   Triggers:  apt-daily-upgrade.service

Note how the Trigger value matches up with our configured value, taking into account the randomized offset of up to 15 minutes (in this case our actual offset is about 8 minutes).

Now, if we travel forward in time to the date of the trigger and query the logs for the service:

grep -E apt-daily-upgrade.service /var/log/syslog

Whose output should show something like this:

2023-10-22T01:37:11.887415+01:00 localhost systemd[1]: Starting apt-daily-upgrade.service - Daily apt upgrade and clean activities...
2023-10-22T01:37:54.120174+01:00 localhost systemd[1]: apt-daily-upgrade.service: Deactivated successfully.
2023-10-22T01:37:54.121916+01:00 localhost systemd[1]: Finished apt-daily-upgrade.service - Daily apt upgrade and clean activities.
2023-10-22T01:37:54.123701+01:00 localhost systemd[1]: apt-daily-upgrade.service: Consumed 9.982s CPU time.

Another approach for validating the remaining settings is to use the apt-config dump command:

apt-config dump APT::Periodic::Unattended-Upgrade && \
apt-config dump APT::Periodic::Update-Package-Lists && \
apt-config dump Unattended-Upgrade::Automatic-Reboot && \
apt-config dump Unattended-Upgrade::Automatic-Reboot-Time

which should hopefully give us the results that we expect:

APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::Update-Package-Lists "1";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";

A note on update types

By default the unattended-upgrades package is configured to install only security updates. However, you can easily change this by uncommenting what you want from the configuration file. Here’s the top of the file and the settings that you want:

// 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 {
        // 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}:${distro_codename}-updates";
//      "${distro_id}:${distro_codename}-proposed";
//      "${distro_id}:${distro_codename}-backports";

If you want to update applications in the same way as if you’d run an apt-upgrade then you’ll probably want to uncomment the line highlighted above.

Special considerations and issues

  • Please note that the comment-syntax for the Ubuntu unattended config files uses double-forward slashes (e.g. ‘//’) and therefore using hashes for comments will cause a failure (e.g. ‘#’). As a result, the markers for the Ansible Blockinfile module must use the double-forward slash method.
  • Since this all relies on unattended-upgrades functioning you’ll want to make sure everything is already working by checking your logs under:
logfiles location



Leave a Reply

Your email address will not be published. Required fields are marked *