Cloud

Create a Custom OpenStack Horizon Theme (Logo and Colours)

Branding Horizon takes two SCSS files, two SVG logos, a favicon and one rebuild. The catch sits further down: on current releases the theme picker swaps the logo but never the colours, so a cloud that offers two themes shows everyone the same palette. This guide builds a custom OpenStack Horizon theme from scratch, deploys it, and fixes that picker bug so each theme really gets its own stylesheet.

Original content from computingforgeeks.com - post 173863

The theme is called Example Cloud. It sets brand colours through Bootstrap variables, replaces the navbar logo, the login splash logo and the favicon, and makes itself the default. You get the same files deployed two ways: with Kolla-Ansible (the method from our Kolla-Ansible install on Rocky Linux 10) and with the Ubuntu openstack-dashboard packages. Everything below ran in October 2026 on Kolla-Ansible 2026.1 on Rocky Linux 10, and on the packaged dashboard on Ubuntu 26.04 and Ubuntu 24.04.

What an OpenStack Horizon theme contains

A theme is a directory. It is not a plugin or a package. Horizon publishes its static/ subfolder to browsers and keeps everything else private, so templates and overrides can live beside the assets without being served.

Path inside the themeWhat it does
static/_variables.scssBootstrap and Horizon variables. Loaded before the Horizon variables, so your values win over the !default ones.
static/_styles.scssExtra CSS rules, compiled after the stock styles.
static/img/logo.svgNavbar logo on every page after login.
static/img/logo-splash.svgLogo on the login card.
static/img/favicon.icoBrowser tab icon.
templates/Optional per-theme template overrides (login page, header). Not served directly.

Horizon has compiled SCSS with libsass since the 2025.1 release, and libsass only understands @import. Modern Sass syntax such as @use and @forward fails to build. The diagram shows what happens between saving a file and a browser getting CSS, and where the picker bug lives:

OpenStack Horizon theme build diagram: theme files to collectstatic, compress per theme, offline manifest and browser theme cookie

The offline manifest maps each compress block to a compiled file. Stock Horizon gives every theme the same block, so the manifest can only hold the colours of one theme. The override at the end of this guide changes that.

Before you start

You need a working Horizon and sudo on the host that runs it:

  • A Kolla-Ansible deployment with Horizon enabled, managed from the deploy host with the all-in-one or multinode inventory. Our Debian Kolla-Ansible guide uses the same layout.
  • Or Ubuntu 26.04 or 24.04 with the openstack-dashboard package installed (the dashboard is served at /horizon).
  • ImageMagick to turn an SVG into favicon.ico. The steps install it.

DevStack works too, since its Horizon reads the same settings, but the paths differ from both methods here. If you only want a test cloud, the DevStack guide sets one up on a single VM.

Set the theme variables

The theme name appears in a dozen commands, so it goes into variables. On the Kolla-Ansible deploy host, export these. THEME_SRC is where the theme lives on the deploy host, THEME_DIR is where Kolla mounts it inside the Horizon container, EDIT_DIR is the folder you edit files in, and HORIZON_URL is your dashboard address:

export THEME="examplecloud"
export THEME_LABEL="Example Cloud"
export THEME_SRC="/etc/kolla/config/horizon/themes/${THEME}"
export THEME_DIR="/etc/openstack-dashboard/themes/${THEME}"
export HORIZON_URL="https://openstack.example.com"
export EDIT_DIR="${THEME_SRC}"

On an Ubuntu host the theme lives directly under /etc/openstack-dashboard, so EDIT_DIR and THEME_DIR are the same folder:

export THEME="examplecloud"
export THEME_LABEL="Example Cloud"
export THEME_DIR="/etc/openstack-dashboard/themes/${THEME}"
export EDIT_DIR="${THEME_DIR}"
export HORIZON_URL="http://localhost/horizon"

Keep the theme name lowercase. Spaces won’t work. It becomes a directory, a URL path and a cookie value. The label is what users see in the menu. From here on, every file you edit uses ${EDIT_DIR}, so the same commands work on both setups.

Create the theme files

Create the directory first. Kolla:

sudo mkdir -p "${THEME_SRC}/static/img"

Ubuntu:

sudo mkdir -p "${THEME_DIR}/static/img"

Every file below goes under static/ inside that directory. Start with the variables file:

sudo vim "${EDIT_DIR}/static/_variables.scss"

Set the brand colours, then import the variables of the default theme last:

// Example Cloud brand colours. Set before the default theme so they win over its !default values.
$brand-primary:                   #0b5cad;
$brand-success:                   #2e7d32;
$brand-info:                      #0277bd;
$brand-warning:                   #ef6c00;
$brand-danger:                    #c62828;
$body-bg:                         #f4f6fa;
$navbar-height:                   48px;
$navbar-default-bg:               #0b2545;
$navbar-default-border:           #0b2545;
$navbar-default-color:            #dce6f2;
$navbar-default-link-color:       #dce6f2;
$navbar-default-link-hover-color: #ffffff;

@import "/themes/default/variables";

Order matters. The Horizon variables all carry !default, which means “use this only if nothing set it yet”. Put your values after the import and they are silently ignored. The import path starts with /themes/default, which only resolves while the default theme stays in AVAILABLE_THEMES. More on that in the errors section.

Next, the styles file:

sudo vim "${EDIT_DIR}/static/_styles.scss"

Import the default styles, then add your own rules below them:

@import "/themes/default/styles";

// Login card header in the brand colour.
.login .panel-heading {
  background-color: $navbar-default-bg;
  color: #ffffff;
}

// Active sidebar entry.
.nav-pills > li.active > a,
.nav-pills > li.active > a:hover {
  background-color: $brand-primary;
}

Now the navbar logo. It sits on the dark navbar, so the text is white:

sudo vim "${EDIT_DIR}/static/img/logo.svg"

Paste your own SVG, or this placeholder wordmark:

<svg xmlns="http://www.w3.org/2000/svg" width="190" height="32" viewBox="0 0 190 32"><rect x="2" y="6" width="20" height="20" rx="4" fill="#4fa3f7"/><rect x="9" y="13" width="20" height="20" rx="4" fill="#ffffff" fill-opacity="0.85"/><text x="38" y="23" font-family="Helvetica, Arial, sans-serif" font-size="18" font-weight="700" fill="#ffffff">Example Cloud</text></svg>

The splash logo is the one people miss. Horizon draws it inside the header of the login card, and our _styles.scss paints that header in the navbar colour. Our first version used dark text and the brand name vanished against the navy. Use light text here as well:

sudo vim "${EDIT_DIR}/static/img/logo-splash.svg"

The splash version is larger:

<svg xmlns="http://www.w3.org/2000/svg" width="300" height="60" viewBox="0 0 300 60"><rect x="4" y="10" width="36" height="36" rx="7" fill="#4fa3f7"/><rect x="16" y="22" width="36" height="36" rx="7" fill="#ffffff" fill-opacity="0.85"/><text x="66" y="42" font-family="Helvetica, Arial, sans-serif" font-size="30" font-weight="700" fill="#ffffff">Example Cloud</text></svg>

Horizon links img/favicon.ico and expects a real ICO file. Save the icon as an SVG in your home directory first:

vim ~/favicon.svg

A simple two-square mark that matches the logos:

<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect x="4" y="4" width="38" height="38" rx="8" fill="#0b5cad"/><rect x="22" y="22" width="38" height="38" rx="8" fill="#4fa3f7" fill-opacity="0.9"/></svg>

Convert it into a three-size ICO with ImageMagick. On a Rocky Linux deploy host ImageMagick comes from EPEL, so enable that first. ImageMagick 7 there prints a deprecation warning for convert, so use magick:

sudo dnf install -y epel-release
sudo dnf install -y ImageMagick
sudo magick -background none ~/favicon.svg -define icon:auto-resize=48,32,16 "${THEME_SRC}/static/img/favicon.ico"

On Ubuntu, and on a Debian Kolla deploy host, install it with apt. Ubuntu 24.04 still ships ImageMagick 6, which has no magick binary, and convert works on every release we tried:

sudo apt install -y imagemagick
sudo convert -background none ~/favicon.svg -define icon:auto-resize=48,32,16 "${EDIT_DIR}/static/img/favicon.ico"

That completes the theme. The next two sections deploy it, so follow the one that matches your cloud.

Deploy the theme with Kolla-Ansible

Kolla needs two things: the theme registered in globals.yml, and the default theme set in the Horizon custom settings file. Register the theme:

echo "horizon_custom_themes:
  - name: ${THEME}
    label: ${THEME_LABEL}" | sudo tee -a /etc/kolla/globals.yml

If globals.yml already has a horizon_custom_themes key, add the new entry under it instead. A second key with the same name replaces the first. The command echoes what it appended:

horizon_custom_themes:
  - name: examplecloud
    label: Example Cloud

Kolla writes AVAILABLE_THEMES for you, with default, material and your theme under the absolute path /etc/openstack-dashboard/themes/examplecloud. It has no variable for the default theme, so set that, and limit the menu to one entry, in _9999-custom-settings.py. That file often holds other Horizon settings (single sign-on, session timeouts), so the command keeps a backup and appends instead of replacing it:

sudo cp -a /etc/kolla/config/horizon/_9999-custom-settings.py /etc/kolla/config/horizon/_9999-custom-settings.py.bak 2>/dev/null
echo "SELECTABLE_THEMES = [
    ('${THEME}', '${THEME_LABEL}', '${THEME_DIR}'),
]
DEFAULT_THEME = '${THEME}'" | sudo tee -a /etc/kolla/config/horizon/_9999-custom-settings.py

The file is Python, so if it already sets DEFAULT_THEME or SELECTABLE_THEMES, the lines you just appended win. With a single selectable theme, Horizon hides the theme picker completely. Apply the change to Horizon only:

cd ~
source ~/kolla-venv/bin/activate
kolla-ansible reconfigure -i ./all-in-one -t horizon

The run took 20 seconds on our all-in-one host and finished with no failures:

PLAY RECAP *********************************************************************
localhost                  : ok=30   changed=7    unreachable=0    failed=0    skipped=17   rescued=0    ignored=0

The container restarts, notices that the settings changed, and runs collectstatic and compress before uWSGI starts. Do not test until docker ps shows Horizon as healthy, then confirm the build and that the login page loads the theme images:

sudo docker logs horizon 2>&1 | grep '^Compressed' | tail -1
curl -s "${HORIZON_URL}/auth/login/" | grep -oE "themes/${THEME}/img/[a-z.-]+"

Three contexts means default, material and examplecloud were compiled, and both theme images are linked:

Compressed 7 block(s) from 21 template(s) for 3 context(s).
themes/examplecloud/img/favicon.ico
themes/examplecloud/img/logo-splash.svg

The navbar logo.svg does not appear in that list. The login page shows only the splash logo, and the navbar logo loads after sign in.

On a multinode deployment, use your multinode inventory in place of ./all-in-one, and run the docker commands on a controller that hosts Horizon, not on the deploy host. If you keep per-host settings in /etc/kolla/config/horizon/<hostname>/_9999-custom-settings.py, Kolla uses that file instead of the global one, so add the lines there.

Deploy the theme on Ubuntu 26.04 and 24.04

The Ubuntu package keeps its settings in /etc/openstack-dashboard/local_settings.py, which sets DEFAULT_THEME = 'ubuntu'. Leave that file alone and add a snippet to local_settings.d, which loads after it and survives package upgrades:

echo "AVAILABLE_THEMES = [
    ('default', 'Default', 'themes/default'),
    ('${THEME}', '${THEME_LABEL}', '${THEME_DIR}'),
]
SELECTABLE_THEMES = [
    ('${THEME}', '${THEME_LABEL}', '${THEME_DIR}'),
]
DEFAULT_THEME = '${THEME}'" | sudo tee /usr/share/openstack-dashboard/openstack_dashboard/local/local_settings.d/_20_custom_theme.py

Unlike Kolla, the packaged dashboard has no theme list of its own to extend, so this snippet defines AVAILABLE_THEMES itself. Keep default in it, since your _variables.scss imports from it. The command prints the file it wrote:

AVAILABLE_THEMES = [
    ('default', 'Default', 'themes/default'),
    ('examplecloud', 'Example Cloud', '/etc/openstack-dashboard/themes/examplecloud'),
]
SELECTABLE_THEMES = [
    ('examplecloud', 'Example Cloud', '/etc/openstack-dashboard/themes/examplecloud'),
]
DEFAULT_THEME = 'examplecloud'

Ubuntu runs Horizon with offline compression, so nothing changes until you collect the static files, compress them and reload Apache:

cd /usr/share/openstack-dashboard
sudo python3 manage.py collectstatic --noinput --clear -v0
sudo python3 manage.py compress --force
sudo systemctl reload apache2

On Ubuntu 26.04 the build prints deprecation warnings from the packaged libraries. They do not affect the build. The line that matters is the last one:

/usr/lib/python3/dist-packages/debreach/__init__.py:6: DeprecationWarning: distutils Version classes are deprecated. Use packaging.version instead.
  version_info = version.StrictVersion(__version__).version
/usr/lib/python3/dist-packages/debreach/__init__.py:6: DeprecationWarning: distutils Version classes are deprecated. Use packaging.version instead.
  version_info = version.StrictVersion(__version__).version
/usr/lib/python3/dist-packages/compressor/base.py:207: DeprecationWarning: codecs.open() is deprecated. Use open() instead.
  with codecs.open(filename, "r", charset) as fd:
Compressing... done
Compressed 7 block(s) from 14 template(s) for 2 context(s).

Both steps are fast on the packaged dashboard: about 2 seconds for collectstatic and 3 seconds for compress on our 2 vCPU VM. The Kolla container needed about 8 seconds with five themes in our test lab. Check the login page:

curl -s "${HORIZON_URL}/auth/login/" | grep -oE "themes/${THEME}/img/[a-z.-]+"

Ubuntu 26.04 and 24.04 return the same two theme images:

themes/examplecloud/img/favicon.ico
themes/examplecloud/img/logo-splash.svg

Ubuntu 24.04 ships the older 2024.1 dashboard in its archive. We ran these steps on 24.04 with the Ubuntu Cloud Archive for 2026.1 enabled (sudo add-apt-repository cloud-archive:gazpacho). That gives the same Horizon as 26.04, so the steps and output match. On the stock 24.04 dashboard the theme steps are the same.

Check the theme in the browser

Open the dashboard. The login card shows the splash logo on the navy header:

OpenStack Horizon custom theme login page with Example Cloud splash logo on Kolla-Ansible

After signing in, the navbar carries the logo and brand colour, and the active sidebar entry uses $brand-primary. Buttons, links and the usage charts pick up the same blue because they read that variable too:

OpenStack Horizon custom theme project overview with branded navbar and sidebar

The Ubuntu 26.04 dashboard at /horizon renders the identical login page from the same files:

OpenStack Horizon custom theme login page on Ubuntu 26.04 openstack-dashboard package

If one browser still shows the old look while a private window shows the new one, that is the browser cache. The compressed CSS file name changes with every build, so a hard reload fixes it.

Offer more than one theme without breaking the picker

A single branded theme is what most clouds need, and it works on stock Horizon. The trouble starts when you let users choose. List a second theme in SELECTABLE_THEMES and a Themes section appears in the user menu. Kolla:

echo "SELECTABLE_THEMES = [
    ('${THEME}', '${THEME_LABEL}', '${THEME_DIR}'),
    ('default', 'Default', 'themes/default'),
]" | sudo tee -a /etc/kolla/config/horizon/_9999-custom-settings.py

The Kolla command appends a new SELECTABLE_THEMES, which overrides the earlier one. Ubuntu (this replaces the snippet from earlier, which is our own file, so run it before the fix below or re-add the fix line afterwards):

echo "AVAILABLE_THEMES = [
    ('default', 'Default', 'themes/default'),
    ('${THEME}', '${THEME_LABEL}', '${THEME_DIR}'),
]
SELECTABLE_THEMES = [
    ('${THEME}', '${THEME_LABEL}', '${THEME_DIR}'),
    ('default', 'Default', 'themes/default'),
]
DEFAULT_THEME = '${THEME}'" | sudo tee /usr/share/openstack-dashboard/openstack_dashboard/local/local_settings.d/_20_custom_theme.py

Rebuild the same way as before: kolla-ansible reconfigure -i ./all-in-one -t horizon on Kolla, or the collectstatic, compress and Apache reload block on Ubuntu. The user menu now lists both themes:

OpenStack Horizon theme picker in the user menu listing Example Cloud and Default themes

Pick Default and the page reloads with the stock OpenStack logo, but the navy navbar and Example Cloud blue stay. The templates followed the cookie. The stylesheet did not:

OpenStack Horizon Default theme selected but Example Cloud colours still applied on stock 2026.1

That is the bug, and it is not in your theme.

Why the colours never change

Up to the 2025.2 release, Horizon pasted the theme SCSS straight into each page with the theme name already filled in, so every theme produced a different compress block. In 2026.1, upstream commit “Link to SCSS styles instead of inlining them in every page” replaced that with a single <link> to dashboard/scss/themes.scss. That markup is identical for every theme. Offline compression keys the manifest on the markup of the block, so all themes share one entry, and it is compiled for DEFAULT_THEME. With offline compression turned off, the first theme anyone requests gets cached and served to everybody instead.

We confirmed it on every build we could get, using two test themes with distinct colours:

Horizon buildReleaseEach theme gets its own CSS?
Ubuntu 24.04 archive, 24.0.1 (pyScss)2024.1Yes
Ubuntu 26.04 archive, 25.7.12026.1No
Ubuntu 24.04 + Cloud Archive Gazpacho, 25.7.12026.1No
Kolla image horizon:2026.1-rocky-10, 25.7.42026.1No
Ubuntu 26.04 + Cloud Archive Hibiscus (proposed), 27.0.02026.2No
pip install, 27.0.0 with 2026.2 constraints2026.2No

The template is unchanged on the master branch as of October 2026. The first tag that carries the <link> is 25.6.0.

Check whether your cloud is affected

Fetch the login page with each theme cookie and compare the theme stylesheet (the second CSS file on the page). This works from any machine that reaches the dashboard. If your dashboard uses a self-signed certificate, add -k to curl here and in the earlier checks, otherwise they print nothing:

for t in "${THEME}" default; do
  echo "${t}: $(curl -s -b "theme=${t}" "${HORIZON_URL}/auth/login/" | grep -oE 'output\.[a-z0-9]+\.css' | sed -n 2p)"
done

On stock Kolla both themes print the same file:

examplecloud: output.b9ce38dfb7b1.css
default: output.b9ce38dfb7b1.css

Same file name means same colours, whatever the user picks. If your check prints two different files (the stock 2024.1 dashboard on Ubuntu 24.04 does), skip the fix below.

Fix it with a _stylesheets.html override

The fix puts the theme name back inside the compress block, the way older releases did. The Horizon offline_context already passes THEME and THEME_DIR to the compressor, so each theme gets its own block, its own manifest key and its own CSS. It has to be a global template override. A copy under the templates/ folder fails, because offline compression never scans theme template folders and Horizon then returns HTTP 500 with OfflineGenerationError.

Keep the override inside the theme, in a folder Horizon does not publish. Kolla:

sudo mkdir -p "${THEME_SRC}/overrides"

Ubuntu:

sudo mkdir -p "${THEME_DIR}/overrides"

Create the template:

sudo vim "${EDIT_DIR}/overrides/_stylesheets.html"

This is the stock 2026.1 _stylesheets.html with only the middle block changed. It is the exact file we ran on every affected build in the table above:

{% load compress %}
{% load themes %}

{% compress css %}
<link href="{{ STATIC_URL }}horizon/lib/bootstrap_datepicker/datepicker3.css" type="text/css" media="screen" rel="stylesheet" />
<link href="{{ STATIC_URL }}horizon/lib/rickshaw/rickshaw.css" type="text/css" media="screen" rel="stylesheet" />
{% endcompress %}

{% current_theme as current_theme %}
{% theme_dir as theme_dir %}
{% with THEME=current_theme THEME_DIR=theme_dir %}
{% compress css %}
<style type="text/scss">
@import "/{{ THEME_DIR }}/{{ THEME }}/variables";
@import "/dashboard/scss/horizon.scss";
@import "/app/app";
@import "/{{ THEME_DIR }}/{{ THEME }}/styles";
</style>
{% endcompress %}
{% endwith %}

{% compress css %}
{% for file in HORIZON_CONFIG.scss_files %}
<link type="text/scss" href="{{ STATIC_URL }}{{ file }}" rel="stylesheet" />
{% endfor %}
{% endcompress %}

<link rel="shortcut icon" href="{% themable_asset 'img/favicon.ico' %}"/>
<link rel="apple-touch-icon" sizes="180x180" href="{% themable_asset 'img/apple-touch-icon.png' %}" />
<link rel="mask-icon" href="{% themable_asset 'img/safari-pinned-tab.svg' %}" color="#5bbad5" />

Tell Django to look in that folder before the stock templates. The settings snippet runs as Python, so one line does it. Kolla:

echo "TEMPLATES[0]['DIRS'].insert(0, '${THEME_DIR}/overrides')" | sudo tee -a /etc/kolla/config/horizon/_9999-custom-settings.py

Ubuntu:

echo "TEMPLATES[0]['DIRS'].insert(0, '${THEME_DIR}/overrides')" | sudo tee -a /usr/share/openstack-dashboard/openstack_dashboard/local/local_settings.d/_20_custom_theme.py

On Kolla that line needs the container path, so both versions use ${THEME_DIR}. Rebuild with kolla-ansible reconfigure -i ./all-in-one -t horizon on Kolla, or the collect, compress and reload commands on Ubuntu. The compress count goes up, from 7 to 9 blocks on Kolla and from 7 to 8 on Ubuntu, because each theme now has its own. Run the check again:

Horizon theme picker CSS check on Kolla-Ansible before and after the stylesheet override

Each theme now loads a different file:

examplecloud: output.d577653077fb.css
default: output.07d10a369294.css

Picking Default now gives the real stock look: light grey navbar, stock logo, lighter blue:

OpenStack Horizon Default theme with stock colours after the _stylesheets.html override

Ubuntu 26.04 and 24.04 behaved the same way: output.465630f9779d.css and output.f6fb30d69eae.css for both themes before the override, and two different files after it.

Errors from older themes on current Horizon

Themes written for releases before 2026.1 break in a few predictable ways. Each of these came up in our lab.

Icons render as empty boxes

Horizon now ships Font Awesome 6. Its font families are Font Awesome 6 Free and Font Awesome 6 Brands, and no FontAwesome family exists anymore. An old rule such as .fa { font-family: FontAwesome; } in _styles.scss points every icon at a font that never loads:

OpenStack Horizon icons rendered as empty boxes by an old FontAwesome font-family rule

Delete the rule. If you set icon fonts yourself, use font-family: 'Font Awesome 6 Free' with font-weight: 900 for solid icons.

Sign Out shows a blank page (HTTP 405)

Logout became a POST request with a CSRF token in 2026.1. A theme that overrides header/_user_menu.html with the old <a href="{% url 'logout' %}"> link sends a GET, and Horizon answers with an empty 405 page. A plain GET with curl shows the headers:

curl -s -o /dev/null -D - "${HORIZON_URL}/auth/logout/" | head -4

Only POST is allowed:

HTTP/2 405 
content-type: text/html; charset=utf-8
allow: POST
x-frame-options: DENY

The Horizon log shows Method Not Allowed (GET): /auth/logout/. Copy the current stock _user_menu.html and reapply your changes on top of it. The stock file wraps Sign Out in a <form method="post"> with {% csrf_token %}.

“File to import not found or unreadable: /themes/default/variables”

This stops the compress step, and on Kolla it stops the Horizon container. It appears when default is missing from AVAILABLE_THEMES. Horizon only collects the themes listed there, so the @import "/themes/default/variables" in your theme has nothing to resolve. This capture and the next one came from our test themes, hence the different names:

CommandError: An error occurred during rendering _stylesheets.html: Error: File to import not found or unreadable: /themes/default/variables.
        on line 4:1 of themes/cfgblue/_variables.scss

Keep default in AVAILABLE_THEMES and hide it with SELECTABLE_THEMES, as the settings above do.

The Kolla Horizon container exits after a theme change

Using a relative path such as themes/examplecloud for a custom theme in _9999-custom-settings.py kills the container. Kolla mounts custom themes under /etc/openstack-dashboard/themes, while relative paths resolve inside the Python package. docker logs horizon shows the cause:

?: (staticfiles.W004) The directory '/var/lib/kolla/venv/lib64/python3.12/site-packages/openstack_dashboard/themes/cfgred' in the STATICFILES_DIRS setting does not exist.
++ /var/lib/kolla/venv/bin/python /var/lib/kolla/venv/bin/manage.py compress --force
CommandError: An error occurred during rendering _stylesheets.html: Error: File to import not found or unreadable: /themes/cfgred/variables.

Always use the absolute /etc/openstack-dashboard/themes/<name> path, which is what Kolla writes itself.

Re-check the theme after every Horizon upgrade

The _stylesheets.html override is a copy of an upstream template, so treat it like a patch. Compare it with the stock file of the release you are moving to, and carry over any upstream change outside the middle block. On Kolla, pull the new Horizon image first and read the template straight out of it. Replace the image tag with your target release:

sudo docker run --rm --entrypoint cat quay.io/openstack.kolla/horizon:2026.1-rocky-10 /var/lib/kolla/venv/lib/python3/site-packages/openstack_dashboard/templates/_stylesheets.html | diff - "${THEME_SRC}/overrides/_stylesheets.html"

Against the current 2026.1 image, only the themes block differs. The IPv4 forwarding warning comes from docker run on a Kolla host and does not matter here:

WARNING: IPv4 forwarding is disabled. Networking will not work.
8a9,11
> {% current_theme as current_theme %}
> {% theme_dir as theme_dir %}
> {% with THEME=current_theme THEME_DIR=theme_dir %}
10c13,18
< <link type="text/scss" href="{{ STATIC_URL }}dashboard/scss/themes.scss" rel="stylesheet" />
---
> <style type="text/scss">
> @import "/{{ THEME_DIR }}/{{ THEME }}/variables";
> @import "/dashboard/scss/horizon.scss";
> @import "/app/app";
> @import "/{{ THEME_DIR }}/{{ THEME }}/styles";
> </style>
11a20
> {% endwith %}

On Ubuntu, run the comparison after apt upgrade has installed the new dashboard and before you rebuild the static files:

sudo diff /usr/lib/python3/dist-packages/openstack_dashboard/templates/_stylesheets.html "${THEME_DIR}/overrides/_stylesheets.html"

On a current release the only difference is the themes block. If upstream fixes the bug, the stock file will contain the theme name inside that block again, and you can delete the override and its TEMPLATES line. After the upgrade, run the cookie check above. Two different file names means the picker still works.

Kolla adds one trap of its own. The container only rebuilds static files when its settings checksum changes, and a failed build still leaves the old checksum in place. Fix a broken theme, reconfigure, and Horizon can keep answering every page with HTTP 500 and You have offline compression enabled but key "..." is missing from offline manifest, even though the theme is now correct. Delete the stored checksum and restart the container to force a full rebuild:

sudo docker exec -u 0 horizon rm -f /var/lib/kolla/.settings.md5sum.txt
sudo docker restart horizon

Kolla also never deletes a theme you remove from horizon_custom_themes. Its files stay in /etc/kolla/horizon/themes on the host and in the container until you remove that directory by hand. With the theme in place, the OpenStack command line client keeps working exactly as before, since only the dashboard changed. The login card is also where a single sign-on choice appears once Keystone federation is in place; our Keycloak, authentik, Zitadel and Authelia comparison covers picking the identity provider.

Keep reading

Create and Configure Bridge Networking For KVM in Linux Virtualization Create and Configure Bridge Networking For KVM in Linux Extend Root Filesystem Using LVM on Linux KVM Extend Root Filesystem Using LVM on Linux Install Nextcloud on Ubuntu 26.04 LTS Cloud Install Nextcloud on Ubuntu 26.04 LTS Best AWS Solutions Architect Associate Books for SAA-C03 (No SAA-C04 Yet) Books Best AWS Solutions Architect Associate Books for SAA-C03 (No SAA-C04 Yet) Best AWS Data Engineer Associate Books for DEA-C01 in 2026 Books Best AWS Data Engineer Associate Books for DEA-C01 in 2026 CDN Traffic Steering Needs a Look-Ahead, Not Just a Snapshot Cloud CDN Traffic Steering Needs a Look-Ahead, Not Just a Snapshot

Leave a Comment

Press ESC to close