{"id":130,"date":"2026-03-30T08:18:53","date_gmt":"2026-03-30T08:18:53","guid":{"rendered":"https:\/\/blogs.bharatstacks.com\/?p=130"},"modified":"2026-04-08T17:11:51","modified_gmt":"2026-04-08T17:11:51","slug":"geoserver-flask-serving-maps-the-right-way-2","status":"publish","type":"post","link":"https:\/\/blogs.bharatstacks.com\/index.php\/2026\/03\/30\/geoserver-flask-serving-maps-the-right-way-2\/","title":{"rendered":"GeoServer + Flask &#8211; Serving Maps the Right Way"},"content":{"rendered":"\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Building a web application that displays geographic maps is one of those tasks that feels deceptively simple at first &#8211; until a developer realises they have no idea how their geographic data (a shapefile, a PostGIS table, a GeoTIFF) is actually supposed to reach the browser as a rendered, zoomable, clickable map. A Flask backend alone cannot do this. Leaflet alone cannot do this. There is a crucial piece sitting in between those two &#8211; a geospatial server -and that piece is GeoServer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide walks through what GeoServer is, how it gets installed, how its internal concepts fit together, and how a Flask application communicates with it to serve real geographic data to a Leaflet map in the browser. Every concept is explained from first principles, because GeoServer introduces several new terms -WMS, WFS, CRS, workspace, store, layer &#8211; that can feel overwhelming when encountered all at once. By the end of this guide, none of those terms will be unfamiliar.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"is-style-default wp-block-paragraph\">   Who This Guide Is For<\/p>\n\n\n\n<ul class=\"wp-block-list has-medium-font-size\">\n<li class=\"has-medium-font-size\">Developers who know Python and Flask but have never worked with geospatial servers before<\/li>\n\n\n\n<li class=\"has-medium-font-size\">Anyone who has a shapefile or geographic dataset and wants to display it on a web map<\/li>\n\n\n\n<li class=\"has-medium-font-size\">Developers who have heard of GeoServer but find the official documentation overwhelming<\/li>\n\n\n\n<li class=\"has-medium-font-size\">Anyone building a map-based feature in a Flask application from scratch<\/li>\n<\/ul>\n<\/blockquote>\n\n\n\n<div style=\"max-width:600px;margin:40px auto;font-family:Arial, Helvetica, sans-serif\">\n\n<div style=\"background:#e9eeea;border:1px solid #d5dbd6;border-radius:10px;padding:30px 40px\">\n\n<div style=\"font-size:14px;letter-spacing:2px;font-weight:600;color:#6c7f73;margin-bottom:25px\">\nWHAT&#8217;S COVERED\n<\/div>\n\n<ul style=\"padding:0;margin:0\">\n\n<li style=\"align-items:center;margin-bottom:14px;font-size:15px;color:#3f4b43\">\n<span style=\"width:35px;font-weight:600;color:#48a77c\"><\/span>\nWhat is GeoServer?\n<\/li>\n\n<li style=\"align-items:center;margin-bottom:14px;font-size:15px;color:#3f4b43\">\n<span style=\"width:35px;font-weight:600;color:#48a77c\"><\/span>\nInstallation\n<\/li>\n\n<li style=\"align-items:center;margin-bottom:14px;font-size:15px;color:#3f4b43\">\n<span style=\"width:35px;font-weight:600;color:#48a77c\"><\/span>\nWMS vs WFS explained\n<\/li>\n\n<li style=\"align-items:center;margin-bottom:14px;font-size:15px;color:#3f4b43\">\n<span style=\"width:35px;font-weight:600;color:#48a77c\"><\/span>\nWorkspaces &amp; Layers\n<\/li>\n\n<li style=\"align-items:center;margin-bottom:14px;font-size:15px;color:#3f4b43\">\n<span style=\"width:35px;font-weight:600;color:#48a77c\"><\/span>\nPublishing your first layer\n<\/li>\n\n<li style=\"align-items:center;margin-bottom:14px;font-size:15px;color:#3f4b43\">\n<span style=\"width:35px;font-weight:600;color:#48a77c\"><\/span>\nFlask + GeoServer integration\n<\/li>\n\n<li style=\"align-items:center;margin-bottom:14px;font-size:15px;color:#3f4b43\">\n<span style=\"width:35px;font-weight:600;color:#48a77c\"><\/span>\nRendering with Leaflet\n<\/li>\n\n<li style=\"align-items:center;font-size:15px;color:#3f4b43\">\n<span style=\"width:35px;font-weight:600;color:#48a77c\"><\/span>\nProduction tips\n<\/li>\n\n<\/ul>\n\n<\/div>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\">01. What is GeoServer, Really?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GeoServer is an open-source server application that stores and serves geographic data over the web. It is written in Java and is one of the most widely used geospatial servers in the world, powering everything from small research projects to national mapping portals used by millions of people.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The most useful way to understand GeoServer is by analogy: GeoServer is to maps what PostgreSQL is to relational data. Just as PostgreSQL stores rows and columns and responds to SQL queries over a network, GeoServer stores geographic features &#8211; polygons, lines, points, raster images &#8211; and responds to geospatial queries over HTTP. A Flask backend talks to PostgreSQL using&nbsp;<code>psycopg2<\/code>&nbsp;or SQLAlchemy. A Flask backend talks to GeoServer using standard HTTP requests that follow protocols called WMS and WFS (explained in detail in section 03).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The reason GeoServer exists &#8211; rather than just serving shapefiles directly from a Flask endpoint &#8211; is that geographic data handling is genuinely complex. Coordinate reference systems, bounding box calculations, map rendering, tiling, format conversion, reprojection &#8211; these are hard problems that GeoServer has already solved. By delegating geographic concerns to GeoServer, a Flask application can focus purely on business logic.<br><\/p>\n\n\n\n<div style=\"max-width:330px;margin:60px auto;font-family:Arial,Helvetica,sans-serif;text-align:center\">\n\n<div style=\"letter-spacing:4px;font-size:13px;color:#6b7280;margin-bottom:30px\">\nTHE FULL STACK\n<\/div>\n\n<div style=\"background:#f8faf9;border:1px solid #d7e6dd;border-radius:14px;padding:28px;margin-bottom:10px\">\n<div style=\"font-size:22px;font-weight:600;color:#2f6f58\">Browser \/ Leaflet<\/div>\n<div style=\"margin-top:6px;color:#6b7280;font-size:14px\">renders tiles &amp; features<\/div>\n<\/div>\n\n<div style=\"font-size:28px;color:#9ca3af;margin:8px 0\">\u2193<\/div>\n\n<div style=\"background:#f8faf9;border:1px solid #d7e6dd;border-radius:14px;padding:28px;margin-bottom:10px\">\n<div style=\"font-size:22px;font-weight:600;color:#2f6f58\">Flask App<\/div>\n<div style=\"margin-top:6px;color:#6b7280;font-size:14px\">proxies &amp; business logic<\/div>\n<\/div>\n\n<div style=\"font-size:28px;color:#9ca3af;margin:8px 0\">\u2193<\/div>\n\n<div style=\"background:#f8faf9;border:1px solid #d7e6dd;border-radius:14px;padding:28px;margin-bottom:10px\">\n<div style=\"font-size:22px;font-weight:600;color:#2f6f58\">GeoServer<\/div>\n<div style=\"margin-top:6px;color:#6b7280;font-size:14px\">WMS \/ WFS \/ REST API<\/div>\n<\/div>\n\n<div style=\"font-size:28px;color:#9ca3af;margin:8px 0\">\u2193<\/div>\n\n<div style=\"background:#f8faf9;border:1px solid #d7e6dd;border-radius:14px;padding:28px\">\n<div style=\"font-size:22px;font-weight:600;color:#2f6f58\">Data Store<\/div>\n<div style=\"margin-top:6px;color:#6b7280;font-size:14px\">PostGIS \u00b7 Shapefile \u00b7 GeoTIFF<\/div>\n<\/div>\n\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Reading the diagram top-to-bottom: a user opens a map in their browser. Leaflet (the JavaScript map library) sends HTTP requests to the Flask backend asking for map tiles and feature data. Flask forwards those requests to GeoServer, optionally adding authentication or filtering. GeoServer reads the raw geographic data from its data store, renders or packages it, and returns the response back up the chain to the browser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Each layer in this stack has a single clear responsibility. GeoServer is the only layer that knows about shapefiles and coordinates. Flask is the only layer that knows about business rules and user authentication. Leaflet is the only layer that knows about the browser&#8217;s screen. This separation is what makes the system clean and maintainable.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><em>GeoServer is to maps what PostgreSQL is to data \u2014 the engine configured once and queried forever.<\/em><\/p>\n<\/blockquote>\n\n\n\n<pre class=\"wp-block-code has-medium-font-size\"><code>What is OGC?\n\nOGC stands for Open Geospatial Consortium. It is the international standards body that defines the protocols GeoServer implements, including WMS and WFS. Because GeoServer follows these standards, any OGC-compatible map client \u2014 Leaflet, OpenLayers, QGIS \u2014 can connect to it automatically without any custom integration. This is a significant advantage: the same GeoServer instance can serve desktop GIS software, web applications, and mobile apps simultaneously.<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">02. Installation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GeoServer is a Java application. This means the first requirement before GeoServer itself can be installed is a working Java runtime. GeoServer 2.24 supports Java 11 and Java 17. Java 8 &#8211; which was supported by older GeoServer versions &#8211; is no longer compatible and will cause GeoServer to refuse to start. The installation steps below use Java 11, which is the most commonly referenced version in GeoServer documentation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Option A &#8211; Direct Installation on Ubuntu<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Direct installation means downloading GeoServer&#8217;s binary package and running it on the host machine. This approach is straightforward and gives direct access to GeoServer&#8217;s files and logs without any container layer in between.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once the startup script runs, GeoServer&#8217;s log output will appear in the terminal. The startup process typically takes 20\u201360 seconds. A successful startup ends with a line similar to&nbsp;<code>INFO:Started ServerConnector...{0.0.0.0:8080}<\/code>, which confirms GeoServer is listening for connections. The admin panel is then accessible in a browser at&nbsp;<code>http:\/\/localhost:8080\/geoserver\/web<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The default login credentials are&nbsp;<strong>admin<\/strong>&nbsp;as the username and&nbsp;<strong>geoserver<\/strong>&nbsp;as the password. These credentials are publicly documented and are the first thing automated security scanners try on any exposed GeoServer instance. The password must be changed immediately after the first login &#8211; this is covered in the first login section below.<\/p>\n\n\n<div \n    class=\"align wp-block-bch-code-highlight\" \n    id=\"bhcCodeHighlight-1\" \n    data-attributes='{&quot;cId&quot;:&quot;b04acfad-5&quot;,&quot;language&quot;:&quot;python&quot;,&quot;lineNumbers&quot;:false,&quot;theme&quot;:&quot;porple&quot;,&quot;headerPart&quot;:true,&quot;codeTypo&quot;:{&quot;desktop&quot;:15,&quot;tablet&quot;:15,&quot;mobile&quot;:14},&quot;codeBg&quot;:{&quot;color&quot;:&quot;#292c36&quot;},&quot;align&quot;:&quot;&quot;,&quot;copyBtnOption&quot;:&quot;Text&quot;,&quot;headingTitleTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:18,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;,&quot;textTransform&quot;:&quot;uppercase&quot;},&quot;clipBoard&quot;:true,&quot;wordWrap&quot;:true,&quot;width&quot;:{&quot;desktop&quot;:&quot;100%&quot;,&quot;tablet&quot;:&quot;100%&quot;,&quot;mobile&quot;:&quot;100%&quot;},&quot;height&quot;:{&quot;desktop&quot;:&quot;0px&quot;,&quot;tablet&quot;:&quot;0px&quot;,&quot;mobile&quot;:&quot;0px&quot;},&quot;padding&quot;:{&quot;top&quot;:&quot;0px&quot;,&quot;right&quot;:&quot;0px&quot;,&quot;bottom&quot;:&quot;0px&quot;,&quot;left&quot;:&quot;0px&quot;},&quot;background&quot;:{&quot;color&quot;:&quot;#d3cfcf42&quot;},&quot;headerBg&quot;:{&quot;color&quot;:&quot;#2f446e&quot;},&quot;headerTextColor&quot;:{&quot;color&quot;:&quot;#ffff&quot;},&quot;headerThemeOpt&quot;:&quot;one&quot;,&quot;layout&quot;:{&quot;align&quot;:&quot;left&quot;},&quot;border&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;},&quot;shadow&quot;:[{&quot;hOffset&quot;:&quot;0px&quot;,&quot;vOffset&quot;:&quot;0px&quot;,&quot;blur&quot;:&quot;0px&quot;,&quot;spread&quot;:&quot;0px&quot;,&quot;color&quot;:&quot;rgba(0, 0, 0, 0.16)&quot;,&quot;isInset&quot;:false}],&quot;alignment&quot;:&quot;center&quot;,&quot;copyTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:13,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;clipBoardColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#00000024&quot;},&quot;copyIconSize&quot;:{&quot;desktop&quot;:24,&quot;tablet&quot;:20,&quot;mobile&quot;:18},&quot;copyBtnPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;headerPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;14px&quot;,&quot;left&quot;:&quot;14px&quot;,&quot;bottom&quot;:&quot;14px&quot;,&quot;right&quot;:&quot;14px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;12px&quot;,&quot;left&quot;:&quot;12px&quot;,&quot;bottom&quot;:&quot;12px&quot;,&quot;right&quot;:&quot;12px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;10px&quot;,&quot;left&quot;:&quot;10px&quot;,&quot;bottom&quot;:&quot;10px&quot;,&quot;right&quot;:&quot;10px&quot;}},&quot;copyBtnPosition&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;}},&quot;outputOption&quot;:false,&quot;outputText&quot;:&quot;Hello Word !&quot;,&quot;outputColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#2F446E&quot;},&quot;outputTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:21,&quot;tablet&quot;:17,&quot;mobile&quot;:13},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;outputPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;8px&quot;,&quot;left&quot;:&quot;8px&quot;,&quot;bottom&quot;:&quot;8px&quot;,&quot;right&quot;:&quot;8px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;7px&quot;,&quot;left&quot;:&quot;7px&quot;,&quot;bottom&quot;:&quot;7px&quot;,&quot;right&quot;:&quot;7px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;5px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;outputBorder&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;,&quot;radius&quot;:&quot;7px&quot;}}'\n>\n    <pre id=\"codeHightLight\" style=\"display:none;\">\n        &quot;#Step 1: Update the package list\\nsudo apt update\\n\\n#Step 2: Install Java 11\\nsudo apt install -y openjdk-11-jdk\\n\\n#Step 3: Verify Java is installed correctly\\njava -version\\n\\n#Step 4: Download GeoServer 2.24.0 binary ZIP\\nwget \\&quot;https:\\\/\\\/sourceforge.net\\\/projects\\\/geoserver\\\/files\\n\\\/GeoServer\\\/2.24.0\\\/geoserver-2.24.0-bin.zip\\&quot;\\n\\n#Step 5: Install unzip\\nsudo apt install -y unzip\\nsudo unzip geoserver-2.24.0-bin.zip -d \\\/opt\\\/geoserver\\n\\n#Step 6: Start GeoServer\\n\\\/opt\\\/geoserver\\\/bin\\\/startup.sh&quot;    <\/pre>\n<\/div>\n\n\n\t\t \n\n\n<p class=\"wp-block-paragraph\"><strong>Option B &#8211; Docker (Recommended for Most Projects)<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Running GeoServer inside Docker is the recommended approach for most development and production deployments. Docker isolates GeoServer&#8217;s Java environment from the host operating system, meaning the correct Java version is guaranteed without affecting any other software on the server. It also makes upgrading GeoServer to a newer version a matter of changing a single version number in the Docker Compose file.<\/p>\n\n\n<div \n    class=\"align wp-block-bch-code-highlight\" \n    id=\"bhcCodeHighlight-2\" \n    data-attributes='{&quot;cId&quot;:&quot;40a91f14-e&quot;,&quot;language&quot;:&quot;python&quot;,&quot;lineNumbers&quot;:false,&quot;theme&quot;:&quot;porple&quot;,&quot;headerPart&quot;:true,&quot;codeTypo&quot;:{&quot;desktop&quot;:15,&quot;tablet&quot;:15,&quot;mobile&quot;:14},&quot;codeBg&quot;:{&quot;color&quot;:&quot;#292c36&quot;},&quot;align&quot;:&quot;&quot;,&quot;copyBtnOption&quot;:&quot;Text&quot;,&quot;headingTitleTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:18,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;,&quot;textTransform&quot;:&quot;uppercase&quot;},&quot;clipBoard&quot;:true,&quot;wordWrap&quot;:true,&quot;width&quot;:{&quot;desktop&quot;:&quot;100%&quot;,&quot;tablet&quot;:&quot;100%&quot;,&quot;mobile&quot;:&quot;100%&quot;},&quot;height&quot;:{&quot;desktop&quot;:&quot;0px&quot;,&quot;tablet&quot;:&quot;0px&quot;,&quot;mobile&quot;:&quot;0px&quot;},&quot;padding&quot;:{&quot;top&quot;:&quot;0px&quot;,&quot;right&quot;:&quot;0px&quot;,&quot;bottom&quot;:&quot;0px&quot;,&quot;left&quot;:&quot;0px&quot;},&quot;background&quot;:{&quot;color&quot;:&quot;#d3cfcf42&quot;},&quot;headerBg&quot;:{&quot;color&quot;:&quot;#2f446e&quot;},&quot;headerTextColor&quot;:{&quot;color&quot;:&quot;#ffff&quot;},&quot;headerThemeOpt&quot;:&quot;one&quot;,&quot;layout&quot;:{&quot;align&quot;:&quot;left&quot;},&quot;border&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;},&quot;shadow&quot;:[{&quot;hOffset&quot;:&quot;0px&quot;,&quot;vOffset&quot;:&quot;0px&quot;,&quot;blur&quot;:&quot;0px&quot;,&quot;spread&quot;:&quot;0px&quot;,&quot;color&quot;:&quot;rgba(0, 0, 0, 0.16)&quot;,&quot;isInset&quot;:false}],&quot;alignment&quot;:&quot;center&quot;,&quot;copyTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:13,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;clipBoardColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#00000024&quot;},&quot;copyIconSize&quot;:{&quot;desktop&quot;:24,&quot;tablet&quot;:20,&quot;mobile&quot;:18},&quot;copyBtnPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;headerPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;14px&quot;,&quot;left&quot;:&quot;14px&quot;,&quot;bottom&quot;:&quot;14px&quot;,&quot;right&quot;:&quot;14px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;12px&quot;,&quot;left&quot;:&quot;12px&quot;,&quot;bottom&quot;:&quot;12px&quot;,&quot;right&quot;:&quot;12px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;10px&quot;,&quot;left&quot;:&quot;10px&quot;,&quot;bottom&quot;:&quot;10px&quot;,&quot;right&quot;:&quot;10px&quot;}},&quot;copyBtnPosition&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;}},&quot;outputOption&quot;:false,&quot;outputText&quot;:&quot;Hello Word !&quot;,&quot;outputColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#2F446E&quot;},&quot;outputTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:21,&quot;tablet&quot;:17,&quot;mobile&quot;:13},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;outputPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;8px&quot;,&quot;left&quot;:&quot;8px&quot;,&quot;bottom&quot;:&quot;8px&quot;,&quot;right&quot;:&quot;8px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;7px&quot;,&quot;left&quot;:&quot;7px&quot;,&quot;bottom&quot;:&quot;7px&quot;,&quot;right&quot;:&quot;7px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;5px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;outputBorder&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;,&quot;radius&quot;:&quot;7px&quot;}}'\n>\n    <pre id=\"codeHightLight\" style=\"display:none;\">\n        &quot;version: \\&quot;3.9\\&quot;\\nservices:\\n\\n  geoserver:\\n    image: kartoza\\\/geoserver:2.24.0\\n    environment:\\n      #Set a strong admin password\\n      - GEOSERVER_ADMIN_PASSWORD=PWD\\n      - GEOSERVER_ADMIN_USER=admin\\n      # Memory allocated to the Java Virtual Machine\\n      - INITIAL_MEMORY=512m\\n      - MAXIMUM_MEMORY=1g\\n    ports:\\n      - \\&quot;8080:8600\\&quot;\\n    volumes:\\n      - .\\\/geoserver_data:\\\/opt\\\/geoserver\\\/data_dir\\n    restart: unless-stopped\\n\\n  flask:\\n    build: .\\n    ports:\\n      - \\&quot;5000:5000\\&quot;\\n    depends_on:\\n      - geoserver\\n&quot;    <\/pre>\n<\/div>\n\n\n\t\t \n\n\n<p class=\"wp-block-paragraph\">To start both services together, run&nbsp;<code>docker compose up-d<\/code>&nbsp;in the directory containing the file above. Docker downloads the GeoServer image on the first run (this may take a few minutes) and then starts both containers. The GeoServer admin panel becomes available at the same address:&nbsp;<code>http:\/\/localhost:8080\/geoserver\/web<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code has-background\" style=\"background-color:#eedbdb\"><code>Critical \u2014 Always Mount a Volume\n\nThe&nbsp;geoserver_data&nbsp;volume in the Docker Compose file above is not optional. GeoServer's data directory is where every workspace, layer, style, and security setting is stored as XML files. Without the volume mount, all of that configuration disappears every time the container is restarted. A developer who skips this and spends an hour configuring layers will lose all of that work the first time the container restarts. Mount the volume from day one.<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>What is the Data Directory?\n\nGeoServer's data directory (data_dir\/) is the folder on disk where all configuration lives. It contains XML files describing workspaces, data stores, layers, and styles. When GeoServer starts, it reads these files to reconstruct its state. When an admin changes something through the web panel, GeoServer updates these files. Backing up this directory is equivalent to backing up all of GeoServer's configuration.<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">03. WMS vs WFS &#8211; Two Protocols, Two Very Different Things<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When developers first encounter GeoServer, one of the most confusing aspects is that it offers multiple protocols for accessing the same geographic data. WMS and WFS are the two most important, and they are frequently mentioned in the same breath &#8211; which leads many beginners to assume they are interchangeable. They are not. They answer fundamentally different questions, and choosing the wrong one for a given task produces results that either work poorly or do not work at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Understanding the distinction between WMS and WFS is arguably the single most important conceptual step in working with GeoServer. Everything else builds on top of this understanding.<\/p>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-3a88641f wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:#FFFFFF\" class=\"has-inline-color\">WMS \u2014 Web Map Service<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Returns a rendered image<\/strong><\/h4>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\">A WMS request asks GeoServer: &#8220;Give me a map of this area as a picture.&#8221; GeoServer reads the underlying data, applies styling rules, renders the map onto a canvas, and returns a PNG or JPEG image. The browser receives pixels. It cannot click on a polygon to get its name. It cannot filter features by an attribute. It just sees an image.<\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:#FFFFFF\" class=\"has-inline-color\">WFS \u2014 Web Feature Service<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Returns raw geographic data<\/strong><\/h4>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\">A WFS request asks GeoServer: &#8220;Give me the actual features in this area as data.&#8221; GeoServer returns a GeoJSON or GML document containing the coordinate geometries and all the attribute values. The browser receives real data it can process &#8211; filter, measure, display in a table, or draw on a map with custom styling.<\/p>\n<\/div>\n<\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-3a88641f wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-vertically-aligned-top is-layout-flow wp-block-column-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:#FFFFFF\" class=\"has-inline-color\">When to Use WMS<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Display without interaction<\/strong><\/h4>\n\n\n\n<p class=\"has-text-align-left has-medium-font-size wp-block-paragraph\">Background layers, choropleth maps, raster imagery, any overlay where the user needs to see something but does not need to click on or query individual features. WMS is efficient because a single image request replaces hundreds of individual feature requests. Leaflet&#8217;s&nbsp;<code>L.tileLayer.wms()<\/code>&nbsp;is built exactly for this.<\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-vertically-aligned-top is-layout-flow wp-block-column-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:#FFFFFF\" class=\"has-inline-color\">When to Use WFS<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Interaction and processing<\/strong><\/h4>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\">Popups that show a feature&#8217;s attributes when clicked, filtering districts by population, counting features in an area, any feature the application needs to reason about as data rather than as pixels. WFS is the right choice whenever the application logic needs to know&nbsp;<em>what<\/em>&nbsp;is on the map, not just&nbsp;<em>how<\/em>&nbsp;it looks.<\/p>\n<\/div>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Anatomy of a WMS Request<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A WMS GetMap request is a plain HTTP GET with a set of required parameters. Understanding what each parameter does removes the mystery from the URL strings that map libraries generate automatically. <br>When Leaflet&#8217;s&nbsp;<code>L.tileLayer.wms()<\/code>&nbsp;is used, Leaflet constructs this URL automatically for each tile it needs, filling in the correct BBOX and dimensions based on the map&#8217;s current view. The developer only needs to supply the base URL and layer name -Leaflet handles the rest.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Anatomy of a WFS Request<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A WFS GetFeature request returns geographic features as data. One of WFS&#8217;s most powerful features is CQL filtering &#8211; a SQL-like query language that lets the application retrieve only the features that match certain conditions, without downloading the entire dataset.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>What is EPSG:4326?\n\nEPSG:4326 is the most common coordinate reference system (CRS) used on the web. It represents coordinates as standard latitude and longitude values - the same system used by GPS devices and Google Maps. When a shapefile or PostGIS table uses EPSG:4326, coordinates like&nbsp;&#91;80.9, 26.8]&nbsp;mean longitude 80.9 East, latitude 26.8 North - which is approximately Lucknow, India. When GeoServer asks for a CRS and nothing is specified, EPSG:4326 is the right default to enter for most geographic datasets.<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-table is-style-stripes has-medium-font-size\"><table><thead><tr><th class=\"has-text-align-left\" data-align=\"left\"><strong>Aspect<\/strong><\/th><th class=\"has-text-align-center\" data-align=\"center\"><strong>WMS<\/strong><\/th><th><strong>WFS<\/strong><\/th><\/tr><\/thead><tbody><tr><td class=\"has-text-align-left\" data-align=\"left\">What it returns<\/td><td class=\"has-text-align-center\" data-align=\"center\">A rendered PNG or JPEG image<\/td><td>GeoJSON or GML feature data<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">Bandwidth usage<\/td><td class=\"has-text-align-center\" data-align=\"center\">Lower \u2014 just pixels<\/td><td>Higher &#8211; all coordinates and attributes<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">Can be filtered<\/td><td class=\"has-text-align-center\" data-align=\"center\">Only visually (by style)<\/td><td>Yes &#8211; full CQL query language support<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">Can show popups<\/td><td class=\"has-text-align-center\" data-align=\"center\">Requires a separate GetFeatureInfo call<\/td><td>Yes &#8211; attributes are in the response<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">Best suited for<\/td><td class=\"has-text-align-center\" data-align=\"center\">Background maps, large datasets as imagery<\/td><td>Interactive features, queries, analytics<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">Leaflet method<\/td><td class=\"has-text-align-center\" data-align=\"center\">L.tileLayer.wms()<\/td><td><code>L.geoJSON()<\/code>&nbsp;after fetching<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">04.&nbsp;How Workspaces and Layers Work Together<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GeoServer organises its content in a three-level hierarchy: Workspaces contain Stores, and Stores contain Layers. Understanding this hierarchy is essential before any data can be published, because every URL that GeoServer exposes &#8211; every WMS endpoint, every WFS endpoint &#8211; reflects this structure.<\/p>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-3a88641f wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-vertically-aligned-top is-layout-flow wp-block-column-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:#FFFFFF\" class=\"has-inline-color\">Workspace<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>The top-level namespace<\/strong><\/h4>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\">A workspace is a logical grouping, similar to a namespace or a schema in a database. It appears directly in GeoServer&#8217;s URLs:&nbsp;<code>\/geoserver\/<strong>myworkspace<\/strong>\/wms<\/code>. Most applications create one workspace per project. All layers within a workspace share the same URL prefix.<\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-vertically-aligned-top is-layout-flow wp-block-column-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:#FFFFFF\" class=\"has-inline-color\">Store (Data Store)<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>The connection to data<\/strong><\/h4>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\">A store is a connection to an actual data source &#8211; a PostGIS database, a directory full of shapefiles, a GeoTIFF raster file. The store holds the connection credentials and file paths. A single store can expose multiple layers. One PostGIS database store, for example, can serve dozens of tables as individual layers.<\/p>\n<\/div>\n<\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-3a88641f wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-vertically-aligned-top is-layout-flow wp-block-column-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:#FFFFFF\" class=\"has-inline-color has-contrast-color\">Layer<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>The published geographic feature<\/strong><\/h4>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\">A layer is a specific, published piece of geographic data &#8211; one shapefile, one PostGIS table, one raster band. Layers are always referenced using the colon notation:&nbsp;<code>workspace:layername<\/code>. This notation appears in every WMS and WFS request, and understanding it removes a lot of the confusion around GeoServer&#8217;s URL structure.<\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-vertically-aligned-top is-layout-flow wp-block-column-is-layout-flow\">\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-3a88641f wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:#FFFFFF\" class=\"has-inline-color\">Layer Group<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Multiple layers as one<\/strong><\/h4>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\">A layer group is a named collection of multiple layers that can be requested as a single unit from WMS. This is useful when a &#8220;basemap&#8221; is actually four separate datasets &#8211; roads, water bodies, land cover, district boundaries &#8211; that should always be rendered together. The client requests one layer group name and receives all four datasets composited into a single image.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The three-level structure means that a WMS request always follows the pattern&nbsp;<code>\/geoserver\/{workspace}\/wms?...&amp;LAYERS={workspace}:{layername}<\/code>. The workspace appears twice: once in the URL path (which restricts the request to that workspace&#8217;s data store), and once in the LAYERS parameter (which identifies the specific layer). Both must match.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>The Colon Notation Explained\n\nWhenever&nbsp;<strong>myproject:districts<\/strong>&nbsp;appears in a WMS or WFS request, it means: the layer named&nbsp;<strong>districts<\/strong>&nbsp;in the workspace named&nbsp;<strong>myproject<\/strong>. The colon separates workspace from layer name. This notation is consistent across all GeoServer protocols - WMS, WFS, the REST API, and the admin panel all use it. Once this pattern is understood, GeoServer's URL structure stops being confusing.<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">05.&nbsp;Publishing the First Layer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Publishing a layer in GeoServer means making a piece of geographic data &#8211; a shapefile, a PostGIS table &#8211; available over WMS and WFS. The process goes through GeoServer&#8217;s web admin panel and involves three things: creating a workspace, creating a store (pointing at the data source), and publishing the layer from that store. The steps below use the admin panel&#8217;s web interface.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before starting, GeoServer should be running and the admin panel should be accessible at&nbsp;<code>http:\/\/localhost:8080\/geoserver\/web<\/code>. The default admin password should already have been changed from&nbsp;<code>geoserver<\/code>&nbsp;to something secure.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Create a Workspace:<\/strong> In the left sidebar, navigate to <strong>Data \u2192 Workspaces \u2192 Add new workspace<\/strong>. Enter a name (for example,&nbsp;<code>myproject<\/code>) and a namespace URI. The namespace URI looks like a URL &#8211; for example,&nbsp;<code>http:\/\/myproject.com<\/code>&nbsp;&#8211; but it does not need to actually resolve to anything. It is just a unique identifier for the workspace. Check &#8220;Default workspace&#8221; if this will be the primary workspace for the application. Click Save.<\/li>\n\n\n\n<li><strong>Create a Data Store:<\/strong> Navigate to <strong>Data \u2192 Stores \u2192 Add new store<\/strong>. For a shapefile, select &#8220;Shapefile&#8221; under the Vector Data Sources section. For a PostGIS database, select &#8220;PostGIS&#8221;. Give the store a name that describes the data source (for example,&nbsp;<code>districts_shapefile<\/code>). For a shapefile store, use the file browser to navigate to the&nbsp;<code>.shp<\/code>&nbsp;file. For a PostGIS store, enter the database host, port, database name, username, and password. Click Save.<\/li>\n\n\n\n<li><strong>Publish the Layer:<\/strong> After saving the store, GeoServer displays a list of detected feature types available in that store. Each entry has a &#8220;Publish&#8221; link. Clicking &#8220;Publish&#8221; opens the layer configuration page. This is where the layer&#8217;s name, title, and  critically &#8211; its coordinate reference system are configured.<\/li>\n\n\n\n<li><strong>Set the Coordinate Reference System (CRS): <\/strong>This is the step where most beginners get stuck. GeoServer may detect the CRS automatically from the shapefile&#8217;s&nbsp;<code>.prj<\/code>&nbsp;file, in which case the &#8220;Native SRS&#8221; field will show a code like&nbsp;<code>EPSG:4326<\/code>. If it shows &#8220;UNKNOWN&#8221;, the CRS must be entered manually. For most geographic datasets using standard latitude\/longitude, entering&nbsp;<code>EPSG:4326<\/code>&nbsp;is correct. After setting the native SRS, click the two &#8220;Compute from data&#8221; links under the &#8220;Bounding Boxes&#8221; section to let GeoServer calculate the layer&#8217;s geographic extent automatically. Without bounding boxes, the layer preview will not work.<\/li>\n\n\n\n<li><strong>Save and Preview:<\/strong> Click Save at the bottom of the layer configuration page. Navigate to <strong>Data \u2192 Layer Preview<\/strong>, find the newly published layer in the list, and click the &#8220;OpenLayers&#8221; link in the &#8220;All Formats&#8221; dropdown. A new browser tab should open showing an interactive map of the layer rendered by GeoServer. If the map appears, the layer is live and ready to be consumed by Flask and Leaflet.<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>If Features Appear in the Ocean\n\nA common issue after publishing a layer is that features appear somewhere completely wrong \u2014 often near the coordinates (0,0) in the middle of the Atlantic Ocean, or scattered in a completely wrong location. This almost always means the CRS is wrong. Return to the layer configuration page, verify the native SRS and declared SRS both show&nbsp;EPSG:4326&nbsp;(or the correct CRS for the data), click \"Force declared\" from the SRS handling dropdown, recompute the bounding boxes, and save. This resolves the issue in the vast majority of cases.<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">06.&nbsp;Flask + GeoServer Integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Flask does not connect to GeoServer the way it connects to a database. There is no ORM, no connection pool, no driver to install. GeoServer is a web service &#8211; it speaks HTTP &#8211; so Flask communicates with it using plain HTTP requests, exactly the way one Flask service might call another API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In practice, Flask serves two roles in the GeoServer stack. First, it acts as a proxy \u2014 it receives requests from the browser for WMS tiles or WFS feature data, adds authentication or rate limiting as needed, and forwards the request to GeoServer. Second, it can use GeoServer&#8217;s REST API to manage GeoServer programmatically &#8211; creating workspaces, registering data stores, publishing layers &#8211; all without touching the admin panel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The cleanest approach is to encapsulate all GeoServer communication in a dedicated client class, keeping it separate from Flask&#8217;s route handlers. This makes the GeoServer interaction easy to test, easy to replace, and easy to extend.<\/p>\n\n\n<div \n    class=\"align wp-block-bch-code-highlight\" \n    id=\"bhcCodeHighlight-3\" \n    data-attributes='{&quot;cId&quot;:&quot;5acbc37a-2&quot;,&quot;language&quot;:&quot;python&quot;,&quot;lineNumbers&quot;:false,&quot;theme&quot;:&quot;porple&quot;,&quot;headerPart&quot;:true,&quot;codeTypo&quot;:{&quot;desktop&quot;:15,&quot;tablet&quot;:15,&quot;mobile&quot;:14},&quot;codeBg&quot;:{&quot;color&quot;:&quot;#292c36&quot;},&quot;align&quot;:&quot;&quot;,&quot;copyBtnOption&quot;:&quot;Text&quot;,&quot;headingTitleTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:18,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;,&quot;textTransform&quot;:&quot;uppercase&quot;},&quot;clipBoard&quot;:true,&quot;wordWrap&quot;:true,&quot;width&quot;:{&quot;desktop&quot;:&quot;100%&quot;,&quot;tablet&quot;:&quot;100%&quot;,&quot;mobile&quot;:&quot;100%&quot;},&quot;height&quot;:{&quot;desktop&quot;:&quot;0px&quot;,&quot;tablet&quot;:&quot;0px&quot;,&quot;mobile&quot;:&quot;0px&quot;},&quot;padding&quot;:{&quot;top&quot;:&quot;0px&quot;,&quot;right&quot;:&quot;0px&quot;,&quot;bottom&quot;:&quot;0px&quot;,&quot;left&quot;:&quot;0px&quot;},&quot;background&quot;:{&quot;color&quot;:&quot;#d3cfcf42&quot;},&quot;headerBg&quot;:{&quot;color&quot;:&quot;#2f446e&quot;},&quot;headerTextColor&quot;:{&quot;color&quot;:&quot;#ffff&quot;},&quot;headerThemeOpt&quot;:&quot;one&quot;,&quot;layout&quot;:{&quot;align&quot;:&quot;left&quot;},&quot;border&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;},&quot;shadow&quot;:[{&quot;hOffset&quot;:&quot;0px&quot;,&quot;vOffset&quot;:&quot;0px&quot;,&quot;blur&quot;:&quot;0px&quot;,&quot;spread&quot;:&quot;0px&quot;,&quot;color&quot;:&quot;rgba(0, 0, 0, 0.16)&quot;,&quot;isInset&quot;:false}],&quot;alignment&quot;:&quot;center&quot;,&quot;copyTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:13,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;clipBoardColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#00000024&quot;},&quot;copyIconSize&quot;:{&quot;desktop&quot;:24,&quot;tablet&quot;:20,&quot;mobile&quot;:18},&quot;copyBtnPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;headerPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;14px&quot;,&quot;left&quot;:&quot;14px&quot;,&quot;bottom&quot;:&quot;14px&quot;,&quot;right&quot;:&quot;14px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;12px&quot;,&quot;left&quot;:&quot;12px&quot;,&quot;bottom&quot;:&quot;12px&quot;,&quot;right&quot;:&quot;12px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;10px&quot;,&quot;left&quot;:&quot;10px&quot;,&quot;bottom&quot;:&quot;10px&quot;,&quot;right&quot;:&quot;10px&quot;}},&quot;copyBtnPosition&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;}},&quot;outputOption&quot;:false,&quot;outputText&quot;:&quot;Hello Word !&quot;,&quot;outputColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#2F446E&quot;},&quot;outputTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:21,&quot;tablet&quot;:17,&quot;mobile&quot;:13},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;outputPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;8px&quot;,&quot;left&quot;:&quot;8px&quot;,&quot;bottom&quot;:&quot;8px&quot;,&quot;right&quot;:&quot;8px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;7px&quot;,&quot;left&quot;:&quot;7px&quot;,&quot;bottom&quot;:&quot;7px&quot;,&quot;right&quot;:&quot;7px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;5px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;outputBorder&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;,&quot;radius&quot;:&quot;7px&quot;}}'\n>\n    <pre id=\"codeHightLight\" style=\"display:none;\">\n        &quot;import requests\\n\\nclass GeoServerClient:\\n    def __init__(self, base_url, username, password):\\n        self.base_url = base_url.rstrip(\\&quot;\\\/\\&quot;)\\n        self.session  = requests.Session()\\n        self.session.auth = (username, password)\\n\\n    def wms_url(self, workspace):\\n        return f\\&quot;{self.base_url}\\\/{workspace}\\\/wms\\&quot;\\n\\n    def get_features(self, workspace, layer, cql_filter=None,        max_features=500):\\n        params = {\\n            \\&quot;service\\&quot;:      \\&quot;WFS\\&quot;,\\n            \\&quot;version\\&quot;:      \\&quot;2.0.0\\&quot;,\\n            \\&quot;request\\&quot;:      \\&quot;GetFeature\\&quot;\\n            \\&quot;typeName\\&quot;:     f\\&quot;{workspace}:{layer}\\&quot;,\\n            \\&quot;outputFormat\\&quot;: \\&quot;application\\\/json\\&quot;,\\n            \\&quot;count\\&quot;:        max_features,\\n        }\\n        \\n        if cql_filter:\\n            params[\\&quot;CQL_FILTER\\&quot;] = cql_filter\\n\\n        resp = self.session.get(\\n            f\\&quot;{self.base_url}\\\/{workspace}\\\/wfs\\&quot;,\\n            params=params,\\n            timeout=30  \\n        )\\n\\n        resp.raise_for_status()\\n        return resp.json()&quot;    <\/pre>\n<\/div>\n\n\n\t\t \n\n\n<p class=\"wp-block-paragraph\">With the client class in place, the Flask routes that expose GeoServer data to the browser are straightforward. The route handlers read configuration values, instantiate the client, and return the data. The client handles all the GeoServer-specific URL construction and authentication.<\/p>\n\n\n<div \n    class=\"align wp-block-bch-code-highlight\" \n    id=\"bhcCodeHighlight-4\" \n    data-attributes='{&quot;cId&quot;:&quot;9aafb1bd-b&quot;,&quot;language&quot;:&quot;python&quot;,&quot;lineNumbers&quot;:false,&quot;theme&quot;:&quot;porple&quot;,&quot;headerPart&quot;:true,&quot;codeTypo&quot;:{&quot;desktop&quot;:15,&quot;tablet&quot;:15,&quot;mobile&quot;:14},&quot;codeBg&quot;:{&quot;color&quot;:&quot;#292c36&quot;},&quot;align&quot;:&quot;&quot;,&quot;copyBtnOption&quot;:&quot;Text&quot;,&quot;headingTitleTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:18,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;,&quot;textTransform&quot;:&quot;uppercase&quot;},&quot;clipBoard&quot;:true,&quot;wordWrap&quot;:true,&quot;width&quot;:{&quot;desktop&quot;:&quot;100%&quot;,&quot;tablet&quot;:&quot;100%&quot;,&quot;mobile&quot;:&quot;100%&quot;},&quot;height&quot;:{&quot;desktop&quot;:&quot;0px&quot;,&quot;tablet&quot;:&quot;0px&quot;,&quot;mobile&quot;:&quot;0px&quot;},&quot;padding&quot;:{&quot;top&quot;:&quot;0px&quot;,&quot;right&quot;:&quot;0px&quot;,&quot;bottom&quot;:&quot;0px&quot;,&quot;left&quot;:&quot;0px&quot;},&quot;background&quot;:{&quot;color&quot;:&quot;#d3cfcf42&quot;},&quot;headerBg&quot;:{&quot;color&quot;:&quot;#2f446e&quot;},&quot;headerTextColor&quot;:{&quot;color&quot;:&quot;#ffff&quot;},&quot;headerThemeOpt&quot;:&quot;one&quot;,&quot;layout&quot;:{&quot;align&quot;:&quot;left&quot;},&quot;border&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;},&quot;shadow&quot;:[{&quot;hOffset&quot;:&quot;0px&quot;,&quot;vOffset&quot;:&quot;0px&quot;,&quot;blur&quot;:&quot;0px&quot;,&quot;spread&quot;:&quot;0px&quot;,&quot;color&quot;:&quot;rgba(0, 0, 0, 0.16)&quot;,&quot;isInset&quot;:false}],&quot;alignment&quot;:&quot;center&quot;,&quot;copyTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:13,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;clipBoardColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#00000024&quot;},&quot;copyIconSize&quot;:{&quot;desktop&quot;:24,&quot;tablet&quot;:20,&quot;mobile&quot;:18},&quot;copyBtnPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;headerPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;14px&quot;,&quot;left&quot;:&quot;14px&quot;,&quot;bottom&quot;:&quot;14px&quot;,&quot;right&quot;:&quot;14px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;12px&quot;,&quot;left&quot;:&quot;12px&quot;,&quot;bottom&quot;:&quot;12px&quot;,&quot;right&quot;:&quot;12px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;10px&quot;,&quot;left&quot;:&quot;10px&quot;,&quot;bottom&quot;:&quot;10px&quot;,&quot;right&quot;:&quot;10px&quot;}},&quot;copyBtnPosition&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;}},&quot;outputOption&quot;:false,&quot;outputText&quot;:&quot;Hello Word !&quot;,&quot;outputColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#2F446E&quot;},&quot;outputTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:21,&quot;tablet&quot;:17,&quot;mobile&quot;:13},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;outputPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;8px&quot;,&quot;left&quot;:&quot;8px&quot;,&quot;bottom&quot;:&quot;8px&quot;,&quot;right&quot;:&quot;8px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;7px&quot;,&quot;left&quot;:&quot;7px&quot;,&quot;bottom&quot;:&quot;7px&quot;,&quot;right&quot;:&quot;7px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;5px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;outputBorder&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;,&quot;radius&quot;:&quot;7px&quot;}}'\n>\n    <pre id=\"codeHightLight\" style=\"display:none;\">\n        &quot;from flask import Blueprint, jsonify, request, current_app\\nfrom geoserver_client import GeoServerClient\\n\\nmaps_bp = Blueprint(\\&quot;maps\\&quot;, __name__)\\n\\ndef get_geoserver_client():\\n\\n    return GeoServerClient(\\n        base_url = current_app.config[\\&quot;GEOSERVER_URL\\&quot;],\\n        username = current_app.config[\\&quot;GEOSERVER_USER\\&quot;],\\n        password = current_app.config[\\&quot;GEOSERVER_PASS\\&quot;],\\n    )\\n\\n@maps_bp.route(\\&quot;\\\/api\\\/wms-config\\&quot;)\\ndef wms_config():\\n\\n    gs = get_geoserver_client()\\n    return jsonify({\\n        \\&quot;url\\&quot;:   gs.wms_url(\\&quot;myproject\\&quot;),\\n        \\&quot;layer\\&quot;: \\&quot;myproject:districts\\&quot;,\\n    })\\n\\n@maps_bp.route(\\&quot;\\\/api\\\/features\\&quot;)\\ndef features():\\n    gs = get_geoserver_client()\\n    geojson = gs.get_features(\\n        workspace  = \\&quot;myproject\\&quot;,\\n        layer      = \\&quot;districts\\&quot;,\\n        cql_filter = request.args.get(\\&quot;filter\\&quot;))  \\n    \\n    return jsonify(geojson)&quot;    <\/pre>\n<\/div>\n\n\n\t\t \n\n\n<pre class=\"wp-block-code\"><code>Flask Config Pattern\n\nThe GeoServer URL, username, and password should be stored in Flask's config object (loaded from environment variables), never hardcoded in source files. A&nbsp;.env&nbsp;file with&nbsp;GEOSERVER_URL=http:\/\/localhost:8080\/geoserver,&nbsp;GEOSERVER_USER=admin,&nbsp;GEOSERVER_PASS=strongpassword&nbsp;- loaded via&nbsp;python-dotenv&nbsp;- is the standard approach. This keeps credentials out of version control and makes it easy to use different GeoServer instances in development and production without changing code.<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">07.&nbsp;Rendering the Map with Leaflet<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Leaflet is a lightweight, open-source JavaScript library for interactive maps. It is the most commonly used client-side map library for Flask-based applications because it is simple, well-documented, and has native support for WMS &#8211; the protocol GeoServer uses to serve rendered map tiles.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The complete example below shows a Flask-rendered HTML template that displays a Leaflet map with three features working together: an OpenStreetMap basemap for geographic context, a GeoServer WMS overlay showing district polygons, and a click handler that queries the Flask WFS proxy to fetch and display feature information in a popup.<\/p>\n\n\n<div \n    class=\"align wp-block-bch-code-highlight\" \n    id=\"bhcCodeHighlight-5\" \n    data-attributes='{&quot;cId&quot;:&quot;72094e43-6&quot;,&quot;language&quot;:&quot;python&quot;,&quot;lineNumbers&quot;:false,&quot;theme&quot;:&quot;porple&quot;,&quot;headerPart&quot;:true,&quot;codeTypo&quot;:{&quot;desktop&quot;:15,&quot;tablet&quot;:15,&quot;mobile&quot;:14},&quot;codeBg&quot;:{&quot;color&quot;:&quot;#292c36&quot;},&quot;align&quot;:&quot;&quot;,&quot;copyBtnOption&quot;:&quot;Text&quot;,&quot;headingTitleTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:18,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;,&quot;textTransform&quot;:&quot;uppercase&quot;},&quot;clipBoard&quot;:true,&quot;wordWrap&quot;:true,&quot;width&quot;:{&quot;desktop&quot;:&quot;100%&quot;,&quot;tablet&quot;:&quot;100%&quot;,&quot;mobile&quot;:&quot;100%&quot;},&quot;height&quot;:{&quot;desktop&quot;:&quot;0px&quot;,&quot;tablet&quot;:&quot;0px&quot;,&quot;mobile&quot;:&quot;0px&quot;},&quot;padding&quot;:{&quot;top&quot;:&quot;0px&quot;,&quot;right&quot;:&quot;0px&quot;,&quot;bottom&quot;:&quot;0px&quot;,&quot;left&quot;:&quot;0px&quot;},&quot;background&quot;:{&quot;color&quot;:&quot;#d3cfcf42&quot;},&quot;headerBg&quot;:{&quot;color&quot;:&quot;#2f446e&quot;},&quot;headerTextColor&quot;:{&quot;color&quot;:&quot;#ffff&quot;},&quot;headerThemeOpt&quot;:&quot;one&quot;,&quot;layout&quot;:{&quot;align&quot;:&quot;left&quot;},&quot;border&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;},&quot;shadow&quot;:[{&quot;hOffset&quot;:&quot;0px&quot;,&quot;vOffset&quot;:&quot;0px&quot;,&quot;blur&quot;:&quot;0px&quot;,&quot;spread&quot;:&quot;0px&quot;,&quot;color&quot;:&quot;rgba(0, 0, 0, 0.16)&quot;,&quot;isInset&quot;:false}],&quot;alignment&quot;:&quot;center&quot;,&quot;copyTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:13,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;clipBoardColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#00000024&quot;},&quot;copyIconSize&quot;:{&quot;desktop&quot;:24,&quot;tablet&quot;:20,&quot;mobile&quot;:18},&quot;copyBtnPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;headerPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;14px&quot;,&quot;left&quot;:&quot;14px&quot;,&quot;bottom&quot;:&quot;14px&quot;,&quot;right&quot;:&quot;14px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;12px&quot;,&quot;left&quot;:&quot;12px&quot;,&quot;bottom&quot;:&quot;12px&quot;,&quot;right&quot;:&quot;12px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;10px&quot;,&quot;left&quot;:&quot;10px&quot;,&quot;bottom&quot;:&quot;10px&quot;,&quot;right&quot;:&quot;10px&quot;}},&quot;copyBtnPosition&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;}},&quot;outputOption&quot;:false,&quot;outputText&quot;:&quot;Hello Word !&quot;,&quot;outputColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#2F446E&quot;},&quot;outputTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:21,&quot;tablet&quot;:17,&quot;mobile&quot;:13},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;outputPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;8px&quot;,&quot;left&quot;:&quot;8px&quot;,&quot;bottom&quot;:&quot;8px&quot;,&quot;right&quot;:&quot;8px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;7px&quot;,&quot;left&quot;:&quot;7px&quot;,&quot;bottom&quot;:&quot;7px&quot;,&quot;right&quot;:&quot;7px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;5px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;outputBorder&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;,&quot;radius&quot;:&quot;7px&quot;}}'\n>\n    <pre id=\"codeHightLight\" style=\"display:none;\">\n        &quot;&lt;div id=\\&quot;map\\&quot; style=\\&quot;height:100vh\\&quot;&gt;&lt;\\\/div&gt;\\n\\n\\n\\\/\\\/ Initialise the map \\nconst map = L.map(\\&quot;map\\&quot;).setView([26.8, 80.9], 7);\\n\\n\\\/\\\/ Layer 1: OpenStreetMap basemap\\nL.tileLayer(\\&quot;https:\\\/\\\/{s}.tile.openstreetmap.org\\\/{z}\\\/{x}\\\/{y}.png\\&quot;, {\\n    attribution: \\&quot;\\u00a9 OpenStreetMap contributors\\&quot;\\n}).addTo(map);\\n\\n\\\/\\\/ Layer 2: GeoServer WMS overlay\\nL.tileLayer.wms(\\&quot;http:\\\/\\\/localhost:8080\\\/geoserver\\\/myproject\\\/wms\\&quot;, {\\n    layers:      \\&quot;myproject:districts\\&quot;, \\n    format:      \\&quot;image\\\/png\\&quot;,          \\n    transparent: true,                \\n    version:     \\&quot;1.1.1\\&quot;,            \\n    opacity:     0.7,                    \\n}).addTo(map);\\n\\n\\\/\\\/ Layer 3: Click handler to query features\\n\\nmap.on(\\&quot;click\\&quot;, async (e) =&gt; {\\n    const res = await fetch(\\n        `\\\/api\\\/features?filter=CONTAINS(geom,POINT(${e.latlng.lng} ${e.latlng.lat}))`\\n    );\\n    const data = await res.json();\\n\\n    \\\/\\\/ Check if any features were returned\\n    if (data.features?.length) {\\n        const props = data.features[0].properties;\\n        L.popup()\\n          .setLatLng(e.latlng)\\n          .setContent(`&lt;b&gt;${props.name}&lt;\\\/b&gt;&lt;br \\\/&gt;Population: ${props.population}`)\\n          .openOn(map);\\n    }\\n});\\n&quot;    <\/pre>\n<\/div>\n\n\n\t\t \n\n\n<p class=\"wp-block-paragraph\">The click handler is what makes this more than just a static map display. When a user clicks on a district, the browser sends a request to&nbsp;<code>\/api\/features<\/code>&nbsp;on the Flask backend. Flask passes the click coordinates to GeoServer as a CQL spatial filter. GeoServer finds the district polygon containing that point and returns it as GeoJSON. Flask returns the GeoJSON to the browser. Leaflet reads the feature&#8217;s properties and displays the district name and population in a popup. All three layers &#8211; Leaflet, Flask, GeoServer &#8211; are doing exactly the work they are suited for, and nothing more.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>What a Working Integration Looks Like\n\nWhen the full stack is working correctly, the browser shows an OpenStreetMap basemap with GeoServer's district polygons overlaid at 70% opacity. Clicking anywhere on the map shows a popup with the district's name and population. Zooming in causes Leaflet to request new WMS tiles from GeoServer at higher resolution. The entire experience is smooth because each layer is optimised for its role: WMS for efficient tile rendering, WFS for precise feature data retrieval.<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">08.&nbsp;Production Tips<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A GeoServer + Flask stack that works perfectly in development can run into problems when deployed to a real server. The following tips address the most common issues developers encounter when moving from a local environment to production, and explain the reasoning behind each recommendation clearly enough that the solution makes sense, not just the fix.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Use PostGIS Instead of Shapefiles for Production Data<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Shapefiles are an excellent choice during development &#8211; they are self-contained files that are easy to download, share, and test with. However, for a production application, storing geographic data in PostGIS (PostgreSQL&#8217;s geographic extension) is strongly recommended. The reasons are practical and significant.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">PostGIS supports spatial indexing, which means GeoServer can execute spatial queries &#8211; &#8220;find all districts within this bounding box&#8221; &#8211; in milliseconds rather than seconds, even on datasets with hundreds of thousands of features. PostGIS also allows data to be updated without any GeoServer configuration changes &#8211; a new row in a PostGIS table is immediately available through the GeoServer layer that reads from it, with no republishing required. With shapefiles, updating data requires replacing the file on disk and potentially reconfiguring the data store.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Put Nginx in Front of GeoServer<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GeoServer listens on port 8080 by default. Exposing port 8080 directly to the internet is inadvisable for several reasons. The non-standard port number means that URLs are ugly and non-standard. More importantly, GeoServer&#8217;s admin panel and REST API &#8211; which can modify or delete all configuration &#8211; are accessible to anyone who can reach port 8080.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The correct production setup places Nginx in front of GeoServer as a reverse proxy. Nginx listens on port 80 (or 443 for HTTPS), forwards requests to GeoServer on port 8080, and adds a critical security layer in the process. The admin panel and REST API can be restricted to localhost-only access at the Nginx level, meaning no external request can reach them regardless of GeoServer&#8217;s own security settings. Additionally, Nginx can cache WMS tile responses, which dramatically reduces GeoServer&#8217;s load on map dashboards that repeatedly display the same geographic area.<\/p>\n\n\n<div \n    class=\"align wp-block-bch-code-highlight\" \n    id=\"bhcCodeHighlight-6\" \n    data-attributes='{&quot;cId&quot;:&quot;08a53e4e-e&quot;,&quot;language&quot;:&quot;python&quot;,&quot;lineNumbers&quot;:false,&quot;theme&quot;:&quot;porple&quot;,&quot;headerPart&quot;:true,&quot;codeTypo&quot;:{&quot;desktop&quot;:15,&quot;tablet&quot;:15,&quot;mobile&quot;:14},&quot;codeBg&quot;:{&quot;color&quot;:&quot;#292c36&quot;},&quot;align&quot;:&quot;&quot;,&quot;copyBtnOption&quot;:&quot;Text&quot;,&quot;headingTitleTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:18,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;,&quot;textTransform&quot;:&quot;uppercase&quot;},&quot;clipBoard&quot;:true,&quot;wordWrap&quot;:true,&quot;width&quot;:{&quot;desktop&quot;:&quot;100%&quot;,&quot;tablet&quot;:&quot;100%&quot;,&quot;mobile&quot;:&quot;100%&quot;},&quot;height&quot;:{&quot;desktop&quot;:&quot;0px&quot;,&quot;tablet&quot;:&quot;0px&quot;,&quot;mobile&quot;:&quot;0px&quot;},&quot;padding&quot;:{&quot;top&quot;:&quot;0px&quot;,&quot;right&quot;:&quot;0px&quot;,&quot;bottom&quot;:&quot;0px&quot;,&quot;left&quot;:&quot;0px&quot;},&quot;background&quot;:{&quot;color&quot;:&quot;#d3cfcf42&quot;},&quot;headerBg&quot;:{&quot;color&quot;:&quot;#2f446e&quot;},&quot;headerTextColor&quot;:{&quot;color&quot;:&quot;#ffff&quot;},&quot;headerThemeOpt&quot;:&quot;one&quot;,&quot;layout&quot;:{&quot;align&quot;:&quot;left&quot;},&quot;border&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;},&quot;shadow&quot;:[{&quot;hOffset&quot;:&quot;0px&quot;,&quot;vOffset&quot;:&quot;0px&quot;,&quot;blur&quot;:&quot;0px&quot;,&quot;spread&quot;:&quot;0px&quot;,&quot;color&quot;:&quot;rgba(0, 0, 0, 0.16)&quot;,&quot;isInset&quot;:false}],&quot;alignment&quot;:&quot;center&quot;,&quot;copyTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:13,&quot;tablet&quot;:13,&quot;mobile&quot;:11},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;clipBoardColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#00000024&quot;},&quot;copyIconSize&quot;:{&quot;desktop&quot;:24,&quot;tablet&quot;:20,&quot;mobile&quot;:18},&quot;copyBtnPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;2px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;2px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;headerPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;14px&quot;,&quot;left&quot;:&quot;14px&quot;,&quot;bottom&quot;:&quot;14px&quot;,&quot;right&quot;:&quot;14px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;12px&quot;,&quot;left&quot;:&quot;12px&quot;,&quot;bottom&quot;:&quot;12px&quot;,&quot;right&quot;:&quot;12px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;10px&quot;,&quot;left&quot;:&quot;10px&quot;,&quot;bottom&quot;:&quot;10px&quot;,&quot;right&quot;:&quot;10px&quot;}},&quot;copyBtnPosition&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;right&quot;:&quot;15px&quot;}},&quot;outputOption&quot;:false,&quot;outputText&quot;:&quot;Hello Word !&quot;,&quot;outputColors&quot;:{&quot;color&quot;:&quot;#fff&quot;,&quot;bg&quot;:&quot;#2F446E&quot;},&quot;outputTypo&quot;:{&quot;fontWeight&quot;:400,&quot;fontSize&quot;:{&quot;desktop&quot;:21,&quot;tablet&quot;:17,&quot;mobile&quot;:13},&quot;lineHeight&quot;:1.5,&quot;fontFamily&quot;:&quot;Aleo&quot;},&quot;outputPadding&quot;:{&quot;desktop&quot;:{&quot;top&quot;:&quot;8px&quot;,&quot;left&quot;:&quot;8px&quot;,&quot;bottom&quot;:&quot;8px&quot;,&quot;right&quot;:&quot;8px&quot;},&quot;tablet&quot;:{&quot;top&quot;:&quot;7px&quot;,&quot;left&quot;:&quot;7px&quot;,&quot;bottom&quot;:&quot;7px&quot;,&quot;right&quot;:&quot;7px&quot;},&quot;mobile&quot;:{&quot;top&quot;:&quot;5px&quot;,&quot;left&quot;:&quot;5px&quot;,&quot;bottom&quot;:&quot;5px&quot;,&quot;right&quot;:&quot;5px&quot;}},&quot;outputBorder&quot;:{&quot;color&quot;:&quot;#000&quot;,&quot;style&quot;:&quot;solid&quot;,&quot;width&quot;:&quot;0px&quot;,&quot;radius&quot;:&quot;7px&quot;}}'\n>\n    <pre id=\"codeHightLight\" style=\"display:none;\">\n        &quot;# Define a proxy cache zone for WMS tile caching\\n# 10m = 10 megabytes of cache metadata storage\\nproxy_cache_path \\\/tmp\\\/nginx_wms_cache levels=1:2\\n                 keys_zone=wms_cache:10m max_size=1g\\n                 inactive=60m use_temp_path=off;\\n\\nserver {\\n    listen 80;\\n    server_name yourdomain.com;\\n\\n    # \\u2500\\u2500 Block the admin panel from external access \\u2500\\u2500\\n    # Only localhost can reach \\\/web\\\/\\n    location \\\/geoserver\\\/web\\\/ {\\n        allow  127.0.0.1;\\n        deny   all;\\n        proxy_pass http:\\\/\\\/localhost:8080;\\n    }\\n\\n    # \\u2500\\u2500 Block the REST API from external access \\u2500\\u2500\\n    # The REST API can create\\\/delete workspaces and change credentials\\n    location \\\/geoserver\\\/rest\\\/ {\\n        allow  127.0.0.1;\\n        deny   all;\\n        proxy_pass http:\\\/\\\/localhost:8080;\\n    }\\n\\n    # \\u2500\\u2500 Proxy all other GeoServer requests (WMS, WFS) with tile caching \\u2500\\u2500\\n    location \\\/geoserver\\\/ {\\n        proxy_pass         http:\\\/\\\/localhost:8080;\\n        proxy_set_header   Host              $host;\\n        proxy_set_header   X-Real-IP         $remote_addr;\\n        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;\\n\\n        # Cache WMS GetMap responses for 1 hour\\n        # Map tiles rarely change \\u2014 caching cuts GeoServer load by 60-80%\\n        proxy_cache        wms_cache;\\n        proxy_cache_valid  200 1h;\\n        proxy_cache_key    \\&quot;$request_uri\\&quot;;\\n    }\\n}&quot;    <\/pre>\n<\/div>\n\n\n\t\t \n\n\n<p class=\"wp-block-paragraph\"><strong>Lock Down the GeoServer REST API<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The GeoServer REST API is extremely powerful &#8211; it can create and delete workspaces, modify layer configurations, change admin credentials, and publish or unpublish data. If it is accessible from the internet without authentication, it represents a significant security vulnerability. The Nginx configuration above restricts the REST API to localhost, but it is also worth reviewing GeoServer&#8217;s own REST API security settings under Security \u2192 Services in the admin panel to ensure API access requires authentication even from localhost.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Before Deploying to Production\n\nRun through this checklist before exposing a GeoServer instance to the internet: the default admin password has been changed; the GeoServer data directory is persisted (either as a Docker volume or as a directory on a server with backups); Nginx is in front of GeoServer; port 8080 is blocked at the firewall level; the admin panel and REST API are restricted to localhost in Nginx; and GeoServer's demo layers have been deleted or secured (they can expose the layer listing to anonymous users).<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code><em>The first time a developer sees their own geographic data rendered as a real, interactive map \u2014 polygons, click-to-identify popups, live spatial filters \u2014 it genuinely feels like something magical. GeoServer is the infrastructure that makes that magic reliable, repeatable, and production-ready.<\/em><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Web mapping has a steep initial learning curve because several unfamiliar standards &#8211; WMS, WFS, CRS, OGC &#8211; all need to be understood simultaneously. But each concept is well-defined, and once each one clicks into place, the whole system becomes coherent. GeoServer&#8217;s role is clear. Flask&#8217;s role is clear. Leaflet&#8217;s role is clear. The protocols that connect them are standard, documented, and supported by every geospatial tool in existence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The patterns shown in this guide &#8211; the client class, the proxy routes, the WMS tile layer, the WFS click handler &#8211; are the same patterns used in production applications serving millions of map requests. They do not need to be reinvented for each project. Start with them, understand what each part does, and build from there.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Building a web application that displays geographic maps is one of those tasks that feels deceptively simple at first &#8211; until a developer realises they have no idea how their geographic data (a shapefile, a PostGIS table, a GeoTIFF) is actually supposed to reach the browser as a rendered, zoomable, clickable map. A Flask backend [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":170,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[16],"tags":[],"class_list":["post-130","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development"],"_links":{"self":[{"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/posts\/130","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/comments?post=130"}],"version-history":[{"count":20,"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/posts\/130\/revisions"}],"predecessor-version":[{"id":191,"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/posts\/130\/revisions\/191"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/media\/170"}],"wp:attachment":[{"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/media?parent=130"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/categories?post=130"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.bharatstacks.com\/index.php\/wp-json\/wp\/v2\/tags?post=130"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}