Gc
Applications Azure

GC2 on Ubuntu 24.04 on Azure User Guide

| Product: GC2 on Ubuntu 24.04 LTS on Azure

Overview

GC2 is an open source platform for managing geospatial data, publishing map services and building geospatial applications. It ties PostGIS, MapServer, QGIS Server, MapCache and GDAL together behind a single web administration application, so you can import spatial data, style it, publish it as standards-compliant OGC services and query it over a REST and SQL API without hand editing a single configuration file. GC2 is part of the OSGeo Community Project GC2/Vidi.

The cloudimg image deploys GC2 2026.6.6 with PostgreSQL 16, PostGIS 3 and pgRouting running natively on the host, and the GC2 core application (Apache, PHP 8.4, MapServer with PHP MapScript, QGIS Server, MapCache and GDAL) in a single container built from the upstream project's own published runtime image at the pinned upstream release tag. Every credential is generated uniquely on the first boot of every VM. Backed by 24/7 cloudimg support.

cloudimg is not affiliated with MapCentia ApS or with the OSGeo GC2/Vidi project.

What is included:

  • GC2 2026.6.6, shipped unmodified at the upstream release tag, built with the project's own grunt production and dashboard build pipelines
  • PostgreSQL 16 with PostGIS 3.4, postgis_raster, pgcrypto and pgRouting, installed from the Ubuntu archive and listening on loopback only
  • MapServer 8 with PHP MapScript, QGIS Server, MapCache as an Apache module, and GDAL 3.9 with 145 raster and vector drivers
  • Valkey 8.1 (BSD 3-Clause) as the session store and application cache GC2 requires
  • The GC2 system databases (template_geocloud, mapcentia, gc2scheduler) created and migrated, ready for your first account
  • A per-VM administration account, PostgreSQL superuser password and GC2 database role password, all generated on first boot and recorded in a root-only file
  • gc2.service, gc2-firstboot.service, postgresql.service and docker.service as systemd units, enabled and active
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet and subnet in the target region. Standard_B2ms (2 vCPU / 8 GiB RAM) is the recommended size; the appliance is tested down to Standard_B2s (2 vCPU / 4 GiB) and idles at roughly 1 GiB, but map rendering and raster import benefit from the extra memory. NSG inbound: allow 22/tcp from your management network and 80/tcp from wherever your users are. GC2 serves plain HTTP on port 80; for production, terminate TLS in front of it with your own domain.

The VM needs outbound HTTPS. The GC2 sign-in page loads its front end libraries from public CDNs, and the map view loads the OpenStreetMap base layer, so a VM with no egress will show an unstyled sign-in page and a blank base map.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for GC2 by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size (Standard_B2ms or larger); under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create -> Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name gc2 \
  --image <marketplace-image-urn> \
  --size Standard_B2ms \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name gc2 --port 80 --priority 1010

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

GC2 runs as a Docker Compose stack managed by gc2.service, alongside a native PostgreSQL cluster. On the first boot of a new VM, gc2-firstboot.service generates every credential, registers this VM's own address with GC2's sign-in flow, and starts the stack. Confirm everything is up:

systemctl is-active docker.service postgresql.service gc2.service

Each reports active. Confirm the first boot completed:

test -f /var/lib/cloudimg/gc2-firstboot.done && echo "first boot: complete"

The GC2 services active, the Compose stack running, and the pinned GC2 and PostGIS versions

Step 5 - Confirm the stack and the pinned versions

The stack is two containers: the GC2 core (Apache, PHP, MapServer, QGIS Server, MapCache, GDAL) and Valkey.

cd /opt/gc2 && sudo docker compose ps --format '{{.Name}}  {{.Status}}'

Confirm the image really carries the pinned upstream release, and that MapServer and PostGIS are wired up:

sudo docker exec gc2core git -C /var/www/geocloud2 describe --tags --exact-match HEAD
sudo docker exec gc2core /usr/lib/cgi-bin/mapserv.fcgi -v
sudo -u postgres psql -d template_geocloud -tAc 'SELECT postgis_version()'

Valkey and PostgreSQL are reachable on loopback only. Nothing but SSH and HTTP listens on a routable address:

ss -ltnH | awk '{print $4}' | sort -u

Step 6 - Retrieve your credentials

Every secret on this VM was generated on its first boot. They are written to a root-only file:

sudo cat /root/gc2-credentials.txt

You get the GC2 administration account (GC2_ADMIN_USER and GC2_ADMIN_PASSWORD), the PostgreSQL superuser password, and the password of the gc2 database role that GC2 itself connects with. Store them somewhere safe. There are no default logins on this image - every published upstream default is rejected:

The per-VM credentials file and every published upstream default credential being rejected

Step 7 - Sign in to GC2

Browse to http://<vm-public-ip>/. GC2 redirects you to its dashboard, where Login starts the sign in. Enter your GC2_ADMIN_USER first and submit it with the arrow button - GC2 looks up which databases that account owns and then enables the password field - and then enter GC2_ADMIN_PASSWORD.

You land on the dashboard for your account. It lists the schemas in your own PostGIS database (a fresh VM has just public) and lets you create sub users and configurations.

The GC2 dashboard after signing in, listing the schemas in the account's PostGIS database

From here, http://<vm-public-ip>/admin/<your-account>/public opens the GC2 administration application - the map view, the database view, the editing workflow and the log.

Step 8 - Import your first spatial dataset

GC2 imports through GDAL, so it reads what you already have: ESRI Shapefile, MapInfo TAB and MIF, GeoJSON, GML, KML, File Geodatabase, GeoTIFF, CSV and more. You can drag a file into the administration application, or load it from the command line with ogr2ogr.

This example loads the public domain Natural Earth 1:110m country polygons. Fetch the file:

curl -fsSL -o /tmp/countries.geojson \
  https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson
ls -l /tmp/countries.geojson

Copy it into the GC2 container and import it into your account's PostGIS database. Read your database role password out of the credentials file first:

GC2PW='<POSTGRES_GC2_PASSWORD>'
GC2USER='<GC2_ADMIN_USER>'
sudo docker cp /tmp/countries.geojson gc2core:/tmp/countries.geojson
sudo docker exec gc2core ogr2ogr -f PostgreSQL \
  "PG:host=127.0.0.1 port=5432 user=gc2 password=${GC2PW} dbname=${GC2USER}" \
  /tmp/countries.geojson \
  -nln countries -nlt MULTIPOLYGON -overwrite \
  -lco GEOMETRY_NAME=the_geom -lco SCHEMA=public -lco FID=gid -lco PRECISION=NO \
  -t_srs EPSG:4326 --config OGR_PG_ENABLE_METADATA NO \
  -select "name,name_long,iso_a3,continent,region_un,subregion,pop_est,gdp_md,economy,income_grp"
sudo docker exec gc2core rm -f /tmp/countries.geojson

Always import with an explicit geometry type (-nlt MULTIPOLYGON, -nlt POINT, -nlt LINESTRING). GC2 reads the geometry type out of PostGIS to decide how MapServer should draw the layer, so a column created as the generic GEOMETRY type is drawn as points and a polygon layer renders as nothing at all.

Confirm the import:

GC2PW='<POSTGRES_GC2_PASSWORD>'
GC2USER='<GC2_ADMIN_USER>'
PGPASSWORD="${GC2PW}" psql -h 127.0.0.1 -U gc2 -d "${GC2USER}" -c \
  "SELECT f_table_name, f_geometry_column, type, srid FROM geometry_columns WHERE f_table_schema='public'"
PGPASSWORD="${GC2PW}" psql -h 127.0.0.1 -U gc2 -d "${GC2USER}" -tAc \
  "SELECT count(*)||' features' FROM public.countries"

You get one MULTIPOLYGON layer in EPSG:4326 with 177 features.

The imported PostGIS table, its geometry column and a PostGIS query returning GeoJSON

Step 9 - Publish the layer

A table becomes a published layer once it belongs to a layer group. In the administration application's Database tab, select the relation and give it a Title and a Group; or do the same over GC2's own API. Sign in to get a session cookie, then set the group:

GC2USER='<GC2_ADMIN_USER>'
GC2PASS='<GC2_ADMIN_PASSWORD>'
IP=$(hostname -I | awk '{print $1}')
JAR=$(mktemp)
curl -sS -c "$JAR" -o /dev/null -X POST -H 'Content-Type: application/json' \
  -d "{\"user\":\"${GC2USER}\",\"password\":\"${GC2PASS}\"}" \
  "http://${IP}/api/v2/session/start"
curl -sS -b "$JAR" -X PUT -H 'Content-Type: application/json' \
  -d '{"data":{"_key_":"public.countries.the_geom","layergroup":"Reference data","f_table_title":"World countries","authentication":"Read/write"}}' \
  "http://${IP}/controllers/layer/records"
echo
curl -sS -b "$JAR" "http://${IP}/controllers/mapfile/${GC2USER}/public" | head -c 200
echo
rm -f "$JAR"

The last call regenerates the MapServer and WFS map files for the schema. Open Map in the administration application, expand the Reference data group and switch the layer on. Give it at least one class from the Classes panel or the Class wizard - MapServer draws nothing for a layer with no class - and the layer appears over the base map.

The published PostGIS layer rendered by MapServer over the base map in the GC2 map view

Everything about how the layer looks is configured from the browser: classes and their expressions, symbols and labels, the tile cache, the legend, and QGIS QML styles.

The layer's classes, expressions and styling configured entirely from the browser

The Database tab shows the same layer as data: its structure, its rows and its attributes, editable in place.

The imported features and their attributes in the GC2 database view

Step 10 - Consume the layer as OGC services and over the API

Publishing a layer publishes it as OGC services at the same time. Ask the server what it offers, fetch a rendered map image, and pull features back as GeoJSON:

GC2USER='<GC2_ADMIN_USER>'
GC2PASS='<GC2_ADMIN_PASSWORD>'
IP=$(hostname -I | awk '{print $1}')
JAR=$(mktemp)
curl -sS -c "$JAR" -o /dev/null -X POST -H 'Content-Type: application/json' \
  -d "{\"user\":\"${GC2USER}\",\"password\":\"${GC2PASS}\"}" \
  "http://${IP}/api/v2/session/start"

curl -sS -b "$JAR" "http://${IP}/ows/${GC2USER}/public?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities" \
  | grep -oE '<CRS>[^<]+</CRS>' | head -4

curl -sS -b "$JAR" -o /tmp/map.png -w 'GetMap: HTTP %{http_code}  %{content_type}  %{size_download} bytes\n' \
  "http://${IP}/wms/${GC2USER}/public?LAYERS=public.countries&TRANSPARENT=TRUE&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=&FORMAT=image/png&SRS=EPSG:4326&BBOX=-180,-90,180,90&WIDTH=800&HEIGHT=400"

curl -sS -b "$JAR" "http://${IP}/ows/${GC2USER}/public?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=public.countries&COUNT=1&OUTPUTFORMAT=geojson" \
  | head -c 240
echo
rm -f "$JAR" /tmp/map.png

GetMap returns a real PNG and GetFeature returns GeoJSON. Point QGIS Desktop, OpenLayers, Leaflet or any OGC client at http://<vm-public-ip>/ows/<your-account>/public and the layer is there. Cached tiles are served from MapCache under /mapcache/.

WMS GetCapabilities, a rendered GetMap image and a WFS GetFeature response, all from the same layer

GC2 also exposes the database itself as a web service. Send SQL, choose your output format, and get GeoJSON, CSV or Excel back:

GC2USER='<GC2_ADMIN_USER>'
GC2PASS='<GC2_ADMIN_PASSWORD>'
IP=$(hostname -I | awk '{print $1}')
JAR=$(mktemp)
curl -sS -c "$JAR" -o /dev/null -X POST -H 'Content-Type: application/json' \
  -d "{\"user\":\"${GC2USER}\",\"password\":\"${GC2PASS}\"}" \
  "http://${IP}/api/v2/session/start"
curl -sS -b "$JAR" -G "http://${IP}/api/v2/sql/${GC2USER}" \
  --data-urlencode "q=SELECT name, iso_a3, pop_est FROM public.countries ORDER BY pop_est DESC LIMIT 5" \
  --data-urlencode "format=json" | head -c 400
echo
rm -f "$JAR"

Swap format=json for format=geojson, format=csv or format=excel to feed dashboards, reports and spreadsheets from the same query.

Step 11 - Connect to PostGIS from your workstation

PostgreSQL listens on loopback only, so it is never exposed to the internet. Reach it from QGIS Desktop, psql or any client by tunnelling it over SSH from your workstation:

ssh -L 5432:127.0.0.1:5432 azureuser@<vm-public-ip>

Then connect to localhost:5432, database <your-account>, user gc2, with the POSTGRES_GC2_PASSWORD from the credentials file. GC2's built-in WFS-T also lets you edit layers directly from QGIS Desktop over HTTP.

Using your own domain or HTTPS

GC2 signs in through its own OAuth2 authorisation code flow, and it only accepts sign-ins whose address is registered against the built-in gc2-cli client. The image registers this VM's public and private addresses on first boot, so http://<vm-public-ip>/ works immediately. When you put a domain name or a TLS terminating proxy in front of GC2, register that address too:

GC2USER='<GC2_ADMIN_USER>'
sudo -u postgres psql -d "${GC2USER}" -tAc \
  "SELECT redirect_uri FROM settings.clients WHERE id='gc2-cli'"

Add your own origin (for example https://gis.example.com, with no trailing slash) to that JSON array and update the row. Sign-ins from an unregistered address are refused with [invalid_client].

Adding Elasticsearch

GC2 can index PostGIS layers into Elasticsearch for full text and faceted search. This image deliberately does not ship Elasticsearch: it does not fit alongside the rest of the stack on the smallest supported VM, and upstream treats it as an optional feature. To add it, run Elasticsearch on a larger VM or as a managed service, then set esIndexingInGui to true and add your Elasticsearch host in /opt/gc2/conf/App.php and restart with sudo systemctl restart gc2.service.

About ECW rasters

GDAL in this image is built without the proprietary ERDAS ECW JPEG 2000 SDK, whose licence does not cover server side applications. Every other GDAL format is unchanged. If you need ECW, licence the SDK from Hexagon directly.

Maintenance

  • Users: add sub users and configurations from the dashboard; manage privileges layer by layer from Database -> Privileges in the administration application.
  • Backups: back up your PostGIS databases with pg_dump, and snapshot /var/lib/gc2 for map files, uploads, QGIS projects and the tile cache.
  • Data: PostgreSQL data lives in /var/lib/postgresql; GC2's map files, uploads and tile cache live in /var/lib/gc2.
  • Stack control: manage the application with sudo systemctl restart gc2.service, or with docker compose in /opt/gc2.
  • Configuration: application settings live in /opt/gc2/conf/App.php; database connection settings live in /opt/gc2/gc2.env.
  • TLS: GC2 serves plain HTTP on port 80; front it with TLS and your own domain before production use, and register that address as described above.
  • Security patches: unattended-upgrades remains enabled, so the operating system, PostgreSQL, PostGIS and pgRouting continue to receive security updates automatically.
  • Licences: the licence notice for GC2 and every bundled component ships at /usr/share/doc/cloudimg/third-party-licences/.

Support

cloudimg provides 24/7 expert support for this image. Contact support@cloudimg.co.uk.