Applications Azure

PyWPS on Ubuntu 24.04 on Azure User Guide

| Product: PyWPS on Ubuntu 24.04 LTS on Azure

Overview

PyWPS is a Python implementation of the OGC Web Processing Service (WPS) standard, the open specification for publishing geoprocessing operations as a standard HTTP API: GetCapabilities lists what a server can do, DescribeProcess describes a specific operation's inputs and outputs, and Execute runs it. On its own, PyWPS is a library, not a turnkey server: it needs a WSGI server and a set of registered processes before it answers any request. This image provides exactly that, a running WPS 1.0.0 service with four working example processes already registered, so you get a genuine, callable geoprocessing endpoint the moment the VM boots, then add your own processes alongside them. PyWPS is served by gunicorn behind nginx. This is a public-by-design, read/compute-only service: GetCapabilities, DescribeProcess and Execute are meant to be reached over the open internet the way any OGC WPS endpoint is, there is no admin console or application account of any kind, and the only way onto the VM itself is the SSH key you supply at deploy time. Backed by 24/7 cloudimg support.

What is included:

  • PyWPS 4.7.0 running in a dedicated Python 3.12 virtual environment at /opt/pywps/venv, served by gunicorn (pywps.service) and fronted by nginx on :80
  • Four hand-picked demo processes, each pure in-memory Python with no file uploads, no shell commands and no outbound network calls: say_hello (string input/output), ultimate_question (a zero-input process), boundingbox (a BoundingBoxData passthrough) and buffer (a real Shapely/GEOS geometry buffer over a WKT input)
  • A minimal, explicit surface: nginx proxies only /wps; every other path returns 404 and there is no admin or output-browsing endpoint
  • pywps.service + nginx.service as systemd units, enabled and active
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point for the bundled demo processes and light custom process development; size up if the processes you add do heavier in-memory computation. NSG inbound: allow 22/tcp from your management network and 80/tcp for the WPS API, which is intended to be publicly reachable.

Step 1 - Deploy from the Azure Marketplace

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

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name pywps \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --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 pywps --port 80 --priority 1010

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

systemctl is-active pywps.service nginx.service
sudo ss -tln | grep -E '127.0.0.1.*:5000 |:80 '

Both services report active. PyWPS (gunicorn) is bound to the loopback address 127.0.0.1:5000 only; nginx fronts it on port 80 and is the only path in from the network.

active
active
LISTEN 0      2048       127.0.0.1:5000      0.0.0.0:*
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      511             [::]:80           [::]:*

pywps.service and nginx.service both active, gunicorn listening on loopback 127.0.0.1:5000 only, and nginx on public port 80

Step 5 - Read your instance info

There is no credential to retrieve, PyWPS has no application accounts, but first boot does write a short instance info note with your VM's actual WPS endpoint URL:

sudo cat /root/pywps-info.txt
PYWPS_URL=http://<your-vm-public-ip>/wps
GetCapabilities:  http://<your-vm-public-ip>/wps?service=WPS&version=1.0.0&request=GetCapabilities
DescribeProcess:  http://<your-vm-public-ip>/wps?service=WPS&version=1.0.0&request=DescribeProcess&identifier=say_hello
Execute (demo):   http://<your-vm-public-ip>/wps?service=WPS&version=1.0.0&request=Execute&identifier=say_hello&datainputs=name=World
Bundled processes: say_hello, ultimate_question, boundingbox, buffer

Step 6 - Call GetCapabilities

Every OGC WPS server must answer GetCapabilities with an XML document describing the service and every registered process. This is meant to be a public, unauthenticated call:

curl -s 'http://127.0.0.1/wps?service=WPS&version=1.0.0&request=GetCapabilities'

The response is a wps:Capabilities document. The ows:ServiceIdentification block names the service, and ows:Identifier entries under ProcessOfferings list all four bundled processes:

<wps:Capabilities service="WPS" version="1.0.0" ...>
    <ows:ServiceIdentification>
        <ows:Title>cloudimg PyWPS Server</ows:Title>
        ...
    <ows:Identifier>say_hello</ows:Identifier>
    <ows:Identifier>ultimate_question</ows:Identifier>
    <ows:Identifier>boundingbox</ows:Identifier>
    <ows:Identifier>buffer</ows:Identifier>

The GetCapabilities response: a valid wps:Capabilities XML document listing all 4 bundled processes - say_hello, ultimate_question, boundingbox and buffer

Step 7 - Describe a process, then Execute it

DescribeProcess returns the inputs and outputs a specific process expects; Execute runs it. Both accept simple GET requests for processes that, like every process in this image, take only literal (non-file) inputs:

curl -s 'http://127.0.0.1/wps?service=WPS&version=1.0.0&request=DescribeProcess&identifier=say_hello'

curl -s -G 'http://127.0.0.1/wps' \
  --data-urlencode 'service=WPS' --data-urlencode 'version=1.0.0' \
  --data-urlencode 'request=Execute' --data-urlencode 'identifier=say_hello' \
  --data-urlencode 'datainputs=name=World'

Execute returns a wps:ExecuteResponse document with wps:ProcessSucceeded and the real output data inline:

<wps:ExecuteResponse ...>
    <wps:Status creationTime="...">
        <wps:ProcessSucceeded>PyWPS Process Say Hello finished</wps:ProcessSucceeded>
    </wps:Status>
    <wps:ProcessOutputs>
        <wps:Output>
            <ows:Identifier>response</ows:Identifier>
            <wps:Data>
                <wps:LiteralData uom="urn:ogc:def:uom:OGC:1.0:unity" dataType="string">Hello World</wps:LiteralData>
            </wps:Data>
        </wps:Output>
    </wps:ProcessOutputs>
</wps:ExecuteResponse>

DescribeProcess for say_hello followed by a real Execute call, returning wps:ProcessSucceeded and the literal output "Hello World"

Step 8 - Execute a real geometry operation

buffer computes an actual planar buffer around an input geometry, supplied as Well-Known Text (WKT), using Shapely/GEOS entirely in memory, and returns the buffered geometry, also as WKT. The distance input accepts any value from 0 to 10:

curl -s -G 'http://127.0.0.1/wps' \
  --data-urlencode 'service=WPS' --data-urlencode 'version=1.0.0' \
  --data-urlencode 'request=Execute' --data-urlencode 'identifier=buffer' \
  --data-urlencode 'datainputs=wkt=POINT (30 10);distance=2'

The response's wps:LiteralData is a genuine buffered POLYGON, not a placeholder, computed fresh from the WKT point you supplied and the requested distance:

<wps:LiteralData dataType="string">POLYGON ((32.0000000000000000 10.0000000000000000, 31.9903694533443925 9.8039657193408782, ...))</wps:LiteralData>

A real Execute(buffer) call: an input WKT POINT with a buffer distance produces a genuine buffered POLYGON, computed in memory with Shapely/GEOS

Step 9 - Try the other bundled processes

ultimate_question needs no input at all, and boundingbox demonstrates the WPS BoundingBoxData binding by returning the same bounding box you send it:

curl -s -G 'http://127.0.0.1/wps' \
  --data-urlencode 'service=WPS' --data-urlencode 'version=1.0.0' \
  --data-urlencode 'request=Execute' --data-urlencode 'identifier=ultimate_question'

curl -s -G 'http://127.0.0.1/wps' \
  --data-urlencode 'service=WPS' --data-urlencode 'version=1.0.0' \
  --data-urlencode 'request=Execute' --data-urlencode 'identifier=boundingbox' \
  --data-urlencode 'datainputs=bboxin=-4,50,2,56'

Step 10 - Add your own process

Every process in this image is a plain pywps.Process subclass. Add a new file under /opt/pywps/app/processes/, following the same shape as the bundled say_hello/buffer processes (sudo cat /opt/pywps/app/processes/buffer_wkt.py to see a complete real example), register an instance of it in /opt/pywps/app/wsgi.py's processes list, then restart the service:

sudo systemctl restart pywps
for i in $(seq 1 20); do
  curl -s -o /dev/null -w '%{http_code}' 'http://127.0.0.1/wps?service=WPS&version=1.0.0&request=GetCapabilities' | grep -q 200 && break
  sleep 1
done
curl -s 'http://127.0.0.1/wps?service=WPS&version=1.0.0&request=GetCapabilities' | grep -oE '<ows:Identifier>[a-zA-Z0-9_]+</ows:Identifier>'

Your new process's identifier now appears alongside the four bundled ones. See the PyWPS documentation for the full Process, LiteralInput/Output, ComplexInput/Output and BoundingBoxInput/Output reference. If your own process needs to accept a file upload (ComplexInput) or an href reference to a remote resource, review PyWPS's allowedinputpaths and network-exposure implications carefully before deploying it publicly - the processes bundled with this image deliberately avoid file/reference inputs entirely so the endpoint can be safely public by default.

Maintenance

  • No credentials: PyWPS has no application accounts and none are generated. Access control, if you need it, is your own responsibility to add (for example nginx Basic Auth or an IP allowlist in your Network Security Group) once you register processes that should not be publicly callable.
  • Processes: add or edit modules under /opt/pywps/app/processes/, register them in /opt/pywps/app/wsgi.py, then sudo systemctl restart pywps to pick up changes.
  • Configuration: the PyWPS server configuration is /etc/pywps/pywps.cfg, covering request size limits (maxrequestsize), the async job pool (parallelprocesses, maxprocesses) and the service metadata shown in GetCapabilities.
  • Restrict access: PyWPS serves plain HTTP on port 80. For production, consider TLS (for example certbot with your own domain) terminating on :443, and restrict inbound access in your Network Security Group if the endpoint should not be fully public.
  • Loopback binding: gunicorn is bound to 127.0.0.1:5000 in pywps.service, so nginx is the only path in. Keep it that way - do not change the gunicorn bind address to a public one.
  • Logs: PyWPS application logs are under /var/log/pywps/; gunicorn's own logs are in the systemd journal (sudo journalctl -u pywps.service).
  • Security patches: unattended-upgrades remains enabled so the OS continues to receive security updates automatically.

Support

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