[{"data":1,"prerenderedAt":1628},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fpublish-layers-with-qgis-server-pyqgis":3},{"id":4,"title":5,"body":6,"description":1617,"extension":1618,"meta":1619,"navigation":246,"path":1624,"seo":1625,"stem":1626,"__hash__":1627},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fpublish-layers-with-qgis-server-pyqgis\u002Findex.md","Publish Layers with QGIS Server from PyQGIS",{"type":7,"value":8,"toc":1603},"minimark",[9,13,17,26,187,192,209,213,411,428,438,442,585,606,610,797,822,826,902,998,1011,1015,1154,1175,1179,1182,1421,1449,1452,1456,1466,1470,1518,1522,1525,1529,1539,1548,1558,1564,1568,1599],[10,11,5],"h1",{"id":12},"publish-layers-with-qgis-server-from-pyqgis",[14,15,16],"p",{},"QGIS Server publishes a QGIS project. Everything it serves — the layer list, the styling, the extents, the metadata in the capabilities document — comes from that project file, which means preparing a project for publication is a scripting job like any other. Doing it in Python rather than through the project properties dialog is what lets a hundred projects be generated and republished on a schedule.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","Headless QGIS & Server Automation",". It covers the project entries that drive the service metadata, restricting which layers are published, per-service layer lists, and verifying the result before it reaches a server.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,100,107,112,116,119,122,126,132,136,139,142,151,156,162,166,170,174,182],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 320","img","The path from a QGIS project file to a served map: server reads the project, its settings decide the capabilities document, and clients request maps and features","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"The project file is the configuration",[40,41,42],"desc",{},"QGIS Server reads a project file on each request. The project's server settings decide the service title and contact metadata, which layers appear, and which are published as web feature services. Clients then read the capabilities document and request rendered maps or raw features.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","320","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"srvArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","No server-side config file — the project is the config",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"24","52","176","92","#eef7f4","#0f766e","stroke-width:2.5",[68,86,90],{"x":87,"y":88,"style":89,"fill":83,"textAnchor":74},"112","80","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","project.qgs",[68,92,95],{"x":87,"y":93,"style":94,"fill":66,"textAnchor":74},"104","text-anchor:middle;font-size:10px;font-family:sans-serif","layers · styles · extents",[68,97,99],{"x":87,"y":98,"style":94,"fill":66,"textAnchor":74},"126","server entries",[101,102],"line",{"x1":103,"y1":104,"x2":105,"y2":104,"stroke":66,"style":106},"200","98","226","stroke-width:2;marker-end:url(#srvArrow)",[44,108],{"x":109,"y":79,"width":80,"height":81,"rx":58,"fill":110,"stroke":111,"style":84},"234","#eff3ff","#2563eb",[68,113,115],{"x":114,"y":88,"style":89,"fill":111,"textAnchor":74},"322","QGIS Server",[68,117,118],{"x":114,"y":93,"style":94,"fill":66,"textAnchor":74},"reads it per request",[68,120,121],{"x":114,"y":98,"style":94,"fill":66,"textAnchor":74},"caches parsed projects",[101,123],{"x1":124,"y1":104,"x2":125,"y2":104,"stroke":66,"style":106},"410","436",[44,127],{"x":128,"y":79,"width":129,"height":81,"rx":58,"fill":130,"stroke":131,"style":84},"444","292","#edf8e9","#15803d",[68,133,135],{"x":134,"y":88,"style":89,"fill":131,"textAnchor":74},"590","capabilities document",[68,137,138],{"x":134,"y":93,"style":94,"fill":66,"textAnchor":74},"title, contact, layer list, extents",[68,140,141],{"x":134,"y":98,"style":94,"fill":66,"textAnchor":74},"what every client reads first",[44,143],{"x":78,"y":144,"width":145,"height":146,"rx":147,"fill":148,"stroke":149,"style":150},"170","712","82","10","#fffdf7","#59645f","stroke-width:2",[68,152,155],{"x":70,"y":153,"style":154,"fill":73,"textAnchor":74},"196","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","the three lists that matter",[68,157,161],{"x":158,"y":159,"style":160,"fill":66},"70","224","font-size:10.5px;font-family:sans-serif","WMSRestrictedLayers — hidden",[68,163,165],{"x":164,"y":159,"style":160,"fill":66},"330","WFSLayers — feature access",[68,167,169],{"x":168,"y":159,"style":160,"fill":66},"560","WFSTLayers — editable",[68,171,173],{"x":70,"y":172,"style":94,"fill":149,"textAnchor":74},"244","each holds layer ids, not layer names",[44,175],{"x":176,"y":177,"width":178,"height":179,"rx":58,"fill":180,"stroke":181,"style":150},"120","272","520","34","#fdf2e2","#b45309",[68,183,186],{"x":70,"y":184,"style":185,"fill":66,"textAnchor":74},"294","text-anchor:middle;font-size:10.5px;font-family:sans-serif","a layer left out of WFSLayers is still visible as an image, just not downloadable",[188,189,191],"h2",{"id":190},"prerequisites","Prerequisites",[193,194,195,203,206],"ul",{},[196,197,198,202],"li",{},[199,200,201],"strong",{},"QGIS 3.34 LTR"," or newer for authoring; QGIS Server of a matching version for serving.",[196,204,205],{},"A project whose layers use paths the server can reach — absolute paths, or relative to the project.",[196,207,208],{},"Credentials for any database layers stored in a way the server can use, not in a personal profile.",[188,210,212],{"id":211},"set-the-service-metadata","Set the service metadata",[214,215,220],"pre",{"className":216,"code":217,"language":218,"meta":219,"style":219},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nproject = QgsProject.instance()\nproject.read(\"\u002Fdata\u002Fprojects\u002Futilities.qgs\")\n\nproject.writeEntry(\"WMSServiceTitle\", \"\u002F\", \"City Utilities\")\nproject.writeEntry(\"WMSServiceAbstract\", \"\u002F\",\n                   \"Water, drainage and lighting assets, updated nightly.\")\nproject.writeEntry(\"WMSKeywordList\", \"\u002F\", [\"utilities\", \"water\", \"lighting\"])\nproject.writeEntry(\"WMSContactOrganization\", \"\u002F\", \"City Council GIS\")\nproject.writeEntry(\"WMSContactMail\", \"\u002F\", \"gis@example.gov\")\nproject.writeEntry(\"WMSOnlineResource\", \"\u002F\", \"https:\u002F\u002Fmaps.example.gov\u002Fows\")\n","python","",[221,222,223,241,248,260,273,278,300,315,323,354,373,392],"code",{"__ignoreMap":219},[224,225,227,231,235,238],"span",{"class":101,"line":226},1,[224,228,230],{"class":229},"snl16","from",[224,232,234],{"class":233},"s95oV"," qgis.core ",[224,236,237],{"class":229},"import",[224,239,240],{"class":233}," QgsProject\n",[224,242,244],{"class":101,"line":243},2,[224,245,247],{"emptyLinePlaceholder":246},true,"\n",[224,249,251,254,257],{"class":101,"line":250},3,[224,252,253],{"class":233},"project ",[224,255,256],{"class":229},"=",[224,258,259],{"class":233}," QgsProject.instance()\n",[224,261,263,266,270],{"class":101,"line":262},4,[224,264,265],{"class":233},"project.read(",[224,267,269],{"class":268},"sU2Wk","\"\u002Fdata\u002Fprojects\u002Futilities.qgs\"",[224,271,272],{"class":233},")\n",[224,274,276],{"class":101,"line":275},5,[224,277,247],{"emptyLinePlaceholder":246},[224,279,281,284,287,290,293,295,298],{"class":101,"line":280},6,[224,282,283],{"class":233},"project.writeEntry(",[224,285,286],{"class":268},"\"WMSServiceTitle\"",[224,288,289],{"class":233},", ",[224,291,292],{"class":268},"\"\u002F\"",[224,294,289],{"class":233},[224,296,297],{"class":268},"\"City Utilities\"",[224,299,272],{"class":233},[224,301,303,305,308,310,312],{"class":101,"line":302},7,[224,304,283],{"class":233},[224,306,307],{"class":268},"\"WMSServiceAbstract\"",[224,309,289],{"class":233},[224,311,292],{"class":268},[224,313,314],{"class":233},",\n",[224,316,318,321],{"class":101,"line":317},8,[224,319,320],{"class":268},"                   \"Water, drainage and lighting assets, updated nightly.\"",[224,322,272],{"class":233},[224,324,326,328,331,333,335,338,341,343,346,348,351],{"class":101,"line":325},9,[224,327,283],{"class":233},[224,329,330],{"class":268},"\"WMSKeywordList\"",[224,332,289],{"class":233},[224,334,292],{"class":268},[224,336,337],{"class":233},", [",[224,339,340],{"class":268},"\"utilities\"",[224,342,289],{"class":233},[224,344,345],{"class":268},"\"water\"",[224,347,289],{"class":233},[224,349,350],{"class":268},"\"lighting\"",[224,352,353],{"class":233},"])\n",[224,355,357,359,362,364,366,368,371],{"class":101,"line":356},10,[224,358,283],{"class":233},[224,360,361],{"class":268},"\"WMSContactOrganization\"",[224,363,289],{"class":233},[224,365,292],{"class":268},[224,367,289],{"class":233},[224,369,370],{"class":268},"\"City Council GIS\"",[224,372,272],{"class":233},[224,374,376,378,381,383,385,387,390],{"class":101,"line":375},11,[224,377,283],{"class":233},[224,379,380],{"class":268},"\"WMSContactMail\"",[224,382,289],{"class":233},[224,384,292],{"class":268},[224,386,289],{"class":233},[224,388,389],{"class":268},"\"gis@example.gov\"",[224,391,272],{"class":233},[224,393,395,397,400,402,404,406,409],{"class":101,"line":394},12,[224,396,283],{"class":233},[224,398,399],{"class":268},"\"WMSOnlineResource\"",[224,401,289],{"class":233},[224,403,292],{"class":268},[224,405,289],{"class":233},[224,407,408],{"class":268},"\"https:\u002F\u002Fmaps.example.gov\u002Fows\"",[224,410,272],{"class":233},[14,412,413,416,417,420,421,423,424,427],{},[199,414,415],{},"Breakdown:"," ",[221,418,419],{},"writeEntry"," is the generic project-settings API, and the server reads these exact keys — there is no dedicated server object to configure in older releases, which is why the string keys matter. The second argument is the settings path and is ",[221,422,292],{}," for all of these. Values can be strings, booleans, numbers or string lists, and the type must match what the server expects: ",[221,425,426],{},"WMSKeywordList"," takes a list, and passing a comma-joined string produces one keyword containing commas. Everything written here appears verbatim in the capabilities document, which is the first thing any client and any catalogue harvester reads.",[14,429,430,431,434,435,437],{},"From QGIS 3.20 onwards ",[221,432,433],{},"project.serverProperties()"," exposes much of this through typed methods, which is more discoverable; the ",[221,436,419],{}," form above works across every 3.x release, which is why a script that must span versions uses it.",[188,439,441],{"id":440},"turn-the-services-on-and-bound-them","Turn the services on and bound them",[214,443,445],{"className":216,"code":444,"language":218,"meta":219,"style":219},"project.writeEntry(\"WMSServiceCapabilities\", \"\u002F\", True)\nproject.writeEntry(\"WMSPrecision\", \"\u002F\", \"6\")\n\nextent = project.mapLayersByName(\"water_mains\")[0].extent()\nproject.writeEntry(\"WMSExtent\", \"\u002F\", [\n    str(extent.xMinimum()), str(extent.yMinimum()),\n    str(extent.xMaximum()), str(extent.yMaximum()),\n])\n\nproject.writeEntry(\"WMSCrsList\", \"\u002F\", [\"EPSG:27700\", \"EPSG:3857\", \"EPSG:4326\"])\n",[221,446,447,466,484,488,509,523,537,549,553,557],{"__ignoreMap":219},[224,448,449,451,454,456,458,460,464],{"class":101,"line":226},[224,450,283],{"class":233},[224,452,453],{"class":268},"\"WMSServiceCapabilities\"",[224,455,289],{"class":233},[224,457,292],{"class":268},[224,459,289],{"class":233},[224,461,463],{"class":462},"sDLfK","True",[224,465,272],{"class":233},[224,467,468,470,473,475,477,479,482],{"class":101,"line":243},[224,469,283],{"class":233},[224,471,472],{"class":268},"\"WMSPrecision\"",[224,474,289],{"class":233},[224,476,292],{"class":268},[224,478,289],{"class":233},[224,480,481],{"class":268},"\"6\"",[224,483,272],{"class":233},[224,485,486],{"class":101,"line":250},[224,487,247],{"emptyLinePlaceholder":246},[224,489,490,493,495,498,501,504,506],{"class":101,"line":262},[224,491,492],{"class":233},"extent ",[224,494,256],{"class":229},[224,496,497],{"class":233}," project.mapLayersByName(",[224,499,500],{"class":268},"\"water_mains\"",[224,502,503],{"class":233},")[",[224,505,46],{"class":462},[224,507,508],{"class":233},"].extent()\n",[224,510,511,513,516,518,520],{"class":101,"line":275},[224,512,283],{"class":233},[224,514,515],{"class":268},"\"WMSExtent\"",[224,517,289],{"class":233},[224,519,292],{"class":268},[224,521,522],{"class":233},", [\n",[224,524,525,528,531,534],{"class":101,"line":280},[224,526,527],{"class":462},"    str",[224,529,530],{"class":233},"(extent.xMinimum()), ",[224,532,533],{"class":462},"str",[224,535,536],{"class":233},"(extent.yMinimum()),\n",[224,538,539,541,544,546],{"class":101,"line":302},[224,540,527],{"class":462},[224,542,543],{"class":233},"(extent.xMaximum()), ",[224,545,533],{"class":462},[224,547,548],{"class":233},"(extent.yMaximum()),\n",[224,550,551],{"class":101,"line":317},[224,552,353],{"class":233},[224,554,555],{"class":101,"line":325},[224,556,247],{"emptyLinePlaceholder":246},[224,558,559,561,564,566,568,570,573,575,578,580,583],{"class":101,"line":356},[224,560,283],{"class":233},[224,562,563],{"class":268},"\"WMSCrsList\"",[224,565,289],{"class":233},[224,567,292],{"class":268},[224,569,337],{"class":233},[224,571,572],{"class":268},"\"EPSG:27700\"",[224,574,289],{"class":233},[224,576,577],{"class":268},"\"EPSG:3857\"",[224,579,289],{"class":233},[224,581,582],{"class":268},"\"EPSG:4326\"",[224,584,353],{"class":233},[14,586,587,416,589,592,593,596,597,600,601,605],{},[199,588,415],{},[221,590,591],{},"WMSServiceCapabilities"," must be true or the metadata above is ignored and the server advertises defaults. ",[221,594,595],{},"WMSExtent"," is the advertised bounding box, and setting it from a representative layer rather than from the union of everything stops one stray feature at the antimeridian advertising a worldwide extent. ",[221,598,599],{},"WMSCrsList"," restricts which projections the server will reproject into; leaving it unset advertises a very long list that makes the capabilities document large and slow for every client. Web Mercator belongs in that list if the service will be consumed by web maps, as covered in ",[21,602,604],{"href":603},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fadd-xyz-tile-basemap-pyqgis\u002F","adding an XYZ tile basemap",".",[188,607,609],{"id":608},"choose-which-layers-are-published","Choose which layers are published",[214,611,613],{"className":216,"code":612,"language":218,"meta":219,"style":219},"published = {\"water_mains\", \"hydrants\", \"street_lighting\"}\n\nrestricted = []\nwfs_ids = []\nfor layer in project.mapLayers().values():\n    if layer.name() not in published:\n        restricted.append(layer.name())\n    elif layer.type().name == \"VectorLayer\":\n        wfs_ids.append(layer.id())\n\nproject.writeEntry(\"WMSRestrictedLayers\", \"\u002F\", restricted)\nproject.writeEntry(\"WFSLayers\", \"\u002F\", wfs_ids)\nproject.writeEntry(\"WFSTLayers\", \"\u002FUpdate\", [])\nproject.writeEntry(\"WFSTLayers\", \"\u002FInsert\", [])\nproject.writeEntry(\"WFSTLayers\", \"\u002FDelete\", [])\n",[221,614,615,640,644,654,663,677,694,699,716,721,725,739,753,769,783],{"__ignoreMap":219},[224,616,617,620,622,625,627,629,632,634,637],{"class":101,"line":226},[224,618,619],{"class":233},"published ",[224,621,256],{"class":229},[224,623,624],{"class":233}," {",[224,626,500],{"class":268},[224,628,289],{"class":233},[224,630,631],{"class":268},"\"hydrants\"",[224,633,289],{"class":233},[224,635,636],{"class":268},"\"street_lighting\"",[224,638,639],{"class":233},"}\n",[224,641,642],{"class":101,"line":243},[224,643,247],{"emptyLinePlaceholder":246},[224,645,646,649,651],{"class":101,"line":250},[224,647,648],{"class":233},"restricted ",[224,650,256],{"class":229},[224,652,653],{"class":233}," []\n",[224,655,656,659,661],{"class":101,"line":262},[224,657,658],{"class":233},"wfs_ids ",[224,660,256],{"class":229},[224,662,653],{"class":233},[224,664,665,668,671,674],{"class":101,"line":275},[224,666,667],{"class":229},"for",[224,669,670],{"class":233}," layer ",[224,672,673],{"class":229},"in",[224,675,676],{"class":233}," project.mapLayers().values():\n",[224,678,679,682,685,688,691],{"class":101,"line":280},[224,680,681],{"class":229},"    if",[224,683,684],{"class":233}," layer.name() ",[224,686,687],{"class":229},"not",[224,689,690],{"class":229}," in",[224,692,693],{"class":233}," published:\n",[224,695,696],{"class":101,"line":302},[224,697,698],{"class":233},"        restricted.append(layer.name())\n",[224,700,701,704,707,710,713],{"class":101,"line":317},[224,702,703],{"class":229},"    elif",[224,705,706],{"class":233}," layer.type().name ",[224,708,709],{"class":229},"==",[224,711,712],{"class":268}," \"VectorLayer\"",[224,714,715],{"class":233},":\n",[224,717,718],{"class":101,"line":325},[224,719,720],{"class":233},"        wfs_ids.append(layer.id())\n",[224,722,723],{"class":101,"line":356},[224,724,247],{"emptyLinePlaceholder":246},[224,726,727,729,732,734,736],{"class":101,"line":375},[224,728,283],{"class":233},[224,730,731],{"class":268},"\"WMSRestrictedLayers\"",[224,733,289],{"class":233},[224,735,292],{"class":268},[224,737,738],{"class":233},", restricted)\n",[224,740,741,743,746,748,750],{"class":101,"line":394},[224,742,283],{"class":233},[224,744,745],{"class":268},"\"WFSLayers\"",[224,747,289],{"class":233},[224,749,292],{"class":268},[224,751,752],{"class":233},", wfs_ids)\n",[224,754,756,758,761,763,766],{"class":101,"line":755},13,[224,757,283],{"class":233},[224,759,760],{"class":268},"\"WFSTLayers\"",[224,762,289],{"class":233},[224,764,765],{"class":268},"\"\u002FUpdate\"",[224,767,768],{"class":233},", [])\n",[224,770,772,774,776,778,781],{"class":101,"line":771},14,[224,773,283],{"class":233},[224,775,760],{"class":268},[224,777,289],{"class":233},[224,779,780],{"class":268},"\"\u002FInsert\"",[224,782,768],{"class":233},[224,784,786,788,790,792,795],{"class":101,"line":785},15,[224,787,283],{"class":233},[224,789,760],{"class":268},[224,791,289],{"class":233},[224,793,794],{"class":268},"\"\u002FDelete\"",[224,796,768],{"class":233},[14,798,799,801,802,805,806,810,811,805,814,817,818,821],{},[199,800,415],{}," The asymmetry here catches people out: ",[221,803,804],{},"WMSRestrictedLayers"," holds layer ",[807,808,809],"em",{},"names"," while ",[221,812,813],{},"WFSLayers",[807,815,816],{},"ids",". Mixing them produces a project that silently publishes what you meant to hide. Restricting a layer removes it from the capabilities and refuses requests for it — it is a real access control, not just a display hint. The three ",[221,819,820],{},"WFSTLayers"," entries control transactional write access and should be explicitly empty unless you genuinely intend to let clients edit the data over HTTP; an unset entry is not the same as an empty one on every release, and being explicit costs nothing.",[188,823,825],{"id":824},"per-layer-metadata-that-shows-up-in-the-service","Per-layer metadata that shows up in the service",[14,827,828],{},[29,829,832,835,838,841,844,847,852,856,861,865,868,872,875,879,882,886,889,894,898],{"viewBox":830,"role":32,"ariaLabel":831,"xmlns":34},"0 0 760 300","Where each piece of layer metadata appears in a served capabilities document: title, abstract, keywords, attribution and the layer's own extent",[36,833,834],{},"Layer metadata is the service's documentation",[40,836,837],{},"Each layer's short name becomes the identifier clients request. Its title and abstract become the human-readable description in the capabilities document. Attribution appears in clients that display it. All of them come from the layer's metadata in the project rather than from the underlying data.",[44,839],{"x":46,"y":46,"width":47,"height":840,"fill":49},"300",[68,842,843],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Nobody reads your project; everybody reads your capabilities",[44,845],{"x":78,"y":846,"width":145,"height":153,"rx":147,"fill":148,"stroke":83,"style":84},"46",[68,848,851],{"x":79,"y":849,"style":850,"fill":83},"76","font-size:11px;font-weight:bold;font-family:sans-serif","set in Python",[68,853,855],{"x":854,"y":849,"style":850,"fill":83},"420","appears as",[101,857],{"x1":846,"y1":858,"x2":859,"y2":858,"stroke":149,"style":860},"86","714","stroke-width:1.4",[68,862,864],{"x":79,"y":87,"style":863,"fill":66},"font-size:10.5px;font-family:monospace","layer.setShortName()",[68,866,867],{"x":854,"y":87,"style":160,"fill":66},"the LAYERS= identifier clients send",[68,869,871],{"x":79,"y":870,"style":863,"fill":66},"142","layer.setTitle()",[68,873,874],{"x":854,"y":870,"style":160,"fill":66},"the human-readable layer name",[68,876,878],{"x":79,"y":877,"style":863,"fill":66},"172","layer.setAbstract()",[68,880,881],{"x":854,"y":877,"style":160,"fill":66},"the description in the capabilities",[68,883,885],{"x":79,"y":884,"style":863,"fill":66},"202","layer.setAttribution()",[68,887,888],{"x":854,"y":884,"style":160,"fill":66},"credit shown by capable clients",[68,890,893],{"x":79,"y":891,"style":892,"fill":149},"228","font-size:10px;font-family:sans-serif","a short name with a space in it is rejected by strict clients",[44,895],{"x":176,"y":896,"width":178,"height":179,"rx":58,"fill":180,"stroke":181,"style":897},"258","stroke-width:1.8",[68,899,901],{"x":70,"y":900,"style":185,"fill":66,"textAnchor":74},"280","set short names once — changing them later breaks every saved client bookmark",[214,903,905],{"className":216,"code":904,"language":218,"meta":219,"style":219},"for name in published:\n    layer = project.mapLayersByName(name)[0]\n    layer.setShortName(name.replace(\" \", \"_\").lower())\n    layer.setTitle(name.replace(\"_\", \" \").title())\n    layer.setAbstract(f\"{layer.featureCount()} features, updated nightly.\")\n    layer.setAttribution(\"© City Council\")\n",[221,906,907,918,933,949,963,988],{"__ignoreMap":219},[224,908,909,911,914,916],{"class":101,"line":226},[224,910,667],{"class":229},[224,912,913],{"class":233}," name ",[224,915,673],{"class":229},[224,917,693],{"class":233},[224,919,920,923,925,928,930],{"class":101,"line":243},[224,921,922],{"class":233},"    layer ",[224,924,256],{"class":229},[224,926,927],{"class":233}," project.mapLayersByName(name)[",[224,929,46],{"class":462},[224,931,932],{"class":233},"]\n",[224,934,935,938,941,943,946],{"class":101,"line":250},[224,936,937],{"class":233},"    layer.setShortName(name.replace(",[224,939,940],{"class":268},"\" \"",[224,942,289],{"class":233},[224,944,945],{"class":268},"\"_\"",[224,947,948],{"class":233},").lower())\n",[224,950,951,954,956,958,960],{"class":101,"line":262},[224,952,953],{"class":233},"    layer.setTitle(name.replace(",[224,955,945],{"class":268},[224,957,289],{"class":233},[224,959,940],{"class":268},[224,961,962],{"class":233},").title())\n",[224,964,965,968,971,974,977,980,983,986],{"class":101,"line":275},[224,966,967],{"class":233},"    layer.setAbstract(",[224,969,970],{"class":229},"f",[224,972,973],{"class":268},"\"",[224,975,976],{"class":462},"{",[224,978,979],{"class":233},"layer.featureCount()",[224,981,982],{"class":462},"}",[224,984,985],{"class":268}," features, updated nightly.\"",[224,987,272],{"class":233},[224,989,990,993,996],{"class":101,"line":280},[224,991,992],{"class":233},"    layer.setAttribution(",[224,994,995],{"class":268},"\"© City Council\"",[224,997,272],{"class":233},[14,999,1000,1002,1003,1006,1007,605],{},[199,1001,415],{}," The short name is the identifier a client puts in its ",[221,1004,1005],{},"LAYERS="," parameter, so it must be stable and should avoid spaces and non-ASCII characters — some clients quote them correctly and some do not. Setting it explicitly rather than letting the layer name serve is what protects existing clients when someone renames a layer for cartographic reasons. Deriving the abstract from the data, as here, keeps it honest across republication; the more general metadata model is described in ",[21,1008,1010],{"href":1009},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fread-and-write-layer-metadata-pyqgis\u002F","reading and writing layer metadata",[188,1012,1014],{"id":1013},"save-and-verify-before-deploying","Save and verify before deploying",[214,1016,1018],{"className":216,"code":1017,"language":218,"meta":219,"style":219},"project.write(\"\u002Fdata\u002Fprojects\u002Futilities.qgs\")\n\nreopened = QgsProject()\nreopened.read(\"\u002Fdata\u002Fprojects\u002Futilities.qgs\")\n\ntitle, ok = reopened.readEntry(\"WMSServiceTitle\", \"\u002F\")\nrestricted, ok = reopened.readListEntry(\"WMSRestrictedLayers\", \"\u002F\")\nprint(\"title:\", title)\nprint(\"hidden layers:\", restricted)\nprint(\"published:\", [l.name() for l in reopened.mapLayers().values()\n                     if l.name() not in restricted])\n",[221,1019,1020,1029,1033,1043,1052,1056,1074,1092,1106,1117,1139],{"__ignoreMap":219},[224,1021,1022,1025,1027],{"class":101,"line":226},[224,1023,1024],{"class":233},"project.write(",[224,1026,269],{"class":268},[224,1028,272],{"class":233},[224,1030,1031],{"class":101,"line":243},[224,1032,247],{"emptyLinePlaceholder":246},[224,1034,1035,1038,1040],{"class":101,"line":250},[224,1036,1037],{"class":233},"reopened ",[224,1039,256],{"class":229},[224,1041,1042],{"class":233}," QgsProject()\n",[224,1044,1045,1048,1050],{"class":101,"line":262},[224,1046,1047],{"class":233},"reopened.read(",[224,1049,269],{"class":268},[224,1051,272],{"class":233},[224,1053,1054],{"class":101,"line":275},[224,1055,247],{"emptyLinePlaceholder":246},[224,1057,1058,1061,1063,1066,1068,1070,1072],{"class":101,"line":280},[224,1059,1060],{"class":233},"title, ok ",[224,1062,256],{"class":229},[224,1064,1065],{"class":233}," reopened.readEntry(",[224,1067,286],{"class":268},[224,1069,289],{"class":233},[224,1071,292],{"class":268},[224,1073,272],{"class":233},[224,1075,1076,1079,1081,1084,1086,1088,1090],{"class":101,"line":302},[224,1077,1078],{"class":233},"restricted, ok ",[224,1080,256],{"class":229},[224,1082,1083],{"class":233}," reopened.readListEntry(",[224,1085,731],{"class":268},[224,1087,289],{"class":233},[224,1089,292],{"class":268},[224,1091,272],{"class":233},[224,1093,1094,1097,1100,1103],{"class":101,"line":317},[224,1095,1096],{"class":462},"print",[224,1098,1099],{"class":233},"(",[224,1101,1102],{"class":268},"\"title:\"",[224,1104,1105],{"class":233},", title)\n",[224,1107,1108,1110,1112,1115],{"class":101,"line":325},[224,1109,1096],{"class":462},[224,1111,1099],{"class":233},[224,1113,1114],{"class":268},"\"hidden layers:\"",[224,1116,738],{"class":233},[224,1118,1119,1121,1123,1126,1129,1131,1134,1136],{"class":101,"line":356},[224,1120,1096],{"class":462},[224,1122,1099],{"class":233},[224,1124,1125],{"class":268},"\"published:\"",[224,1127,1128],{"class":233},", [l.name() ",[224,1130,667],{"class":229},[224,1132,1133],{"class":233}," l ",[224,1135,673],{"class":229},[224,1137,1138],{"class":233}," reopened.mapLayers().values()\n",[224,1140,1141,1144,1147,1149,1151],{"class":101,"line":375},[224,1142,1143],{"class":229},"                     if",[224,1145,1146],{"class":233}," l.name() ",[224,1148,687],{"class":229},[224,1150,690],{"class":229},[224,1152,1153],{"class":233}," restricted])\n",[14,1155,1156,1158,1159,1162,1163,1166,1167,1170,1171,1174],{},[199,1157,415],{}," Reading the project back into a fresh ",[221,1160,1161],{},"QgsProject"," instance — rather than trusting the in-memory one you just configured — is what catches an entry that was written with the wrong type or under the wrong key. ",[221,1164,1165],{},"readListEntry"," is the counterpart to writing a list and returns a ",[221,1168,1169],{},"(value, ok)"," tuple like most of these readers; ignoring the ",[221,1172,1173],{},"ok"," flag turns a missing key into an empty list that looks like a deliberate empty restriction. Printing the effective published set is the check worth having in any deployment script, because \"which layers does this actually expose\" is the question that matters and the one no dialog answers directly.",[188,1176,1178],{"id":1177},"generating-a-service-project-from-a-template","Generating a service project from a template",[14,1180,1181],{},"The reason to do any of this in Python rather than in the dialog is that it scales. A template project carrying the styling and the service metadata, plus a script that swaps in the data sources, produces one service per dataset with no manual work.",[214,1183,1185],{"className":216,"code":1184,"language":218,"meta":219,"style":219},"import os\n\nTEMPLATE = \"\u002Fdata\u002Ftemplates\u002Fservice_template.qgs\"\n\ndef build_service(area_name, gpkg_path, out_dir):\n    project = QgsProject()\n    project.read(TEMPLATE)\n\n    for layer in project.mapLayers().values():\n        source = layer.source()\n        if \"TEMPLATE.gpkg\" in source:\n            layer.setDataSource(\n                source.replace(\"TEMPLATE.gpkg\", gpkg_path),\n                layer.name(), layer.providerType(),\n            )\n\n    project.writeEntry(\"WMSServiceTitle\", \"\u002F\", f\"{area_name} Utilities\")\n    project.writeEntry(\"WMSOnlineResource\", \"\u002F\",\n                       f\"https:\u002F\u002Fmaps.example.gov\u002Fows\u002F{area_name.lower()}\")\n\n    target = os.path.join(out_dir, f\"{area_name.lower()}.qgs\")\n    project.write(target)\n    return target\n",[221,1186,1187,1194,1198,1209,1213,1225,1234,1243,1247,1258,1268,1281,1286,1297,1302,1307,1312,1342,1355,1375,1380,1406,1412],{"__ignoreMap":219},[224,1188,1189,1191],{"class":101,"line":226},[224,1190,237],{"class":229},[224,1192,1193],{"class":233}," os\n",[224,1195,1196],{"class":101,"line":243},[224,1197,247],{"emptyLinePlaceholder":246},[224,1199,1200,1203,1206],{"class":101,"line":250},[224,1201,1202],{"class":462},"TEMPLATE",[224,1204,1205],{"class":229}," =",[224,1207,1208],{"class":268}," \"\u002Fdata\u002Ftemplates\u002Fservice_template.qgs\"\n",[224,1210,1211],{"class":101,"line":262},[224,1212,247],{"emptyLinePlaceholder":246},[224,1214,1215,1218,1222],{"class":101,"line":275},[224,1216,1217],{"class":229},"def",[224,1219,1221],{"class":1220},"svObZ"," build_service",[224,1223,1224],{"class":233},"(area_name, gpkg_path, out_dir):\n",[224,1226,1227,1230,1232],{"class":101,"line":280},[224,1228,1229],{"class":233},"    project ",[224,1231,256],{"class":229},[224,1233,1042],{"class":233},[224,1235,1236,1239,1241],{"class":101,"line":302},[224,1237,1238],{"class":233},"    project.read(",[224,1240,1202],{"class":462},[224,1242,272],{"class":233},[224,1244,1245],{"class":101,"line":317},[224,1246,247],{"emptyLinePlaceholder":246},[224,1248,1249,1252,1254,1256],{"class":101,"line":325},[224,1250,1251],{"class":229},"    for",[224,1253,670],{"class":233},[224,1255,673],{"class":229},[224,1257,676],{"class":233},[224,1259,1260,1263,1265],{"class":101,"line":356},[224,1261,1262],{"class":233},"        source ",[224,1264,256],{"class":229},[224,1266,1267],{"class":233}," layer.source()\n",[224,1269,1270,1273,1276,1278],{"class":101,"line":375},[224,1271,1272],{"class":229},"        if",[224,1274,1275],{"class":268}," \"TEMPLATE.gpkg\"",[224,1277,690],{"class":229},[224,1279,1280],{"class":233}," source:\n",[224,1282,1283],{"class":101,"line":394},[224,1284,1285],{"class":233},"            layer.setDataSource(\n",[224,1287,1288,1291,1294],{"class":101,"line":755},[224,1289,1290],{"class":233},"                source.replace(",[224,1292,1293],{"class":268},"\"TEMPLATE.gpkg\"",[224,1295,1296],{"class":233},", gpkg_path),\n",[224,1298,1299],{"class":101,"line":771},[224,1300,1301],{"class":233},"                layer.name(), layer.providerType(),\n",[224,1303,1304],{"class":101,"line":785},[224,1305,1306],{"class":233},"            )\n",[224,1308,1310],{"class":101,"line":1309},16,[224,1311,247],{"emptyLinePlaceholder":246},[224,1313,1315,1318,1320,1322,1324,1326,1328,1330,1332,1335,1337,1340],{"class":101,"line":1314},17,[224,1316,1317],{"class":233},"    project.writeEntry(",[224,1319,286],{"class":268},[224,1321,289],{"class":233},[224,1323,292],{"class":268},[224,1325,289],{"class":233},[224,1327,970],{"class":229},[224,1329,973],{"class":268},[224,1331,976],{"class":462},[224,1333,1334],{"class":233},"area_name",[224,1336,982],{"class":462},[224,1338,1339],{"class":268}," Utilities\"",[224,1341,272],{"class":233},[224,1343,1345,1347,1349,1351,1353],{"class":101,"line":1344},18,[224,1346,1317],{"class":233},[224,1348,399],{"class":268},[224,1350,289],{"class":233},[224,1352,292],{"class":268},[224,1354,314],{"class":233},[224,1356,1358,1361,1364,1366,1369,1371,1373],{"class":101,"line":1357},19,[224,1359,1360],{"class":229},"                       f",[224,1362,1363],{"class":268},"\"https:\u002F\u002Fmaps.example.gov\u002Fows\u002F",[224,1365,976],{"class":462},[224,1367,1368],{"class":233},"area_name.lower()",[224,1370,982],{"class":462},[224,1372,973],{"class":268},[224,1374,272],{"class":233},[224,1376,1378],{"class":101,"line":1377},20,[224,1379,247],{"emptyLinePlaceholder":246},[224,1381,1383,1386,1388,1391,1393,1395,1397,1399,1401,1404],{"class":101,"line":1382},21,[224,1384,1385],{"class":233},"    target ",[224,1387,256],{"class":229},[224,1389,1390],{"class":233}," os.path.join(out_dir, ",[224,1392,970],{"class":229},[224,1394,973],{"class":268},[224,1396,976],{"class":462},[224,1398,1368],{"class":233},[224,1400,982],{"class":462},[224,1402,1403],{"class":268},".qgs\"",[224,1405,272],{"class":233},[224,1407,1409],{"class":101,"line":1408},22,[224,1410,1411],{"class":233},"    project.write(target)\n",[224,1413,1415,1418],{"class":101,"line":1414},23,[224,1416,1417],{"class":229},"    return",[224,1419,1420],{"class":233}," target\n",[14,1422,1423,416,1425,1428,1429,1432,1433,1436,1437,1440,1441,1444,1445,1448],{},[199,1424,415],{},[221,1426,1427],{},"setDataSource"," re-points a layer at different data while keeping its styling, its short name and everything else the service depends on — which is the whole trick, because re-creating the layer would lose all of it. Passing the existing ",[221,1430,1431],{},"providerType()"," back in rather than hard-coding ",[221,1434,1435],{},"\"ogr\""," keeps the function usable for PostGIS layers too. Working on a fresh ",[221,1438,1439],{},"QgsProject()"," rather than ",[221,1442,1443],{},"QgsProject.instance()"," matters here: the singleton is shared, so a loop over twenty areas using it would accumulate state between iterations. Each call writes an independent project, and the server picks whichever the ",[221,1446,1447],{},"MAP"," parameter names.",[14,1450,1451],{},"Running this from a nightly job, followed by the read-back verification above, means a published service that is never out of step with the data behind it.",[188,1453,1455],{"id":1454},"qgis-version-compatibility","QGIS version compatibility",[14,1457,1458,1459,1461,1462,1465],{},"The ",[221,1460,419],{}," keys used here have been stable across QGIS 3 and are shared with QGIS Server of the same major version. ",[221,1463,1464],{},"QgsProject.serverProperties()"," arrived in 3.20 and offers typed accessors for the same settings. Serving a project authored in a newer QGIS from an older server generally works for simple styling and fails on newer symbol types, so keep the authoring and serving versions aligned.",[188,1467,1469],{"id":1468},"troubleshooting","Troubleshooting",[193,1471,1472,1480,1488,1497,1503,1512],{},[196,1473,1474,416,1477,1479],{},[199,1475,1476],{},"The capabilities document shows default metadata.",[221,1478,591],{}," is not true.",[196,1481,1482,416,1485,1487],{},[199,1483,1484],{},"A layer you hid is still served.",[221,1486,804],{}," was given ids instead of names.",[196,1489,1490,1493,1494,1496],{},[199,1491,1492],{},"WFS returns nothing for a published layer."," It is missing from ",[221,1495,813],{},", which takes ids.",[196,1498,1499,1502],{},[199,1500,1501],{},"Clients cannot find a layer they used yesterday."," A short name changed, or was never set and the layer was renamed.",[196,1504,1505,1508,1509,1511],{},[199,1506,1507],{},"The advertised extent covers the world."," One feature has a bad geometry, or ",[221,1510,595],{}," was not set.",[196,1513,1514,1517],{},[199,1515,1516],{},"The server cannot open the project's layers."," Relative paths that resolve differently, or database credentials stored in a personal authentication database.",[188,1519,1521],{"id":1520},"conclusion","Conclusion",[14,1523,1524],{},"Set the service metadata explicitly, bound the advertised extent and CRS list, be precise about which list takes names and which takes ids, give every published layer a stable short name, and read the project back to verify before deploying. A published service is a contract with its clients, and all of it lives in a project file you can generate.",[188,1526,1528],{"id":1527},"frequently-asked-questions","Frequently Asked Questions",[14,1530,1531,1534,1535,1538],{},[199,1532,1533],{},"Can I test the capabilities without a web server?","\nYes — the ",[221,1536,1537],{},"qgis_mapserv.fcgi"," binary can be run from the command line with the request parameters in the environment, which is enough to render a capabilities document and check it.",[14,1540,1541,1544,1545,1547],{},[199,1542,1543],{},"How do I serve several projects?","\nOne project per service endpoint, selected by the ",[221,1546,1447],{}," parameter. Generating those projects from a template in Python is the natural extension of this recipe.",[14,1549,1550,1553,1554,605],{},[199,1551,1552],{},"Does the server honour scale-based visibility?","\nYes, along with most other rendering settings, which is why the project is the configuration — see ",[21,1555,1557],{"href":1556},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fset-scale-based-visibility-pyqgis\u002F","setting scale-based visibility",[14,1559,1560,1563],{},[199,1561,1562],{},"Where do database credentials go?","\nInto the data source URI in a form the server user can read, or into a service file. A personal authentication database is not readable by the server process.",[188,1565,1567],{"id":1566},"related","Related",[193,1569,1570,1576,1582,1588,1594],{},[196,1571,1572,1575],{},[21,1573,1574],{"href":23},"Headless QGIS & Server Automation in PyQGIS"," — the guide this recipe belongs to",[196,1577,1578],{},[21,1579,1581],{"href":1580},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fuse-qgis-process-command-line-runner\u002F","Use the qgis_process Command Line Runner",[196,1583,1584],{},[21,1585,1587],{"href":1586},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Frun-pyqgis-in-docker-container\u002F","Run PyQGIS in a Docker Container",[196,1589,1590],{},[21,1591,1593],{"href":1592},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-wms-layer-pyqgis\u002F","Load a WMS Layer in PyQGIS",[196,1595,1596],{},[21,1597,1598],{"href":1009},"Read and Write Layer Metadata in PyQGIS",[1600,1601,1602],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":219,"searchDepth":243,"depth":243,"links":1604},[1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616],{"id":190,"depth":243,"text":191},{"id":211,"depth":243,"text":212},{"id":440,"depth":243,"text":441},{"id":608,"depth":243,"text":609},{"id":824,"depth":243,"text":825},{"id":1013,"depth":243,"text":1014},{"id":1177,"depth":243,"text":1178},{"id":1454,"depth":243,"text":1455},{"id":1468,"depth":243,"text":1469},{"id":1520,"depth":243,"text":1521},{"id":1527,"depth":243,"text":1528},{"id":1566,"depth":243,"text":1567},"Prepare a project for QGIS Server in Python — service metadata, which layers are published as WMS and WFS, per-layer capabilities, and checking the result before deploying.","md",{"slug":1620,"type":1621,"breadcrumb":1622,"datePublished":1623,"dateModified":1623},"publish-layers-with-qgis-server-pyqgis","article","Publish with QGIS Server","2026-09-05","\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fpublish-layers-with-qgis-server-pyqgis",{"title":5,"description":1617},"pyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fpublish-layers-with-qgis-server-pyqgis\u002Findex","AvdZKYytExykyEQ6EsoEX0Y5y3Y3WDWs3F8ZB73f1Pc",1788563847739]