Deployment guide

PyWPS

Overview

PyWPS is a Python implementation of the OGC Web Processing Service (WPS) standard, the open specification for publishing geoprocessing operations as an HTTP API. A client calls GetCapabilities to list what the server can do, DescribeProcess to see an operation's inputs and outputs, and Execute to run it.

PyWPS is a library, not a turnkey server: it needs a WSGI server and registered processes before it answers any request. This image supplies both. It runs a WPS 1.0.0 service, served by gunicorn behind nginx, with four working example processes already registered. You get a callable geoprocessing endpoint as soon as the VM boots, and you can add your own processes alongside the examples.

The service is public by design and read/compute-only. GetCapabilities, DescribeProcess and Execute are meant to be reached over the internet, as with any OGC WPS endpoint. There is no admin console or application account, and the only way onto the VM itself is the SSH key you supply at deploy time.

What you get:

  • PyWPS 4.7.0 in a dedicated Python 3.12 virtual environment at /opt/pywps/venv, served by gunicorn (pywps.service) and fronted by nginx on :80
  • Four demo processes, each pure in-memory Python with no file uploads, no shell commands and no outbound network calls: say_hello (string input and output), ultimate_question (a zero-input process), boundingbox (a BoundingBoxData passthrough) and buffer (a 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 and nginx.service as enabled, active systemd units
  • 24/7 SecureAnyCloud support

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.

Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting size for the bundled demo processes and light custom process development. Choose a larger size if the processes you add do heavier in-memory computation.

Configure the network security group (NSG) to allow inbound 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 SecureAnyCloud, and select Create.

On the Basics tab, 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 select Review + create and Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:pywps:default:latest
az vm create \
  --resource-group <your-rg> \
  --name pywps \
  --image sac1647283583153:pywps:default:latest \
  --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

Connect to the VM over SSH:

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

Check that both services are active and see which ports they listen on:

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 [::]:*

Step 5 - Read your instance info

PyWPS has no application accounts, so there is no credential to retrieve. On first boot, the VM writes a short instance info note that contains your WPS endpoint URL. Read it as root:

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 answers GetCapabilities with an XML document that describes the service and every registered process. This call is public and unauthenticated by design:

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. The ows:Identifier entries under ProcessOfferings list all four bundled processes:

<wps:Capabilities service="WPS" version="1.0.0" ...> <ows:ServiceIdentification> <ows:Title>sac 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>

Step 7 - Describe a process, then Execute it

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

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 that contains wps:ProcessSucceeded and the 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>

Step 8 - Execute a real geometry operation

The buffer process computes a planar buffer around an input geometry. You supply the geometry as Well-Known Text (WKT), and the process returns the buffered geometry, also as WKT. It uses Shapely/GEOS entirely in memory. 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 wps:LiteralData in the response is a buffered POLYGON, computed 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>

Step 9 - Try the other bundled processes

The ultimate_question process needs no input. The boundingbox process 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. To add your own, follow these steps.

First, add a new file under /opt/pywps/app/processes/, using the same shape as the bundled say_hello and buffer processes. Run sudo cat /opt/pywps/app/processes/buffer_wkt.py to see a complete example.

Next, register an instance of your process in the processes list in /opt/pywps/app/wsgi.py. 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. The PyWPS documentation has the full reference for Process, LiteralInput/Output, ComplexInput/Output and BoundingBoxInput/Output.

If your process accepts a file upload (ComplexInput) or an href reference to a remote resource, review the PyWPS allowedinputpaths setting and the network exposure it creates before you make the endpoint public. The processes bundled with this image deliberately avoid file and reference inputs, so the endpoint can be public by default.

Maintenance

  • No credentials. PyWPS has no application accounts, and none are generated. If you register processes that should not be publicly callable, add access control yourself, for example nginx Basic Auth or an IP allowlist in your NSG.
  • Processes. Add or edit modules under /opt/pywps/app/processes/ and register them in /opt/pywps/app/wsgi.py. Then run sudo systemctl restart pywps to pick up the changes.
  • Configuration. The PyWPS server configuration is /etc/pywps/pywps.cfg. It covers 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 terminating TLS on :443, for example with certbot and your own domain. Restrict inbound access in your NSG 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. 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 stays enabled, so the OS keeps receiving security updates automatically.

Support

SecureAnyCloud provides 24/7 expert support for this image. For questions or help with your deployment, contact sales@secureanycloud.com.

Deploy on Azure

Launch PyWPS on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure