Install a public demo server on Ubuntu¶
This guide starts with a minimal Ubuntu 24.04 server reachable as a named administrator over SSH. On hpd, that account is assela. It must have passwordless, non-interactive sudo; direct root SSH is neither required nor assumed. The guide prepares Dokku, PostgreSQL/PostGIS, wildcard DNS, automatic HTTPS, and private demo storage. The lifecycle command remains in the administrator workstation checkout.
Use the upstream installation instructions for software whose commands and repositories change:
Do not install a demo-manager helper, registry database, provisioner role, server release mirror, or delegated demo-manager SSH user. Use the server's existing administrator account and its deliberately configured passwordless sudo access.
1. Configure the administrator's sudo access¶
The lifecycle tool needs unrestricted root-equivalent access because it creates databases and roles, manages Dokku applications, and writes protected storage. This simplified design deliberately uses unrestricted passwordless sudo. It does not install a narrow single-command sudo rule like the older delegated-helper design.
Log in as the named administrator. On hpd, use ssh assela@hpd. If the account can currently use sudo with a password, create a dedicated rule with:
sudo visudo -f /etc/sudoers.d/lmstool-demo-admin
Add exactly this line for hpd:
assela ALL=(ALL:ALL) NOPASSWD: ALL
Save the file, then validate both its syntax and non-interactive behavior:
sudo visudo -cf /etc/sudoers.d/lmstool-demo-admin
sudo -k
sudo -n true
Both commands must succeed. If the account cannot initially run sudo, use the hosting provider's console or recovery mechanism to create the rule. Do not weaken another production or shared server to satisfy this guide. Anyone controlling this SSH account can become root, so protect its private key and restrict server access accordingly.
2. Install and verify the platform¶
Install Dokku 0.38.22 or later, PostgreSQL with PostGIS, Git, and the Dokku Let's Encrypt plugin using the linked upstream instructions. Version alone is not sufficient; the exact storage behavior must also match the verified model below.
On the server:
sudo dokku version
sudo dokku letsencrypt:help
sudo dokku storage:help
sudo dokku storage:list-entries --format json
psql --version
Keep ports 22, 80, and 443 reachable. Configure the firewall through the server provider or the Ubuntu method used for the rest of the host.
3. Configure wildcard DNS¶
Choose a dedicated suffix such as demo.example.org. At the authoritative DNS provider create:
- an
Arecord fordemo.example.orgpointing to the server IPv4 address; - an
Arecord for*.demo.example.orgpointing to the same address; and - equivalent
AAAArecords only when IPv6 is correctly routed to the server.
Verify from the workstation:
dig +short demo.example.org
dig +short demo-tutorial.demo.example.org
Both names must resolve to the intended server before creating demos. Dokku's Let's Encrypt plugin obtains and renews an individual certificate for each public demo application. A manually distributed wildcard private key is not required.
4. Create the empty PostGIS template¶
Create a database used only as the empty source for disposable demo databases. Run on the server:
sudo -u postgres createdb demo_template
sudo -u postgres psql --dbname demo_template \
--command 'CREATE EXTENSION IF NOT EXISTS postgis;'
sudo -u postgres psql --dbname demo_template \
--command 'CREATE EXTENSION IF NOT EXISTS postgis_topology;'
sudo -u postgres psql --dbname demo_template \
--command 'SELECT PostGIS_Full_Version();'
Apply the reviewed LMSTool migrations needed by the template using the project's normal database preparation procedure. Leave it empty of demo parcels, workflow records, and users. Do not allow applications to connect to or modify this database.
5. Create private storage and configuration¶
On the server:
sudo install -d -o root -g root -m 0711 /var/lib/lmstool/demo-storage
sudo install -d -o root -g root -m 0755 /etc/lmstool
sudo install -o root -g root -m 0600 /dev/null /etc/lmstool/demo-manager.env
The first command creates the required parent for all per-demo storage. Run it also when upgrading a server prepared for the older manager, even if the old manager used a different storage location. Verify it before continuing:
sudo stat -c '%U:%G %a %n' /var/lib/lmstool/demo-storage
The expected ownership and mode are root:root 711. The lifecycle command
creates only the child directory for an individual demo; it deliberately does
not create or repair this configured parent automatically.
Edit /etc/lmstool/demo-manager.env as root:
sudoedit /etc/lmstool/demo-manager.env
Set the following values, replacing the example domain, bridge address, and email:
DEMO_DATABASE_HOST=172.17.0.1
DEMO_DATABASE_PORT=5432
DEMO_DATABASE_TEMPLATE=demo_template
DEMO_DOMAIN_SUFFIX=demo.example.org
DEMO_LOCK_FILE=/run/lock/lmstool-demo-manager.lock
DEMO_STORAGE_ROOT=/var/lib/lmstool/demo-storage
DEMO_LETSENCRYPT_EMAIL=admin@example.org
DEMO_OPERATION_TIMEOUT_SECONDS=900
DEMO_SUBPROCESS_TIMEOUT_SECONDS=120
This file contains no database password, application secret, or deploy key. Keep it root-owned, mode 0600, and never make it a symlink.
Production safety does not depend on a configurable production application name. The manager accepts only the strictly validated demo_ namespace, rejects protected names, and checks ownership markers before deletion. Keep production applications and resources outside that namespace.
If this server was prepared for the older installed-helper design, replace its configuration rather than merging the block above into it. In particular, remove all obsolete registry, provisioner, release-repository, executable-path, delegated-user, email-backend, and external-write keys. The simplified file must contain only keys shown in the block above. Configuration parsing rejects unknown or obsolete keys instead of silently ignoring them. The old registry and provisioner URLs may contain passwords, so do not print or copy their values into terminal output or support reports.
DEMO_DATABASE_HOST must be the host address reachable from Dokku containers,
not 127.0.0.1. Neither the gateway nor subnet is a fixed LMSTool value.
172.17.0.1 and 172.17.0.0/16 are common Docker defaults, but Docker daemon
configuration, address pools, or an existing network allocation can change
them. Always obtain the actual values from the server:
sudo docker network inspect bridge \
--format '{{(index .IPAM.Config 0).Gateway}} {{(index .IPAM.Config 0).Subnet}}'
sudo -u postgres psql -Atc 'SHOW config_file'
sudo -u postgres psql -Atc 'SHOW hba_file'
sudo -u postgres psql -Atc 'SHOW listen_addresses'
sudo pg_lsclusters
Use the reported gateway as DEMO_DATABASE_HOST. Open the path reported by
SHOW config_file with sudoedit. Set listen_addresses to include both
loopback and that gateway. For the common gateway shown above:
listen_addresses = 'localhost,172.17.0.1'
Open the path reported by SHOW hba_file with sudoedit. Add a rule permitting
SCRAM authentication only from the reported bridge subnet to deterministically
named demo databases by their matching demo login roles. For example, use this
only when Docker actually reported 172.17.0.0/16:
host "/^demo_[a-z0-9_]+$" "/^demo_[a-z0-9_]+_user$" 172.17.0.0/16 scram-sha-256
Replace the CIDR when the reported subnet differs. Validate the complete HBA
file before restarting. In PostgreSQL HBA syntax, the leading / selects a
regular expression; it is not paired with a closing slash. The double quotes
above delimit each complete field, and $ is the end-of-name anchor.
sudo -u postgres psql -x -c \
"SELECT line_number, type, database, user_name, address, netmask, auth_method, error
FROM pg_hba_file_rules
ORDER BY line_number;"
The new bridge rule must appear and its error field must be empty.
pg_hba_file_rules separates CIDR notation into two fields: for
172.17.0.0/16, expect address to contain 172.17.0.0 and netmask to
contain 255.255.0.0. A query comparing address directly with
172.17.0.0/16 will therefore return no rows even when the rule is valid.
Because a
change to listen_addresses requires a restart, restart the cluster shown by
pg_lsclusters. For Ubuntu 24.04's usual PostgreSQL 16 main cluster:
sudo pg_ctlcluster 16 main restart
Replace 16 main when pg_lsclusters reports another version or cluster name.
Then verify the active listener and ensure a bridge rule remains loaded:
sudo -u postgres psql -Atc 'SHOW listen_addresses'
sudo ss -ltn | grep ':5432'
sudo -u postgres psql -x -c \
"SELECT line_number, database, user_name, address, netmask, auth_method, error
FROM pg_hba_file_rules
WHERE address IS NOT NULL
ORDER BY line_number;"
If UFW is active with incoming traffic denied, explicitly allow only the
reported bridge subnet to reach PostgreSQL through docker0. For the example
gateway and subnet above:
sudo ufw allow in on docker0 \
from 172.17.0.0/16 \
to 172.17.0.1 \
port 5432 proto tcp
sudo ufw status numbered
Replace both addresses with the values reported by Docker. Do not use the
broader ufw allow 5432/tcp, which could expose PostgreSQL publicly. Verify
that ufw status numbered shows a rule limited to docker0, the reported
subnet, the reported gateway, TCP, and port 5432.
Before continuing, test basic TCP access from a disposable container on the same Docker bridge without transmitting a database credential. The command may download the standard Python image when it is not already present:
sudo docker run --rm \
--network bridge \
--entrypoint python \
python:3.12-slim \
-c "import socket; socket.create_connection(('172.17.0.1', 5432), 5); print('PostgreSQL port reachable')"
Replace the gateway in the test when Docker reported another value. Do not
continue until it prints PostgreSQL port reachable. A timeout normally
indicates firewall filtering; Connection refused normally means PostgreSQL is
not listening on that address. Recheck the UFW rule, listen_addresses, and the
active listener before retrying. Do not expose port 5432 publicly. Firewall
access must remain limited to the local Docker bridge and any separately
approved administration path.
6. Prove named-storage behavior¶
The disposable test on Dokku 0.38.22 established the supported storage model. Before using a different Dokku/storage-driver combination, repeat the fixture in specs/016-simplify-demo-management/quickstart.md. It must prove:
storage:create NAME PATHregisters a named entry at the dedicated root;- separate named entries backed by the
mediaandsecretsdirectories attach during deploy and run; - the secrets attachment is reported read-only;
- an entrypoint/run process can read the canary secret; and
- an in-container write to the secrets attachment fails.
One entry with --volume-subpath did not isolate subdirectories with the tested docker-local driver. Docker also rejected --volume-options noexec,nosuid. Therefore, use two entries and the read-only flag for the secrets entry. If any check fails, stop. Do not replace named mounts with the deprecated HOST:CONTAINER form.
7. Register the workstation deployment key¶
Select a workstation SSH public key. The private key stays on the workstation. From the workstation:
cat ~/.ssh/id_ed25519.pub | \
ssh assela@demo-server.example.org \
sudo -n dokku ssh-keys:add lmstool-demo-workstation
ssh assela@demo-server.example.org \
sudo -n dokku ssh-keys:list --format json
Lifecycle coordination uses assela@demo-server.example.org and invokes each privileged server command through sudo -n. Git deployment uses the restricted destination dokku@demo-server.example.org:APP. Registering the key does not create another general-purpose server login.
8. Verify readiness¶
From the workstation:
ssh -T assela@demo-server.example.org sudo -n true
ssh -T dokku@demo-server.example.org help
The second command may display Dokku's restricted command help rather than a shell. Continue with Managed demo environments.