Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/auto_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
- name: Install deps
run: |
cd server
sudo apt-get update
sudo apt-get -y install libsqlite3-mod-spatialite
pip install pipenv==2026.8.0
pipenv install --dev --deploy --verbose --python 3.12
Expand Down
41 changes: 41 additions & 0 deletions deployment/common/ssl-proxy.conf
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,45 @@ server {
proxy_redirect off;
proxy_pass http://app_server;
}

# Enterprise edition maps locations
# Embeddable map view - allow framing from any origin
location ^~ /maps/embed/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://app_server;

proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors *;" always;
# add_header here disables inheritance of server-level headers, so repeat them
add_header X-Robots-Tag "none";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

# OGC API endpoints - allow cross-origin requests from any origin
location ^~ /v2/ogc/ {
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Range" always;
add_header Access-Control-Max-Age 5 always;
# add_header here disables inheritance of server-level headers, so repeat them
add_header X-Robots-Tag "none";
add_header Content-Security-Policy "frame-ancestors 'self';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

if ($request_method = OPTIONS) {
return 204;
}

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app_server;
}
}
4 changes: 0 additions & 4 deletions deployment/community/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,6 @@ GLOBAL_STORAGE=10737418240
# Gunicorn server socket
PORT=5000

GEVENT_WORKER=True
# Deprecated from 2024.7.0, replacement is to set GEVENT_WORKER=True
NO_MONKEY_PATCH=False

# Diagnostic logs

DIAGNOSTIC_LOGS_URL=
Expand Down
30 changes: 27 additions & 3 deletions deployment/enterprise/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ MAIL_SERVER=fixme
#MAIL_USERNAME=NODEFAULT
MAIL_USERNAME=fixme

#EMAIL_TEMPLATES_DIR=

# data sync

Expand Down Expand Up @@ -187,6 +188,9 @@ ACCOUNT_EXPIRATION=1
#MERGIN_LOGO_URL= # for link to logo in emails
MERGIN_LOGO_URL=https://merginmaps.com/MM_logo_HORIZ_COLOR_TRANSPARENT_no_padding.png

#DASHBOARD_LOGO_URL for logo in web dashboard (default is same as MERGIN_LOGO_URL)
#DASHBOARD_LOGO_URL=$MERGIN_LOGO_URL

# global workspace related bits - ignored in non-CE versions
# GLOBAL_WORKSPACE mergin

Expand Down Expand Up @@ -217,16 +221,29 @@ MERGIN_LOGO_URL=https://merginmaps.com/MM_logo_HORIZ_COLOR_TRANSPARENT_no_paddin

MAPS_ENABLED=True

# Enable OGC API for projects
OGC_API_ENABLED=True

# Enable sharing of maps with public link
MAPS_SHARING_ENABLED=True

OVERVIEW_DATA=/overviews

VECTOR_TILES_URL=https://tiles-ee.merginmaps.com/data/default/{z}/{x}/{y}.pbf
VECTOR_TILES_URL=https://tiles.merginmaps.com/data/default/{z}/{x}/{y}.pbf

VECTOR_TILES_STYLE_URL=https://tiles.merginmaps.com/styles/default.json

VECTOR_TILES_LIGHT_STYLE_URL=https://tiles.merginmaps.com/styles/light.json

VECTOR_TILES_STYLE_URL=https://tiles-ee.merginmaps.com//styles/default.json
VECTOR_TILES_DARK_STYLE_URL=https://tiles.merginmaps.com/styles/dark.json

#WMTS_SERVER_URL=http://mergin-qgis-nginx:80

#OVERVIEW_MAX_FILE_SIZE=1048576 # 1MB

# If patch geopackage file journal mode
#MAPS_SET_JOURNAL_MODE=True

### Diagnostic logs from Mobile and QGIS Plugin
DIAGNOSTIC_LOGS_DIR=/diagnostic_logs

Expand Down Expand Up @@ -254,5 +271,12 @@ SSO_SERVER_URL=http://localhost:8081
# Default value is mergin-maps-product string
# SSO_SERVER_PRODUCT_ID=mergin-maps-product

### Audit logs ###########################################################################################################

AUDIT_LOGS_ENABLED=True

### Telemetry ############################################################################################################
OTEL_ENABLED=0
OTEL_ENABLED=0

### Caching ############################################################################################################
REDIS_URL=redis://merginmaps-redis:6379/1
2 changes: 1 addition & 1 deletion deployment/enterprise/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ docker compose -f docker-compose.maps.yml up -d

## Running telemetry stack

Running telemetry is optional and it is disabled by default. Mergimaps supports OpenTelemetry standard. Check [Readme](deployment/common/README.md) for example of how to run telemetry stack locally.
Running telemetry is optional and it is disabled by default. Mergimaps supports OpenTelemetry standard. Check [README](/deployment/common/README.md) for example of how to run telemetry stack locally.

To enable telemetry in Mergin Maps, please set `OTEL_ENABLED=1` in `.prod.env`. In case of custom telemetry deployment also check [.otel.env](deployment/common/telemetry/.otel.env) file for more details on how to send data to your telemetry stack.
8 changes: 5 additions & 3 deletions deployment/enterprise/docker-compose.maps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ networks:
services:
qgis:
container_name: mergin-qgis
image: lutraconsulting/qgis-server:2025.1.0
image: lutraconsulting/qgis-server:2026.4.0
user: 1000:999
networks:
- mergin-net
Expand All @@ -16,11 +16,13 @@ services:
- QGIS_SERVER_WMS_MAX_HEIGHT=1536
- QGIS_SERVER_WMS_MAX_WIDTH=1536
- QGIS_SERVER_LOG_LEVEL=2
- QGIS_SERVER_SERVICE_URL=http://localhost:5555
- QGIS_SERVER_API_WFS3_ROOT_PATH=/wfs3
volumes:
- ./map_data:/overviews
qgis_nginx:
container_name: mergin-qgis-nginx
image: nginxinc/nginx-unprivileged:1.27
image: nginxinc/nginx-unprivileged:1.30
user: 101:999
networks:
- mergin-net
Expand All @@ -30,7 +32,7 @@ services:
- ./qgis_nginx.conf:/etc/nginx/conf.d/default.conf
qgis_extractor:
container_name: mergin-qgis-extractor
image: lutraconsulting/qgis-extractor:2025.3.1
image: lutraconsulting/qgis-extractor:2026.4.1
user: 901:999
networks:
- mergin-net
Expand Down
12 changes: 6 additions & 6 deletions deployment/enterprise/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ networks:

services:
server:
image: lutraconsulting/merginmaps-backend-ee:2025.7.3
image: lutraconsulting/merginmaps-backend-ee:2026.10.1
container_name: merginmaps-server
restart: always
user: 901:999
Expand All @@ -25,7 +25,7 @@ services:
networks:
- mergin
web:
image: lutraconsulting/merginmaps-frontend-ee:2025.7.3
image: lutraconsulting/merginmaps-frontend-ee:2026.10.1
container_name: merginmaps-web
restart: always
depends_on:
Expand All @@ -35,7 +35,7 @@ services:
networks:
- mergin
proxy:
image: nginxinc/nginx-unprivileged:1.27
image: nginxinc/nginx-unprivileged:1.30
container_name: merginmaps-proxy
restart: always
# run nginx as built-in user but with group mergin-family for files permissions
Expand All @@ -55,7 +55,7 @@ services:
- server

celery-beat:
image: lutraconsulting/merginmaps-backend-ee:2025.7.3
image: lutraconsulting/merginmaps-backend-ee:2026.10.1
container_name: merginmaps-celery-beat
restart: always
user: 901:999
Expand All @@ -74,7 +74,7 @@ services:
- mergin

celery-worker:
image: lutraconsulting/merginmaps-backend-ee:2025.7.3
image: lutraconsulting/merginmaps-backend-ee:2026.10.1
container_name: merginmaps-celery-worker
restart: always
user: 901:999
Expand All @@ -98,7 +98,7 @@ services:
networks:
- mergin
db:
image: postgres:14
image: postgres:17
container_name: merginmaps-db
restart: always
volumes:
Expand Down
1 change: 1 addition & 0 deletions server/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# Please monkey-patch earlier. See https://github.com/gevent/gevent/issues/1016.
# Modules that had direct imports (NOT patched): ['urllib3.util, 'urllib3.util.ssl']"
# which comes from using requests (its deps) lib in webhooks
# For gunicorn, GEVENT_WORKER is derived from worker class (see on_starting hook in config.py).

from mergin.config import Configuration as MainConfig

Expand Down
33 changes: 28 additions & 5 deletions server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,37 @@ def format(self, record):
return msg


def post_fork(server, worker):
def on_starting(server):
"""
Set GEVENT_WORKER (read by workers forked later) from the actual worker class, including command-line overrides,
so that the app gets gevent specific setup (early monkey-patching, green psycopg, request timeout) without
having to configure it.
"""
import os
from decouple import strtobool
from gunicorn.workers.ggevent import GeventWorker

gevent_worker = issubclass(server.worker_class, GeventWorker)
configured = os.environ.get("GEVENT_WORKER")
if configured is not None and bool(strtobool(configured)) != gevent_worker:
server.log.warning(
"Ignoring GEVENT_WORKER=%s, it is derived from worker class %s",
configured,
server.cfg.worker_class_str,
)
os.environ["GEVENT_WORKER"] = str(gevent_worker)


def post_worker_init(worker):
"""
Runs in the worker after gevent monkey-patching and app loading.
Do not load the app in post_fork, it would import ssl (via requests/urllib3) before it is patched
and outgoing HTTPS requests would fail with RecursionError.
"""
from mergin.config import Configuration
from mergin.otel.instrument import instrument_flask_app

server.log.info("Worker spawned (pid: %s)", worker.pid)
flask_app = worker.app.wsgi()

# We initialize the OTel SDK here, inside the child process.
# This ensures each worker gets its own fresh background exporter thread.
if Configuration.OTEL_ENABLED:
instrument_flask_app(flask_app)
instrument_flask_app(worker.wsgi)
7 changes: 4 additions & 3 deletions server/mergin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,11 @@ class Configuration(object):
VERSION = config("VERSION", default=get_version())
SERVER_TYPE = config("SERVER_TYPE", default="ce")

# whether to run flask app with gevent worker type in gunicorn
# using gevent type of worker impose some requirements on code, e.g. to be greenlet safe, custom timeouts
# whether app runs under gevent (it does not select worker type), enables monkey-patching, green psycopg
# and request timeout middleware; for gunicorn it is set automatically from worker class (see on_starting hook
# in gunicorn config.py)
GEVENT_WORKER = config("GEVENT_WORKER", default=False, cast=bool)
GEVENT_REQUEST_TIMEOUT = config("GEVENT_REQUEST_TIMEOUT", default=30, cast=int)
GEVENT_REQUEST_TIMEOUT = config("GEVENT_REQUEST_TIMEOUT", default=50, cast=int)
DIAGNOSTIC_LOGS_URL = config(
"DIAGNOSTIC_LOGS_URL",
default="",
Expand Down
6 changes: 3 additions & 3 deletions server/mergin/otel/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def get_description(self) -> str:

def setup_otel_provider():
"""
Initializes the Tracer Engine. Call this Gunicorn in post_fork or in Celery worker_process_init.
Initializes the Tracer Engine. Call this in Gunicorn post_worker_init or in Celery worker_process_init.
"""
resource = create_resource()

Expand All @@ -87,7 +87,7 @@ def setup_otel_provider():


def setup_otel_metrics():
"""Initializes the Metrics Engine. Call this in post_fork."""
"""Initializes the Metrics Engine. Call this in Gunicorn post_worker_init."""
resource = create_resource()
exporter = OTLPMetricExporter(
endpoint=Configuration.OTEL_EXPORTER_OTLP_ENDPOINT, insecure=True
Expand All @@ -109,7 +109,7 @@ def setup_otel_logging():


def instrument_flask_app(app):
"""Initializes the Flask app with OTel hooks. Call this in post_fork."""
"""Initializes the Flask app with OTel hooks. Call this in Gunicorn post_worker_init."""
setup_otel_provider()
setup_otel_metrics()
# setup_otel_logging()
Expand Down
2 changes: 1 addition & 1 deletion server/mergin/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@


def get_version():
return "2026.5.0"
return "2026.10.1"
2 changes: 1 addition & 1 deletion server/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

setup(
name="mergin",
version="2026.5.0",
version="2026.10.1",
url="https://github.com/MerginMaps/mergin",
license="AGPL-3.0-only",
author="Lutra Consulting Limited",
Expand Down
Loading