diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000000000000000000000000000000000000..032bf20d9d7b46a9328f9411e46102a913024e9d
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+ignore = D103
+max-line-length = 120
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9e117ef9884ad2fe479010ce38b8f39864b2e012
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,25 @@
+exclude: '^$'
+fail_fast: false
+repos:
+  - repo: https://github.com/PyCQA/flake8
+    rev: 4.0.1
+    hooks:
+      - id: flake8
+        additional_dependencies: [flake8-docstrings, flake8-debugger, flake8-bugbear, mccabe]
+        args: [--max-complexity, "10"]
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.1.0
+    hooks:
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: check-yaml
+        args: [--unsafe]
+        exclude: ^chart/geosphere-mapserver/templates/.*\.yaml
+  - repo: https://github.com/scop/pre-commit-shfmt
+    rev: v3.4.2-1
+    hooks:
+      # Choose one of:
+      - id: shfmt         # native (requires Go to build)
+        args: ["-i", "4"]
+      #- id: shfmt-docker  # Docker image (requires Docker to run)
+      #
diff --git a/ci/test_mapserver_image.sh b/ci/test_mapserver_image.sh
index a5f3e0b288fcbf823db492fe0dca6d4485eac9a4..c370a5404d4da19b6b7caf0a4e716feda94af05f 100755
--- a/ci/test_mapserver_image.sh
+++ b/ci/test_mapserver_image.sh
@@ -1,11 +1,11 @@
 #!/usr/bin/env bash
 
 debug() {
-    >&2 echo "DEBUG: $@"
+    echo >&2 "DEBUG: $@"
 }
 
 error() {
-    >&2 echo "ERROR: $@"
+    echo >&2 "ERROR: $@"
     exit 1
 }
 
diff --git a/mapserver/cgi-bin/layer_times.py b/mapserver/cgi-bin/layer_times.py
index 14c42f0cffefe696fd89a494798914940d442ed7..1fe6f1962f775250477d4074caadbb4638594623 100755
--- a/mapserver/cgi-bin/layer_times.py
+++ b/mapserver/cgi-bin/layer_times.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python3
+"""CGI script to read on-disk shapefiles for available times for one layer."""
 import os
 import cgi
 import json
diff --git a/mapserver/cgi-bin/layer_times_postgres.py b/mapserver/cgi-bin/layer_times_postgres.py
index 4864e762b09cc2add50936a8aecfef4fff74837b..fad2b1a72b8882e1a869d06ce9124f0a10a45a59 100755
--- a/mapserver/cgi-bin/layer_times_postgres.py
+++ b/mapserver/cgi-bin/layer_times_postgres.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python3
+"""CGI script to read PostGIS for available times for one layer."""
 import os
 import sys
 import cgi
diff --git a/mapserver/html/index.html b/mapserver/html/index.html
index abecce665033fd17310b003767a4b7acd2cda6c4..99e8560f38556143eea3e1cde683f42bcea160db 100644
--- a/mapserver/html/index.html
+++ b/mapserver/html/index.html
@@ -18,4 +18,4 @@ parameters. Other sectors available are "radm01", "radm02", and "radc".
 <br/>
 
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/mapserver/render.py b/mapserver/render.py
index aa227b8c09dd217d90bf9bd23bab5f15f3155b7d..fd5e7d533087ec732d3928dbfe66eae2606b64d2 100644
--- a/mapserver/render.py
+++ b/mapserver/render.py
@@ -81,4 +81,4 @@ def main():
 
 
 if __name__ == "__main__":
-    sys.exit(main())
\ No newline at end of file
+    sys.exit(main())
diff --git a/mapserver/run.sh b/mapserver/run.sh
index 17de218948e84864e1e4494c63b30665cff44aa3..56f5b6aa9b83f46ec877ebcae9a0e0218e7f3e81 100755
--- a/mapserver/run.sh
+++ b/mapserver/run.sh
@@ -10,7 +10,7 @@ export WMS_SECTORS=${WMS_SECTORS:-"radm1 radm2 radc radf"}
 python3 /work/render.py $mf_tmpl "/work/mapfiles/{platform}_abi_{sector}_l1b.map"
 sed -i "s:__LAYER_BASE_DIR__:$LAYER_BASE_DIR:g" /etc/apache2/sites-available/cspp_geo.conf
 
-if [[ "${POSTGRES_HOST}" != "" ]]; then
+if [[ ${POSTGRES_HOST} != "" ]]; then
     sed -i "s:wms_times_postgres:wms_times:g" /etc/apache2/sites-available/cspp_geo.conf
     sed -i "s:__POSTGRES_HOST__:${POSTGRES_HOST}:g" /usr/lib/cgi-bin/layer_times_postgres.py
     sed -i "s:__POSTGRES_PORT__:${POSTGRES_PORT:-"5432"}:g" /usr/lib/cgi-bin/layer_times_postgres.py
diff --git a/mapserver/site-conf b/mapserver/site-conf
index 7e625acfb06f22a2f58213009a749d8563953590..5386b076ce7a835153244d34ffbc374d6fd1d471 100644
--- a/mapserver/site-conf
+++ b/mapserver/site-conf
@@ -58,4 +58,4 @@
         </IfModule>
 </VirtualHost>
 
-# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
\ No newline at end of file
+# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
diff --git a/mapserver/sql/goesr_crs.sql b/mapserver/sql/goesr_crs.sql
index e9df427a2c881bf22dc7d4684894a583311d4d38..2642c14a0f9352d50bed609fbb48c174578df180 100644
--- a/mapserver/sql/goesr_crs.sql
+++ b/mapserver/sql/goesr_crs.sql
@@ -1,2 +1,2 @@
 INSERT INTO projected_crs (auth_name, code, name, description, coordinate_system_auth_name, coordinate_system_code, geodetic_crs_auth_name, geodetic_crs_code, conversion_auth_name, conversion_code, text_definition, deprecated) VALUES ('EPSG', '930917', 'GOES-17 ABI Fixed Grid', 'GOES-17 (GOES-WEST) ABI Fixed Grid in the Geostationary projection', null, null, 'EPSG', '4269', null, null, 'PROJCRS["GOES-17 ABI Fixed Grid",BASEGEOGCRS["GOES-17 ABI Fixed Grid",DATUM["North American Datum 1983",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]],ID["EPSG",6269]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8901]]],CONVERSION["unknown",METHOD["Geostationary Satellite (Sweep X)"],PARAMETER["Longitude of natural origin",-137,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["Satellite Height",35786023,LENGTHUNIT["metre",1,ID["EPSG",9001]]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["(E)",east,ORDER[1],LENGTHUNIT["metre",1,ID["EPSG",9001]]],AXIS["(N)",north,ORDER[2],LENGTHUNIT["metre",1,ID["EPSG",9001]]]]', 0);
-INSERT INTO projected_crs (auth_name, code, name, description, coordinate_system_auth_name, coordinate_system_code, geodetic_crs_auth_name, geodetic_crs_code, conversion_auth_name, conversion_code, text_definition, deprecated) VALUES ('EPSG', '930916', 'GOES-16 ABI Fixed Grid', 'GOES-16 (GOES-WEST) ABI Fixed Grid in the Geostationary projection', null, null, 'EPSG', '4269', null, null, 'PROJCRS["GOES-16 ABI Fixed Grid",BASEGEOGCRS["GOES-16 ABI Fixed Grid",DATUM["North American Datum 1983",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]],ID["EPSG",6269]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8901]]],CONVERSION["unknown",METHOD["Geostationary Satellite (Sweep X)"],PARAMETER["Longitude of natural origin",-75,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["Satellite Height",35786023,LENGTHUNIT["metre",1,ID["EPSG",9001]]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["(E)",east,ORDER[1],LENGTHUNIT["metre",1,ID["EPSG",9001]]],AXIS["(N)",north,ORDER[2],LENGTHUNIT["metre",1,ID["EPSG",9001]]]]', 0);
\ No newline at end of file
+INSERT INTO projected_crs (auth_name, code, name, description, coordinate_system_auth_name, coordinate_system_code, geodetic_crs_auth_name, geodetic_crs_code, conversion_auth_name, conversion_code, text_definition, deprecated) VALUES ('EPSG', '930916', 'GOES-16 ABI Fixed Grid', 'GOES-16 (GOES-WEST) ABI Fixed Grid in the Geostationary projection', null, null, 'EPSG', '4269', null, null, 'PROJCRS["GOES-16 ABI Fixed Grid",BASEGEOGCRS["GOES-16 ABI Fixed Grid",DATUM["North American Datum 1983",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]],ID["EPSG",6269]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8901]]],CONVERSION["unknown",METHOD["Geostationary Satellite (Sweep X)"],PARAMETER["Longitude of natural origin",-75,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["Satellite Height",35786023,LENGTHUNIT["metre",1,ID["EPSG",9001]]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["(E)",east,ORDER[1],LENGTHUNIT["metre",1,ID["EPSG",9001]]],AXIS["(N)",north,ORDER[2],LENGTHUNIT["metre",1,ID["EPSG",9001]]]]', 0);