PostGIS on Ubuntu 24.04 LTS on Azure
This image ships PostGIS 3.6 on PostgreSQL 18 on Ubuntu 24.04 LTS, configured and ready to answer spatial queries from the first boot of every deployed virtual machine. PostgreSQL 18 and PostGIS are installed unmodified from the official PGDG repository, so the database stays patchable through the normal apt path for the life of the release.
PostGIS turns an ordinary PostgreSQL database into a full geographic database: it adds geometry and geography column types, spatial indexing so those columns stay fast at scale, and several hundred spatial SQL functions covering distance, containment, intersection, buffering and coordinate system transformation. Because it is an extension rather than a separate engine, spatial data lives alongside your ordinary tables and you query it with the same SQL, transactions, backups and tooling you already use.
What makes this image different
The image contains no database at all. Most database images ship a database that was created on the build machine, which means a build time role, password hash and write ahead log ride into every customer virtual machine. This image does not: the build cluster is destroyed before capture, and a first boot service creates a pristine cluster on your virtual machine and generates credentials unique to it. There is no default login anywhere in the image, and nothing to inherit.
PostgreSQL refuses to start until that has happened. Two independent mechanisms enforce it, so a failure to generate credentials can never leave a database running without them.
Loopback only by default. A database reachable from the network the moment it boots is a materially larger attack surface than a documented opt in. Section 7 shows exactly how to open it to your own virtual network when you need to.
1. Deploy the virtual machine
Deploy from the Azure Marketplace listing, or from the gallery image with the Azure CLI. The recommended size is Standard_B2s or larger.
az vm create \
--resource-group my-resource-group \
--name my-postgis-vm \
--image "PostGIS on Ubuntu 24.04 LTS by cloudimg" \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
The image declares a dedicated 32 GiB data disk. It is provisioned automatically and mounted at /var/lib/postgresql, so your database files live on their own volume rather than on the operating system disk.
2. Connect to the virtual machine
ssh azureuser@<vm-ip>
The login banner confirms what is installed and where to find your credentials.

3. Retrieve your credentials
First boot generates two independent passwords and writes them to a file readable only by root. The superuser password and the application password are different, so leaking the application role cannot grant superuser.
sudo cat /root/postgis-credentials.txt
Expected output, with the password values unique to your virtual machine:
# PostGIS on Ubuntu 24.04 LTS by cloudimg
# Per-VM credentials generated on first boot -- unique to THIS virtual machine.
# Nothing in the captured image contained these values; the image ships with no
# database at all. Keep this file secure and delete it once you have stored the
# passwords in your own secret manager.
POSTGRES_SUPERUSER_PASSWORD=<POSTGRES_SUPERUSER_PASSWORD>
GISADMIN_PASSWORD=<GISADMIN_PASSWORD>
Store both passwords in your own secret manager, then delete the file.
4. Confirm the service and the cluster
systemctl is-active postgresql@18-main.service
active
pg_lsclusters
Ver Cluster Port Status Owner Data directory Log file
18 main 5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log
The data directory is on the dedicated data disk, exactly as intended.
5. Connect to the ready made spatial database
The image provides a database called gisdb, owned by the gisadmin role, with the PostGIS extensions already created. Connect to it:
PGPASSWORD='<GISADMIN_PASSWORD>' psql -h 127.0.0.1 -U gisadmin -d gisdb -c "SELECT PostGIS_Full_Version();"
POSTGIS="3.6.4 94d984b" [EXTENSION] PGSQL="180" GEOS="3.12.1-CAPI-1.18.1" PROJ="9.4.0 NETWORK_ENABLED=OFF URL_ENDPOINT=https://cdn.proj.org USER_WRITABLE_DIRECTORY=/tmp/proj DATABASE_PATH=/usr/share/proj/proj.db" (compiled against PROJ 9.4.0) LIBXML="2.9.14" LIBJSON="0.17" LIBPROTOBUF="1.4.1" WAGYU="0.5.0 (Internal)" TOPOLOGY
Confirm which extensions are installed:
PGPASSWORD='<GISADMIN_PASSWORD>' psql -h 127.0.0.1 -U gisadmin -d gisdb -c "\dx"
Name | Version | Default version | Schema | Description
------------------+---------+-----------------+------------+------------------------------------------------------------
plpgsql | 1.0 | 1.0 | pg_catalog | PL/pgSQL procedural language
postgis | 3.6.4 | 3.6.4 | public | PostGIS geometry and geography spatial types and functions
postgis_topology | 3.6.4 | 3.6.4 | topology | PostGIS topology spatial types and functions

6. Load spatial data and query it
Create a table with a geometry column, add a spatial index, and insert some real points. This example stores four UK cities in WGS 84, which is the coordinate system used by GPS and by most published datasets.
PGPASSWORD='<GISADMIN_PASSWORD>' psql -h 127.0.0.1 -U gisadmin -d gisdb <<'SQL'
CREATE TABLE cities (
id serial PRIMARY KEY,
name text NOT NULL,
geom geometry(Point, 4326) NOT NULL
);
CREATE INDEX cities_geom_idx ON cities USING GIST (geom);
INSERT INTO cities (name, geom) VALUES
('London', ST_SetSRID(ST_MakePoint(-0.1276, 51.5072), 4326)),
('Manchester', ST_SetSRID(ST_MakePoint(-2.2426, 53.4808), 4326)),
('Edinburgh', ST_SetSRID(ST_MakePoint(-3.1883, 55.9533), 4326)),
('Cardiff', ST_SetSRID(ST_MakePoint(-3.1791, 51.4816), 4326));
SQL
Now ask a real spatial question: how far is each city from London, in kilometres? Casting to geography makes PostGIS compute true distance on the spheroid rather than in degrees.
PGPASSWORD='<GISADMIN_PASSWORD>' psql -h 127.0.0.1 -U gisadmin -d gisdb -c "
SELECT c.name,
round((ST_Distance(c.geom::geography,
(SELECT geom::geography FROM cities WHERE name = 'London')) / 1000)::numeric, 1) AS km_from_london
FROM cities c
ORDER BY km_from_london;"
name | km_from_london
------------+----------------
London | 0.0
Cardiff | 211.9
Manchester | 262.4
Edinburgh | 534.4
(4 rows)
Ask a containment question. Build a 300 km circle around London and find which cities fall inside it:
PGPASSWORD='<GISADMIN_PASSWORD>' psql -h 127.0.0.1 -U gisadmin -d gisdb -c "
SELECT name
FROM cities
WHERE ST_DWithin(geom::geography,
(SELECT geom::geography FROM cities WHERE name = 'London'),
300000)
ORDER BY name;"
name
------------
Cardiff
London
Manchester
(3 rows)
Edinburgh is correctly excluded: it is 534 km away, well outside the 300 km circle. The cities_geom_idx GiST index is what keeps this kind of query fast as the table grows:
PGPASSWORD='<GISADMIN_PASSWORD>' psql -h 127.0.0.1 -U gisadmin -d gisdb -c "SELECT indexname, indexdef FROM pg_indexes WHERE tablename='cities';"
indexname | indexdef
-----------------+-------------------------------------------------------------------
cities_pkey | CREATE UNIQUE INDEX cities_pkey ON public.cities USING btree (id)
cities_geom_idx | CREATE INDEX cities_geom_idx ON public.cities USING gist (geom)
(2 rows)

7. Enable remote access (optional)
PostgreSQL listens on the loopback address only. This is a deliberate choice for a database rather than a packaging default, and the image asserts it at build time. To let other machines in your virtual network reach it, for example a GeoServer or QGIS host, make three scoped changes.
Never open port 5432 to 0.0.0.0/0. Always scope both the host rule and the network security group to your own network range.
1. Listen on the virtual network interface. Edit the cloudimg configuration file:
sudo nano /etc/postgresql/18/main/conf.d/10-cloudimg.conf
Change listen_addresses to include the virtual network address, or to * if you want it to listen on all interfaces and rely on the host rule plus the network security group to restrict access:
listen_addresses = '*'
2. Add one host rule scoped to your own network range. Append a single line to pg_hba.conf, replacing the range with your own virtual network address space:
echo "host all all 10.0.0.0/16 scram-sha-256" | sudo tee -a /etc/postgresql/18/main/pg_hba.conf
Use the most specific range you can. Keep scram-sha-256; never use trust or md5.
3. Restart PostgreSQL and open the port to that range only:
sudo systemctl restart postgresql@18-main.service
az network nsg rule create \
--resource-group my-resource-group \
--nsg-name my-postgis-vm-nsg \
--name allow-postgres-from-vnet \
--priority 1000 \
--source-address-prefixes 10.0.0.0/16 \
--destination-port-ranges 5432 \
--access Allow --protocol Tcp
8. Security posture
No default login exists. The image contains no database, no role and no password hash. On the first boot of every virtual machine a one shot service creates a pristine cluster and generates a superuser password and a separate application password unique to that machine, writing them to /root/postgis-credentials.txt with mode 0600.
PostgreSQL fails closed. Two independent mechanisms stop the database running before credentials exist. The service unit requires postgis-firstboot.service, so a failed first boot propagates. Independently, an ExecStartPre preflight re-checks on every start, not just the first, that the sentinel and a real credentials file are present. Either one alone refuses the start.
Authentication. Only scram-sha-256 is configured. There are no trust entries, no md5 entries and no rule covering 0.0.0.0/0.
Patching. Nothing is pinned with apt-mark hold, which would block security updates silently and indefinitely. PostgreSQL and PostGIS come from the PGDG archive, and the image adds that origin to the unattended upgrades allowed origins so database security updates are applied automatically alongside Ubuntu's own.

9. Backups
Take a logical backup of the spatial database with pg_dump. The custom format is compressed and restores with pg_restore:
PGPASSWORD='<GISADMIN_PASSWORD>' pg_dump -h 127.0.0.1 -U gisadmin -d gisdb -Fc -f /var/lib/postgresql/gisdb-backup.dump
ls -lh /var/lib/postgresql/gisdb-backup.dump
For a full cluster backup including roles, use pg_dumpall as the postgres superuser. Copy backups off the virtual machine, for example to Azure Blob Storage, and take Azure snapshots of the data disk for point in time recovery of the whole volume.
10. Logs and troubleshooting
The PostgreSQL log:
sudo tail -n 20 /var/log/postgresql/postgresql-18-main.log
If PostgreSQL is not running, check the first boot service first. It is the most likely cause, because the database deliberately refuses to start until it has completed:
systemctl status postgis-firstboot.service --no-pager
sudo journalctl -u postgis-firstboot.service --no-pager | tail -n 20
11. Licence
PostgreSQL is distributed under the PostgreSQL Licence, a permissive licence. PostGIS is distributed under the GNU General Public License, version 2 or later.
Both are installed unmodified from the official PGDG archive. The licence audit for this image, recording the exact package versions and the checksums of the licence files as shipped, is on the virtual machine:
cat /usr/share/doc/cloudimg/LICENCE-AUDIT.txt
Because PostGIS is licensed under the GPL version 2, you are entitled to its complete corresponding source code. There are two ways to obtain it. The first is immediate and free: the binary is the unmodified PGDG package, and this virtual machine is already configured to use that archive, so the source for the exact binary installed is one command away.
apt-get source postgresql-18-postgis-3
The second is a written offer, valid for three years, which is shipped on the image:
cat /usr/share/doc/cloudimg/POSTGIS-GPL-2-WRITTEN-OFFER.txt
Support
cloudimg provides 24/7/365 expert technical support for this image. Contact support@cloudimg.co.uk. PostgreSQL is developed by the PostgreSQL Global Development Group and PostGIS by the PostGIS Project Steering Committee and contributors; this image is provided by cloudimg and is not affiliated with or endorsed by either project. Additional charges apply for build, maintenance and 24/7 support.