Deployment guide

PostGIS

Overview

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. These cover distance, containment, intersection, buffering and coordinate system transformation.

PostGIS is an extension rather than a separate engine, so spatial data lives alongside your ordinary tables. You query it with the same SQL, transactions, backups and tooling you already use. This image ships PostGIS 3.6 on PostgreSQL 18 on Ubuntu 24.04 LTS, ready to answer spatial queries from the first boot of every deployed virtual machine.

What you get:

  • PostgreSQL 18 and PostGIS installed unmodified from the official PGDG repository, so the database stays patchable through the normal apt path for the life of the release
  • A ready-made spatial database with the PostGIS extensions already created
  • A dedicated 32 GiB data disk for your database files, separate from the operating system disk
  • Unique credentials generated on the first boot of your virtual machine, with no default login
  • A listener on the loopback address only, with a documented way to open it to your own virtual network
  • 24/7/365 SecureAnyCloud support

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. A build-time role, password hash and write-ahead log then ride into every customer virtual machine. This image does not. The build cluster is destroyed before capture. A first-boot service then 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 this, 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 how to open the database to your own virtual network when you need to.

1. Deploy the virtual machine

Deploy from the Azure Marketplace listing, or from the Marketplace image URN with the Azure CLI. The recommended size is Standard_B2s or larger.

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:postgis-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-resource-group \
  --name my-postgis-vm \
  --image sac1647283583153:postgis-ubuntu-24-04:default:latest on Ubuntu 24.04 LTS by sac" \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

The image declares a dedicated 32 GiB data disk. Azure provisions it automatically and mounts it 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 that only root can read. The superuser password and the application password are different, so leaking the application role cannot grant superuser.

sudo cat /root/postgis-credentials.txt

The output looks like this, with password values unique to your virtual machine:

# PostGIS on Ubuntu 24.04 LTS by sac
# 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, 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, because it is 534 km away, well outside the 300 km circle. The cities_geom_idx GiST index 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 the database, 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 SecureAnyCloud configuration file:

sudo nano /etc/postgresql/18/main/conf.d/10-sac.conf

Change listen_addresses to include the virtual network address. Alternatively, set it to * 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, and replace 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, and 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. It generates a superuser password and a separate application password unique to that machine, and writes 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 mechanism 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. The image adds that origin to the allowed origins for unattended upgrades, 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 that includes roles, use pg_dumpall as the postgres superuser. Copy backups off the virtual machine, for example to Azure Blob Storage. 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 that service 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 is on the virtual machine. It records the exact package versions and the checksums of the licence files as shipped:

cat /usr/share/doc/sac/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 ships on the image:

cat /usr/share/doc/sac/POSTGIS-GPL-2-WRITTEN-OFFER.txt

Support

SecureAnyCloud provides 24/7/365 expert technical support for this image. Contact sales@secureanycloud.com.

PostgreSQL is developed by the PostgreSQL Global Development Group, and PostGIS by the PostGIS Project Steering Committee and contributors. SecureAnyCloud provides this image and is not affiliated with or endorsed by either project. Additional charges apply for build, maintenance and 24/7 support.

Deploy on Azure

Launch PostGIS on Ubuntu 24.04 LTS by SecureAnyCloud with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure