🧰 Build a Test Cloud with DevStack: From a Blank Ubuntu to a Working Cloud

How to install a complete OpenStack test cloud on one machine with DevStack: the stack user, local.conf, the 18-minute install, what it changed on the machine, how to check it, and what survives a reboot (and the two small fixes that make everything come back)

💡 How this page was made. The whole install was run for real on 8 October 2026 in a throwaway virtual machine (QEMU/KVM, the official Ubuntu 24.04 cloud image, 4 CPUs, 8 GB of RAM, a 58 GB disk and 4 GB of swap) using DevStack's stable/2026.1 branch (the "Gazpacho" release from the previous page). The numbers (time, downloads, disk, memory) are from that run, on one machine with a fast connection; yours will differ. The commands for creating the stack user and the warnings about passwords and production use come from the DevStack documentation, read the same day. Things not run are marked.

What DevStack is, and what it is not

DevStack is, in its own documentation's words, "a series of extensible scripts used to quickly bring up a complete OpenStack environment based on the latest versions of everything from git master." You give it one small file (local.conf), run one script (stack.sh), and it installs the packages, downloads the OpenStack source code, writes the configuration for every service, starts them, and creates a first set of users, networks and images.

It is meant for learning, development and testing, and the documentation is blunt about it: "DevStack will make substantial changes to your system during installation. Only run DevStack on servers or virtual machines that are dedicated to this purpose."

⚠️ Use a machine you can throw away. A virtual machine is ideal: you can snapshot it, break it and rebuild it. Never run DevStack on your own laptop's operating system, or on a server that does anything else. And never expose a DevStack cloud to the internet: its passwords are in a text file and its design is for convenience, not for hardening. For a cloud that has to last, people use packaged installers (see the end of this page).

The documentation recommends Ubuntu 24.04 (Noble) as the most tested system, which is what this page uses. The default services are the ones from the previous page: Keystone, Glance, Nova, Placement, Cinder, Neutron and Horizon.

What you need

On the machine used here:

Step 1: a user called stack

DevStack must not run as root, and it refuses. This is the real message, produced by running it as root on purpose (the script stops right there and changes nothing):

$ sudo ./stack.sh        (run as root, on purpose; the trace lines starting with + are left out)
DevStack should be run as a user with sudo permissions, 
not root.
A "stack" user configured correctly can be created with:
 /home/stack/devstack/tools/create-stack-user.sh
exit status: 1

So create a normal user with sudo rights. These are the commands from the DevStack documentation:

sudo useradd -s /bin/bash -d /opt/stack -m stack
sudo chmod +x /opt/stack
echo "stack ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/stack
sudo su stack && cd ~

(DevStack also ships a script that does the same, tools/create-stack-user.sh, named in the message above. In the lab, the user was created at first boot by cloud-init, with the same sudo rule and its home in /home/stack instead of /opt/stack; DevStack worked fine, and created /opt/stack itself for the code and data. So these commands were not run by hand here.) Then install git if it is missing (sudo apt-get install git -y).

Step 2: get DevStack, on a stable branch

By default git clone gives you the master branch, the development version, which changes every day and sometimes breaks. For a learning environment that behaves the same tomorrow, use a stable branch, named after the release:

$ git clone https://opendev.org/openstack/devstack -b stable/2026.1 && cd devstack
$ git log -1 --format="%h %cd" --date=short
da2f4d7 2026-05-21
$ git rev-parse --abbrev-ref HEAD
stable/2026.1
$ ls
CONTRIBUTING.rst FUTURE.rst HACKING.rst LICENSE Makefile README.rst clean.sh data doc extras.d 
files functions functions-common gate inc lib local.conf openrc playbooks roles run_tests.sh 
samples stack.sh stackrc tests tools tox.ini unstack.sh 

stable/2026.1 was the most settled branch on 8 October 2026 (the very new 2026.2 branch existed too). To see which stable branches exist: git ls-remote --heads https://opendev.org/openstack/devstack 'stable/*'. Before you start, check that the branch supports your operating system: its stack.sh has a SUPPORTED_DISTROS line, and the 2026.1 one lists trixie|bookworm|noble|rhel9|rhel10. If your distribution is not in it, stack.sh prints WARNING: this script has not been tested on ... and stops with "If you wish to run this script anyway run with FORCE=yes" (that override exists, per the script; it was not tried).

Step 3: local.conf

One file, in the devstack directory, controls the install. The one used here:

$ cat ~/devstack/local.conf
[[local|localrc]]
ADMIN_PASSWORD=practice
DATABASE_PASSWORD=practice
RABBIT_PASSWORD=practice
SERVICE_PASSWORD=practice

HOST_IP=10.0.2.15
LIBVIRT_TYPE=kvm

LOGFILE=/opt/stack/logs/stack.sh.log
LOG_COLOR=False
LOGDAYS=1

# keep the lab small
VOLUME_BACKING_FILE_SIZE=10G
disable_service tempest

Line by line:

The document's own example also sets FLOATING_RANGE and FIXED_RANGE (the addresses of the outside network and of the instances' private network). The lab left them at DevStack's defaults, and the next sections show what those turned out to be.

Step 4: run it

cd ~/devstack
./stack.sh

The documentation's description is accurate: "a seemingly endless stream of activity ensues". Every command is printed with a + in front (the output of this run was about 20,000 lines), so the screen is not a progress bar, only a reassurance that something is happening. Two practical tips:

When it finished, 17 minutes and 44 seconds after it started, it printed:

$ ./stack.sh      (the last lines it prints)
This is your host IP address: 10.0.2.15
This is your host IPv6 address: fec0::5054:ff:fe12:3456
Horizon is now available at http://10.0.2.15/dashboard
Keystone is serving at http://10.0.2.15/identity/
The default users are: admin and demo
The password: practice
Services are running under systemd unit files.
For more information see: 
https://docs.openstack.org/devstack/latest/systemd.html
DevStack Version: 2026.1
Change: da2f4d73f5ad74fc8ecfbe15bd7e20f6b0982dbb Fix noble OVN source Zuul parent 2026-05-21 15:33:38 +0000
OS Version: Ubuntu 24.04 noble

The log also holds a timing table, which shows where the time went:

$ grep -A 22 "DevStack Component Timing" /opt/stack/logs/stack.sh.log
DevStack Component Timing
 (times are in seconds)
=========================
wait_for_service      19
async_wait           104
osc                  269
apt-get              251
test_with_retry        3
dbsync                 6
pip_install          138
apt-get-update        15
run_process           32
git_timed            205
-------------------------
Unaccounted time      22
=========================
Total runtime        1064

=================
 Async summary
=================
 Time spent in the background minus waits: 362 sec
 Elapsed time: 1064 sec

Reading it: most of the time went into creating things in the cloud (osc, 269 seconds: users, projects, endpoints, networks, flavours, with the openstack command), installing packages (apt-get, 251), downloading source code (git_timed, 205) and installing Python packages (pip_install, 138). Starting the services took 32 seconds, and the database setup 6. About six minutes of the work happened in the background at the same time, which is why the pieces add up to more than the total.

One thing that looks alarming in the middle of the log and is not: a line starting ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. It is pip complaining about version pins between libraries, and the install finished fine. The question to ask of any message is: did stack.sh stop? If it did not, it was a warning.

Step 5: check that it works

DevStack writes a client configuration file for you, /etc/openstack/clouds.yaml, with ten ready-made logins (ordinary, admin, alternative project, read-only users and system-level users). You pick one with OS_CLOUD:

$ grep -E "^  [a-z-]+:" /etc/openstack/clouds.yaml
  devstack:
  devstack-admin:
  devstack-admin-demo:
  devstack-alt:
  devstack-alt-member:
  devstack-alt-reader:
  devstack-reader:
  devstack-system-admin:
  devstack-system-member:
  devstack-system-reader:
$ sed -n 1,10p /etc/openstack/clouds.yaml
clouds:
  devstack:
    auth:
      auth_url: http://10.0.2.15/identity
      password: practice
      project_domain_id: default
      project_name: demo
      user_domain_id: default
      username: demo
    region_name: RegionOne
$ OS_CLOUD=devstack-admin openstack token issue -c project_id -c user_id
+------------+----------------------------------+
| Field      | Value                            |
+------------+----------------------------------+
| project_id | 2b380a2be5ad443e92cabeae7d9909b4 |
| user_id    | 8da6246745c84127a7d28b5c9810a1f6 |
+------------+----------------------------------+
$ OS_CLOUD=devstack-admin openstack service list -c Name -c Type
+-------------+----------------+
| Name        | Type           |
+-------------+----------------+
| nova        | compute        |
| nova_legacy | compute_legacy |
| keystone    | identity       |
| cinder      | block-storage  |
| glance      | image          |
| placement   | placement      |
| neutron     | network        |
+-------------+----------------+
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:18080/dashboard/auth/login/     (on the laptop, through the forwarded port)
200

What the installer did to the machine

Because DevStack "makes substantial changes", it helps to see them. All of this is from the lab machine after the install:

$ grep -c " install " /var/log/dpkg.log
496
$ grep -c "^Commandline" /var/log/apt/history.log
12
$ grep " install " /var/log/dpkg.log | awk "{print \$4}" | cut -d: -f1 | grep -E "^(mysql-server|rabbitmq-server|libvirt-daemon-system|qemu-system-x86|openvswitch-switch|ovn-central|ovn-host|apache2|memcached|python3-dev|targetcli-fb|haproxy)$" | sort -u | tr "\n" " "; echo
apache2 haproxy libvirt-daemon-system memcached mysql-server openvswitch-switch ovn-central ovn-host python3-dev qemu-system-x86 rabbitmq-server targetcli-fb 
$ getent passwd | awk -F: "\$1 ~ /^(stack|rabbitmq|mysql|memcache|haproxy|dnsmasq|libvirt-qemu|libvirt-dnsmasq|epmd)\$/ {print \$1\":\"\$3\":\"\$6}"
stack:1000:/home/stack
dnsmasq:999:/var/lib/misc
memcache:109:/nonexistent
haproxy:110:/var/lib/haproxy
epmd:112:/run/epmd
rabbitmq:113:/var/lib/rabbitmq
mysql:114:/nonexistent
libvirt-qemu:64055:/var/lib/libvirt
libvirt-dnsmasq:116:/var/lib/libvirt/dnsmasq
$ sudo cat /etc/sudoers.d/50_stack_sh
stack ALL=(root) NOPASSWD:ALL
Defaults:stack secure_path=/sbin:/usr/sbin:/usr/bin:/bin:/usr/local/sbin:/usr/local/bin
Defaults:stack !requiretty
$ sudo du -sh /opt/stack/data /opt/stack/horizon /opt/stack/nova /opt/stack/neutron /opt/stack/cinder /opt/stack/keystone /opt/stack/glance /opt/stack/logs
1005M	/opt/stack/data
234M	/opt/stack/horizon
227M	/opt/stack/nova
148M	/opt/stack/neutron
124M	/opt/stack/cinder
52M	/opt/stack/keystone
32M	/opt/stack/glance
2.5M	/opt/stack/logs
$ df -h / | tail -1
/dev/vda1        58G   13G   46G  22% /
$ ls -ls /opt/stack/data/stack-volumes-lvmdriver-1-backing-file
24708 -rw-r--r-- 1 stack stack 10737418240 Oct  8 21:19 /opt/stack/data/stack-volumes-lvmdriver-1-backing-file
$ sudo losetup -a
/dev/loop0: [64769]:787286 (/opt/stack/data/stack-volumes-lvmdriver-1-backing-file)
$ sudo vgs --noheadings -o vg_name,vg_size,vg_free
  stack-volumes-lvmdriver-1 <10.00g 484.00m
$ ip -br addr show br-ex
br-ex            UNKNOWN        172.24.4.1/24 2001:db8::2/64 fe80::a827:48ff:fe5
$ ip -br addr show virbr0
virbr0           DOWN           192.168.122.1/24 
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
$ systemctl is-enabled devstack@n-api devstack@keystone mysql rabbitmq-server apache2 libvirtd | tr "\n" " "; echo
enabled enabled enabled enabled enabled enabled 

In plain words:

Rebooting: what comes back by itself, and what does not

Sooner or later you will restart the machine, and the honest answer to "will my cloud survive?" is "mostly". This was tested, twice.

First reboot, with the install exactly as stack.sh left it:

$ sudo systemctl reboot        (then, after the machine is back)
$ uptime -p; systemctl --failed --no-legend | awk '{print $2}' | tr "\n" " "
up 0 minutes
(no failed units listed)
$ systemctl is-active mysql rabbitmq-server apache2 libvirtd ovn-controller ovn-northd
active active active active active active
$ systemctl list-units "devstack@*" --no-legend | awk '{print $1, $4}'
(all 20 devstack@ units listed, every one "running": c-api c-sch c-vol dstat etcd g-api keystone n-api-meta n-api n-cond-cell1 n-cpu n-novnc-cell1 n-sch n-super-cond neutron-api neutron-ovn-maintenance-worker neutron-periodic-workers neutron-rpc-server placement-api q-ovn-agent)
$ sudo losetup -a
/dev/loop0: [64769]:787286 (/opt/stack/data/stack-volumes-lvmdriver-1-backing-file)
$ openstack compute service list -c Binary -c State -c Status
+----------------+---------+-------+
| Binary         | Status  | State |
+----------------+---------+-------+
| nova-scheduler | enabled | up    |
| nova-conductor | enabled | up    |
| nova-conductor | enabled | up    |
| nova-compute   | enabled | up    |
+----------------+---------+-------+
$ curl -s -o /dev/null -w "horizon %{http_code}\n" http://127.0.0.1/dashboard/auth/login/
horizon 200

Good news first. Every service came back on its own, with nothing failed: the database, the queue, Apache, libvirt, OVN, all 20 DevStack units, Nova reporting its services up, and even the loop-backed Cinder volume group was re-attached. Horizon answered. Now the two things that did not come back:

$ ip -br addr show br-ex
br-ex            DOWN           
$ ping -c2 -W1 172.24.4.42            (the router's address on the public network)
PING 172.24.4.42 (172.24.4.42) 56(84) bytes of data.

--- 172.24.4.42 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1000ms
$ openstack server list -c Name -c Status
| flow-test | ACTIVE |
$ sudo virsh list --all
 Id   Name                State
------------------------------------
 -    instance-00000001   shut off
$ openstack server start flow-test
ConflictException: 409: Client Error for url: http://10.0.2.15/compute/v2.1/servers/5207c12a-d569-4456-8f1c-c998a0aa8735/action, Cannot 'start' instance 5207c12a-d569-4456-8f1c-c998a0aa8735 while it is in vm_state active
  1. The br-ex bridge is DOWN with no address. The outside network no longer works: the router's address on it, 172.24.4.42, cannot be reached, which would also break every floating IP you create. The address was added by hand by stack.sh, and nothing writes it down.
  2. The instance is shut off, but Nova still says ACTIVE. The hypervisor's guest was gone with the reboot, and Nova's records did not notice yet. openstack server start is refused with 409 Conflict, because as far as Nova knows the server is already running.

The first fix is one command each (a quick repair; it lasts until the next reboot):

$ sudo ip link set br-ex up; sudo ip addr add 172.24.4.1/24 dev br-ex; sudo ip -6 addr add 2001:db8::2/64 dev br-ex
$ ip -br addr show br-ex
br-ex            UNKNOWN        172.24.4.1/24 2001:db8::2/64 fe80::a827:48ff:fe5
$ ping -c2 -W1 172.24.4.42
64 bytes from 172.24.4.42: icmp_seq=1 ttl=254 time=2.94 ms
64 bytes from 172.24.4.42: icmp_seq=2 ttl=254 time=0.794 ms

--- 172.24.4.42 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms

The router's address answers once br-ex has its addresses back. For the instance, the way out of the "ACTIVE but not running" state is a hard reboot, which tells Nova to rebuild the guest from its own records. The lab simulated the state by destroying the guest in libvirt, and then:

$ virsh destroy instance-00000001        (simulate what a reboot leaves behind)
Domain 'instance-00000001' destroyed
nova: ACTIVE | libvirt: shut off
$ openstack server start flow-test
ConflictException: 409: Client Error for url: http://10.0.2.15/compute/v2.1/servers/5207c12a-.../action, Cannot 'start' instance 5207c12a-... while it i...
nova: ACTIVE | libvirt: shut off
$ openstack server reboot --hard flow-test
nova: HARD_REBOOT | libvirt: shut off
nova: HARD_REBOOT | libvirt: running
(a few seconds later)
nova: ACTIVE | libvirt: running

start is refused, reboot --hard works: Nova shows HARD_REBOOT and then ACTIVE with libvirt running. (Left alone, Nova would also notice by itself, through a periodic power-state check, but not at once, and the 409 messages you get while it catches up are confusing.)

Neither of those is satisfying, so the permanent fixes. One is a Nova setting, resume_guests_state_on_host_boot = true, in the [DEFAULT] section of /etc/nova/nova-cpu.conf: when the compute service starts after a boot, it restarts the guests that were running. The other is a small systemd unit that gives br-ex its addresses at boot, after Open vSwitch is up:

$ sudo sed -i '/^\[DEFAULT\]/a resume_guests_state_on_host_boot = true' /etc/nova/nova-cpu.conf
$ grep -n resume_guests_state_on_host_boot /etc/nova/nova-cpu.conf
3:resume_guests_state_on_host_boot = true
$ cat /etc/systemd/system/br-ex-address.service
[Unit]
Description=Give the DevStack public bridge br-ex its addresses
After=openvswitch-switch.service ovn-controller.service
Wants=openvswitch-switch.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sh -c 'for i in $(seq 1 60); do ip link show br-ex >/dev/null 2>&1 && exit 0; sleep 2; done; exit 1'
ExecStart=/bin/sh -c 'ip link set br-ex up && ip addr replace 172.24.4.1/24 dev br-ex && ip -6 addr replace 2001:db8::2/64 dev br-ex'

[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload; sudo systemctl enable br-ex-address.service
Created symlink /etc/systemd/system/multi-user.target.wants/br-ex-address.service → /etc/systemd/system/br-ex-address.service.
$ systemd-analyze verify /etc/systemd/system/br-ex-address.service && echo "unit verifies OK"
unit verifies OK

And then the second reboot, with nothing done by hand afterwards:

$ sudo systemctl reboot        (nothing done by hand afterwards)
$ uptime -p; systemctl --failed --no-legend | awk '{print $2}'
up 1 minute
$ systemctl is-active br-ex-address.service; ip -br addr show br-ex
active
br-ex            UNKNOWN        172.24.4.1/24 2001:db8::2/64 fe80::a827:48ff:fe5
$ ping -c2 -W1 172.24.4.42 | tail -3
--- 172.24.4.42 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1034ms
rtt min/avg/max/mdev = 0.696/0.701/0.706/0.005 ms
$ openstack server list -c Name -c Status -f value; sudo virsh list --all | sed -n 3,4p
flow-test ACTIVE
 1    instance-00000001   running
$ journalctl -u devstack@n-cpu -b | grep -i "Rebooting instance after"
INFO nova.compute.manager [None req-7e7eecad-... None None] [instance: 5207c12a-d569-4456-8f1c-c998a0aa8735] Rebooting instance after nova-compute restart.

This time the machine came back with br-ex addressed, the router answering pings, flow-test ACTIVE and running (Nova's log says "Rebooting instance after nova-compute restart"), and no failed unit. This recipe is tied to this DevStack version and this networking layout (OVN with the default 172.24.4.0/24 public network): if you changed the floating range or the public network's settings in local.conf, use your own addresses in the unit.

💡 Why does DevStack not do this itself? It is a tool for building a development cloud, and the usual workflow is to rebuild, not to reboot: the documentation's own scripts for stopping and starting over are unstack.sh (stops what stack.sh started, leaving MySQL and RabbitMQ running unless you pass -a) and clean.sh (removes the data files and leaves the packages alone). Re-running stack.sh after either is the normal way to start fresh. Those were not run in this lab, because the cloud is needed for the next pages.

Where things are

/opt/stack/logs/stack.sh.log           the installer's complete record
/opt/stack/NAME/                       the source of each project (git checkouts)
/opt/stack/data/venv/                  the shared Python virtual environment
/etc/nova  /etc/neutron  /etc/keystone  /etc/glance  /etc/cinder     each service's configuration
/etc/openstack/clouds.yaml             the ready-made client logins (OS_CLOUD=...)
~/devstack/openrc                      the older way: source it for one user and project
~/devstack/local.conf                  what you asked for
journalctl -u 'devstack@*'             every service's log, in the journal
systemctl status devstack@n-api        one service (n-api, n-cpu, g-api, keystone, ...)

The services are ordinary systemd units, named devstack@NAME, so systemctl restart and journalctl -u work the way you already know. (Restarting a single one was not run here.) Editing a config file under /etc takes effect after restarting that service, which is how the resume_guests_state_on_host_boot change above was made.

When it goes wrong

Where to go from here

DevStack is the learning install. For a cloud that has to keep running, the usual choices are packaged, container-based or Kubernetes-based installers: Kolla-Ansible (OpenStack in containers, installed with Ansible), Canonical's Sunbeam or the OpenStack-Helm project, among others. They solve what DevStack leaves out: high availability, upgrades without rebuilding, and TLS between services. None of them was run for this series.

What this page did not cover

The cheat sheet

sudo useradd -s /bin/bash -d /opt/stack -m stack         the stack user
echo "stack ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/stack
git clone https://opendev.org/openstack/devstack -b stable/2026.1
vi ~/devstack/local.conf                                 passwords (letters and digits), HOST_IP, LIBVIRT_TYPE
cd ~/devstack && ./stack.sh                              the install (about 18 minutes here)
tail -f /opt/stack/logs/stack.sh.log                     watch it
OS_CLOUD=devstack-admin openstack service list           is the cloud alive
journalctl -u 'devstack@*' -f                            watch every service
systemctl list-units 'devstack@*'                        the twenty units
./unstack.sh   ./clean.sh                                stop / wipe (documented, not run here)
resume_guests_state_on_host_boot = true                  in /etc/nova/nova-cpu.conf, [DEFAULT]
br-ex-address.service                                    restores 172.24.4.1/24 on br-ex at boot
openstack server reboot --hard NAME                      the cure for "ACTIVE but not running"

← OpenStack in One Page: What It Is and How the Services Fit Together · ↑ All tutorials · Your First OpenStack Instance: Key Pair, Security Group, Floating IP and the Mistakes Everybody Makes →