Configure QGIS Environment Variables and Profiles

The same script can behave differently on two machines for reasons that have nothing to do with the code: a different set of installed plugins, a CRS default in the user's settings, a proxy configured in one place and not the other, PROJ finding different datum grids. QGIS draws its behaviour from two places outside the script — the user profile, which holds settings, plugins and styles, and the environment, which tells QGIS where its libraries and data are and how to run. Controlling both is what makes scripts reproducible on desktops, servers and in CI.

This recipe belongs to Headless QGIS & Server Automation. It explains user profiles and how to create and select them, the environment variables that matter for PyQGIS, startup scripts per profile, and how to give servers and containers a clean, known configuration.

Where QGIS behaviour comes fromThree sources shape a QGIS process. Environment variables, set before start, locate libraries and data and choose the display platform: QGIS_PREFIX_PATH, QT_QPA_PLATFORM, PROJ_DATA, GDAL options. The user profile holds settings, plugins, styles, authentication and the startup script. The script itself sets anything else in code. Reproducible runs pin the first two.Environment, profile, then codeenvironmentQGIS_PREFIX_PATHQT_QPA_PLATFORMPROJ_DATA · GDAL_*set before startuser profilesettings (QGIS3.ini)plugins, stylesauth, startup.pyper profile folderscriptQgsApplication(…)QgsSettingsexplicit in code

Prerequisites

  • QGIS 3.34 LTR or newer, or the QGIS 4 series.
  • Access to the shell or service configuration where scripts are launched — a terminal, a systemd unit, a scheduled task, a Dockerfile.

Understand user profiles

A profile is a folder containing everything QGIS stores per user: QGIS/QGIS3.ini with settings, python/plugins with installed plugins, processing with scripts and models, qgis-auth.db with credentials, symbology-style.db with styles. Profiles live under the QGIS settings directory; their location differs by platform.

from qgis.core import QgsApplication
from pathlib import Path

print("active profile folder:", QgsApplication.qgisSettingsDirPath())
profiles_root = Path(QgsApplication.qgisSettingsDirPath()).parent
print("profiles:", [p.name for p in profiles_root.iterdir() if p.is_dir()])

Breakdown: qgisSettingsDirPath returns the active profile's folder, and its parent holds all profiles. Separate profiles let one person keep a clean configuration for production work next to an experimental one with development plugins, or give a shared machine one profile per task. Because plugins and settings are per profile, a script run "in QGIS" can behave differently depending on which profile was active — a common cause of "it works on my machine".

Start QGIS with a specific profile

Desktop QGIS and standalone scripts can both be pointed at a profile explicitly.

Choosing a profileQGIS Desktop accepts --profile and --profiles-path on the command line to start with a named profile in a chosen folder. Standalone scripts pass a profile folder to QgsApplication, or set QGIS_CUSTOM_CONFIG_PATH, so that settings, plugins and authentication come from that folder instead of the user's default profile.Desktop flags, or a settings path in codeQGIS Desktopqgis --profile automation--profiles-path /opt/qgis-profilesstandalone scriptQgsApplication([], False, '/opt/qgis-profiles/automation')

# desktop: a named profile from a shared folder
qgis --profiles-path /opt/qgis-profiles --profile automation

# headless script: point QGIS at a dedicated configuration folder
export QGIS_CUSTOM_CONFIG_PATH=/opt/qgis-profiles/automation
python3 /opt/scripts/nightly_export.py
from qgis.core import QgsApplication
qgs = QgsApplication([], False, "/opt/qgis-profiles/automation")
qgs.initQgis()
print(QgsApplication.qgisSettingsDirPath())

Breakdown: On the desktop, --profiles-path chooses the folder that contains profiles and --profile the profile inside it, so a shortcut or launcher can start QGIS in a known configuration. For scripts, the third argument of the QgsApplication constructor sets the configuration path directly; QGIS_CUSTOM_CONFIG_PATH does the same through the environment. Pointing automation at its own profile isolates it from whatever plugins and settings the interactive user has.

Set the environment variables that matter

A handful of environment variables decide whether a standalone PyQGIS process starts at all and how it behaves.

# where QGIS is installed (prefix containing lib/ and share/qgis/)
export QGIS_PREFIX_PATH=/usr
# headless Qt: no display needed
export QT_QPA_PLATFORM=offscreen
# PROJ data and grids, shared and pinned
export PROJ_DATA=/opt/proj-data:/usr/share/proj
# GDAL behaviour: cache size, network timeouts, no AUX files beside rasters
export GDAL_CACHEMAX=1024
export GDAL_HTTP_TIMEOUT=60
export GDAL_PAM_ENABLED=NO
# Python path to the QGIS bindings (Linux system install)
export PYTHONPATH=/usr/share/qgis/python:$PYTHONPATH

Breakdown: QGIS_PREFIX_PATH tells QgsApplication where providers and resources live; scripts can also call QgsApplication.setPrefixPath in code. QT_QPA_PLATFORM=offscreen lets QGIS start without a display server — essential on servers, in cron jobs and in containers. PROJ_DATA pins the coordinate transformation data and grids, so results agree across machines, as explained in installing PROJ datum grids. GDAL variables tune caching, network behaviour and sidecar files. On Windows, the OSGeo4W environment batch files set the equivalents.

Use a startup script per profile

Each profile can run a startup.py when QGIS starts — the place for project-independent setup such as registering custom expression functions, setting defaults or adding paths.

Startup script orderWhen QGIS starts it initialises the application, loads the profile's settings, then runs startup.py from the profile's python folder, then loads plugins, then opens any project. Code in startup.py can therefore register expression functions and set defaults before plugins and projects need them.Before plugins, before projectsinit appload profilesettingsstartup.pyyour setuppluginsproject

# <profile>/python/startup.py
from qgis.core import QgsSettings, QgsExpressionContextUtils

s = QgsSettings()
s.setValue("app/projections/unknownCrsBehavior", "PromptUserForCrs")
s.setValue("Processing/Configuration/INVALID_GEOMETRIES", 1)   # skip invalid, do not stop
QgsExpressionContextUtils.setGlobalVariable("organisation_name", "City Planning Office")

try:
    from org_expressions import register_all          # site package with custom functions
    register_all()
except ImportError:
    pass

Breakdown: Settings written at startup apply to everything that follows in the session, so organisation-wide defaults are enforced without relying on each user's choices. Global variables set here are available to every project's expressions. Importing a shared module of custom expression functions makes them available before any project needs them. Keep startup scripts small and defensive — an exception here produces an error at every launch. The feature is covered from the user side in running Python at QGIS startup.

Provide organisation-wide defaults

Administrators often want every user's QGIS to start with the same defaults — a proxy, a default CRS, approved plugin repositories — without touching individual profiles. QGIS reads a global settings file whose values act as defaults for any setting a profile has not changed.

; qgis_global_settings.ini
[proxy]
proxyEnabled=true
proxyHost=proxy.example.org
proxyPort=8080

[Projections]
defaultProjectCrs=EPSG:25832

[app]
plugin_repositories\org\url=https://plugins.example.org/plugins.xml
plugin_repositories\org\enabled=true
export QGIS_GLOBAL_SETTINGS_FILE=/etc/qgis/qgis_global_settings.ini
qgis   # or: qgis --globalsettingsfile /etc/qgis/qgis_global_settings.ini

Breakdown: Values in the global file are defaults: a user's own setting still wins, so the file sets sensible behaviour without locking anyone in. Pointing QGIS at it with an environment variable or command-line option means it can be deployed centrally, for example in a login script or a desktop launcher. Settings keys follow the same paths as in profiles; find them in the advanced settings editor. Combined with a private plugin repository, as in hosting a private plugin repository, this gives an organisation a consistent QGIS setup on every desktop.

Make servers and containers reproducible

On servers and in CI, the goal is that every run starts from the same configuration. Baking a profile and environment into the image or service definition achieves that.

FROM qgis/qgis:release-3_40
ENV QT_QPA_PLATFORM=offscreen \
    QGIS_CUSTOM_CONFIG_PATH=/opt/qgis-profile \
    PROJ_DATA=/opt/proj-data:/usr/share/proj
COPY qgis-profile/ /opt/qgis-profile/
COPY proj-data/ /opt/proj-data/
COPY scripts/ /opt/scripts/
CMD ["python3", "/opt/scripts/nightly_export.py"]

Breakdown: A pinned QGIS image, a profile folder with exactly the settings and plugins the scripts need, and pinned PROJ data make every container run identical — regardless of the host. Keeping the profile folder in version control means configuration changes are reviewed like code. The same principles apply to a systemd service or scheduled task: set the variables in the unit file, point at a dedicated profile, and never depend on an interactive user's settings. Container details are in running PyQGIS in a Docker container.

Record the environment with every run

When results differ between runs, knowing the environment is half the diagnosis. A few lines at the start of a script log what matters.

import os, platform
from qgis.core import Qgis, QgsApplication
from osgeo import gdal

info = {
    "qgis": Qgis.version(), "python": platform.python_version(), "gdal": gdal.__version__,
    "profile": QgsApplication.qgisSettingsDirPath(),
    "qt_platform": os.environ.get("QT_QPA_PLATFORM"),
    "proj_data": os.environ.get("PROJ_DATA") or os.environ.get("PROJ_LIB"),
}
for k, v in info.items():
    print(f"{k:<12} {v}")

Breakdown: QGIS, Python and GDAL versions, the active profile and the key environment variables explain most differences between two runs. Writing this to the run log costs nothing; when a colleague reports a different result, comparing the two records usually finds the cause in seconds. Combine it with the logging setup in handling errors and logging in unattended scripts.

QGIS version compatibility

Profiles, --profile/--profiles-path, QGIS_CUSTOM_CONFIG_PATH, the QgsApplication configuration-path argument and startup.py are available on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. PROJ 9.1 and newer read PROJ_DATA, with PROJ_LIB accepted as an alias. Settings keys can change between versions; read them from the Options dialog's advanced settings editor on the version you target.

Troubleshooting

  • "Could not connect to display". QT_QPA_PLATFORM=offscreen is not set for a headless run.
  • Providers missing in a script. QGIS_PREFIX_PATH or setPrefixPath points at the wrong folder.
  • A plugin's behaviour appears in automation. The script uses the interactive user's profile; give it its own.
  • Coordinates differ between server and desktop. Different PROJ data; pin PROJ_DATA.

Conclusion

Keep automation in its own user profile, select it with --profile or a configuration path, set the prefix path, offscreen Qt platform, PROJ and GDAL variables explicitly, put organisation-wide setup in the profile's startup.py, bake profile and environment into containers and services, and log the environment with every run.

Frequently Asked Questions

Can two profiles share plugins? Not automatically; install plugins per profile, or point both at a shared plugin path with QGIS_PLUGINPATH.

Where is QGIS3.ini? In the profile folder under QGIS/QGIS3.ini; prefer QgsSettings in code to editing it directly.

Can I start QGIS with a different language? Yes — the --lang option, or the locale setting in the profile.

Does QGIS Server use profiles? No; it is configured through its own environment variables such as QGIS_SERVER_LOG_FILE.