Configuration
Every QueryProxy environment variable — query limits, execution, result storage, trusted proxies, the application database and the queue.
Updated:
All configuration is environment variables (.env locally, container
environment in Docker). .env.example in the repository documents the full
set; this page explains the ones you’ll actually change.
QueryProxy settings
| Variable | Default | Meaning |
|---|---|---|
QUERYPROXY_SELECT_DEFAULT_LIMIT |
1000 |
LIMIT injected into SELECTs that have none |
QUERYPROXY_SELECT_HARD_LIMIT |
10000 |
Larger LIMITs are clamped down to this |
QUERYPROXY_EXECUTION_TIMEOUT |
300 |
Max seconds a single request may run on the worker |
QUERYPROXY_RESULT_DISK |
local |
Laravel filesystem disk for result files (s3 supported); a publicly visible disk is refused |
QUERYPROXY_RESULT_TTL_DAYS |
30 |
Days before results:prune deletes stored results |
QUERYPROXY_SEED_DEMO |
false |
Docker only: seed the demo team on first boot (one random password, printed once in the logs; refused under APP_ENV=production unless QUERYPROXY_SEED_DEMO_FORCE=true) |
QUERYPROXY_ADMIN_EMAIL / QUERYPROXY_ADMIN_PASSWORD |
— | Docker only: create the first system administrator on boot while the instance has no users (password at least 8 characters); a no-op once any user exists |
QUERYPROXY_ADMIN_NAME |
Admin |
Docker only: display name for that first administrator |
QUERYPROXY_RUN_WORKER |
true |
Docker only: run the queue worker inside the container; set false when the worker runs in its own container |
QUERYPROXY_RUN_SCHEDULER |
true |
Docker only: run the scheduler (schedule:work) inside the container |
QUERYPROXY_WORKER_PROCESSES |
1 |
Docker only: number of queue worker processes |
QUERYPROXY_KEY_FILE |
/var/www/html/storage/app/.app_key |
Docker only: where the generated APP_KEY is persisted when APP_KEY itself is not set. When the variable is unset and a key file already exists at the legacy path /var/www/html/database/.app_key, the entrypoint keeps using that file |
QUERYPROXY_REQUIRE_2FA |
none |
Enforce TOTP two-factor auth: none, admins, dba (admins plus anyone with a DBA role) or all |
QUERYPROXY_CHAT_WEBHOOK_ALLOWED_HOSTS |
— | Extra allowed hosts for outbound chat webhooks, comma-separated, * wildcards allowed (SSRF guard; hooks.slack.com, *.webhook.office.com and *.logic.azure.com are allowed by default) |
QUERYPROXY_CHAT_INCLUDE_SQL |
true |
Embed a SQL preview in chat notifications |
QUERYPROXY_CONNECTION_HOST_DENYLIST |
— | Extra hosts blocked as connection targets (comma-separated) |
QUERYPROXY_SQLITE_ALLOWED_DIR |
— | Restrict SQLite connection files to this directory. Files inside the application directory (where QueryProxy’s default SQLite database lives) are always rejected, whether or not this is set |
QUERYPROXY_EXTRA_UNTRUSTED_LANGUAGES |
— | Extra procedural languages the guard refuses in CREATE EXTENSION (comma-separated); extends the built-in list. DO, CREATE FUNCTION and CREATE PROCEDURE accept only plpgsql and sql regardless |
QUERYPROXY_EXTRA_DANGEROUS_VARIABLES |
— | Extra server variables the guard refuses in SET (persistent scopes on MySQL / MariaDB, every scope on PostgreSQL) and in set_config (comma-separated); extends the built-in list |
QUERYPROXY_EXTRA_BLOCKED_FUNCTIONS |
— | Extra functions the guard refuses outright (comma-separated); extends the built-in list |
QUERYPROXY_EXTRA_STATE_CHANGING_FUNCTIONS |
— | Extra functions that make a statement a write (comma-separated); extends the built-in list |
QUERYPROXY_EXTRA_RESOURCE_CONSUMING_FUNCTIONS |
— | Extra functions that make a statement a write because they hold resources, such as sleeps (comma-separated); extends the built-in list |
QUERYPROXY_EXTRA_BLOCKED_PROCEDURES |
— | Extra SQL Server procedures the guard refuses (comma-separated); extends the built-in list |
QUERYPROXY_EXTRA_SQLITE_PRAGMAS |
— | Extra SQLite pragmas allowed in their bare, read form (comma-separated); extends the built-in list |
TRUSTED_PROXIES |
— | Reverse proxy IPs/CIDRs allowed to set X-Forwarded-* (comma-separated); empty trusts no proxy |
TRUSTED_HOSTS |
— | Extra hostnames this installation answers to, beyond APP_URL’s host (comma-separated) |
DB_QUEUE_RETRY_AFTER |
QUERYPROXY_EXECUTION_TIMEOUT + 30 (330) |
Seconds before a reserved queue job is retried. Derived from the execution timeout when unset; an explicit value wins. BEANSTALKD_QUEUE_RETRY_AFTER and REDIS_QUEUE_RETRY_AFTER work the same way |
retry_afteris derived: unless you setDB_QUEUE_RETRY_AFTER(or the Beanstalkd / Redis equivalent) yourself, it isQUERYPROXY_EXECUTION_TIMEOUT
- 30 seconds (
330for the default300). The Docker entrypoint derives the worker--timeout(execution timeout + 10) andstopwaitsecs(execution timeout + 20) from the same setting. If you setretry_afterexplicitly, keep it above the execution timeout, because the query job is stopped at that point; the default +30 leaves a margin. A value at or below it lets a slow query be re-reserved while it is still running and then marked failed, even though its write may commit, and logs a warning whenqueue:work(daemon or--once) orqueue:listenstarts. A fixed*_QUEUE_RETRY_AFTER=330left over from the 0.2.0.env.examplestops the derivation, so remove it unless you set it on purpose.
A TOTP code accepted for QUERYPROXY_REQUIRE_2FA is burned, so it cannot be
replayed for the rest of its window. One practical consequence: signing in on a
second device within the same 30-second step means waiting for the next code.
QUERYPROXY_EXTRA_UNTRUSTED_LANGUAGES and QUERYPROXY_EXTRA_DANGEROUS_VARIABLES
add to the two lists the SQL guards judge CREATE EXTENSION
and SET by. They can only extend: the built-in entries are
the floor, merged back on top of whatever the environment provides, so a trimmed
list cannot disarm the guard.
MySQL / MariaDB targets: sql_mode
The guard reads quotes and backslashes the default way, so a request is refused
when the session sql_mode would read them differently. Before it runs the SQL
on a mysql or mariadb connection, QueryProxy reads @@SESSION.sql_mode; if
it contains NO_BACKSLASH_ESCAPES or ANSI_QUOTES, the request fails and
nothing runs, with a message naming the flag. QueryProxy itself sets a strict
sql_mode when it connects, which replaces the server’s default, so if this
fires, look for what changes the mode after connect (a proxy in between, a
changed connection configuration); keeping the flags out of the server and
connection-user defaults is still sensible, but on its own it will not clear
it. Submitting a SET that changes sql_mode is refused on every
driver (see SQL guards).
Application key and HTTPS
APP_KEY encrypts your stored connection credentials and chat secrets. For
production, set it explicitly and keep it stable — a regenerated key (e.g. on
container recreation) makes every stored credential unreadable. Generate one
with php artisan key:generate --show, and when rotating, move the old key into
APP_PREVIOUS_KEYS (comma-separated) so existing ciphertext still decrypts.
When serving over HTTPS behind a reverse proxy, set SESSION_SECURE_COOKIE=true.
Behind a reverse proxy
Breaking change — set
TRUSTED_PROXIESwhen a proxy sits in front of QueryProxy. Earlier versions trusted every proxy. From this version no proxy is trusted by default, and until you name yours the app reads the connecting peer as the client.
TRUSTED_PROXIES takes a comma-separated list of the IPs or CIDRs allowed to
set X-Forwarded-* headers:
TRUSTED_PROXIES=10.0.0.0/8
Until that list names your proxy, X-Forwarded-Proto is ignored — HTTPS is not
detected, so no HSTS header is sent — and audit entries record the proxy’s
address instead of the client’s.
Never set it to *. Trusting every proxy lets any client send its own
X-Forwarded-For, which forges the client IP in the audit trail and hands out
unlimited login and 2FA attempts, because both throttles are keyed per IP.
X-Forwarded-Host is not honoured at all, whatever the list says: a forged host
header rewrote the link in password-reset mail. As a second layer, the request
host is pinned to APP_URL’s host (and its www. sibling), so absolute URLs —
signed routes, password-reset mails — always point at your own installation. Set
APP_URL to the address users actually reach.
If the installation answers to more than one name, list the others in
TRUSTED_HOSTS:
TRUSTED_HOSTS=queryproxy.internal,db.example.com
That is the supported way to run a multi-domain deployment now that
X-Forwarded-Host is ignored — a hostname you have not listed still cannot
rewrite a password-reset link. The names are added to APP_URL’s host (and its
www. sibling), never replace it. Entries that do not parse as a hostname are
dropped silently, so a typo cannot take the installation down.
Both values are read through the application config, so setting them in .env
is enough. If you cache the configuration, re-run php artisan config:cache
after changing them; the Docker image re-caches on every start.
Application database
QueryProxy’s own data (users, teams, requests, audit log) lives in SQLite by default — zero configuration. For MySQL or PostgreSQL, set the standard Laravel variables:
DB_CONNECTION=pgsql # or mysql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=queryproxy
DB_USERNAME=queryproxy
DB_PASSWORD=change-me
This is the application database — the databases you proxy are configured in the UI under Connections, per team, with encrypted credentials.
Queue
The queue is database-backed by default (no Redis required). To move it to Redis:
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_QUEUE_RETRY_AFTER is optional: when unset it is derived from
QUERYPROXY_EXECUTION_TIMEOUT + 30 seconds (see the note under the settings
table). Only set it to override that value, and keep it above the execution
timeout.
The worker command stays the same. Keep its --timeout a little above the
execution timeout (execution timeout + 10 seconds; QUERYPROXY_EXECUTION_TIMEOUT
is 300 by default, hence 310):
php artisan queue:work --queue=queries,default --tries=1 --timeout=310
In a manual setup this number is not derived for you: if you change
QUERYPROXY_EXECUTION_TIMEOUT, change --timeout to match (the Docker
entrypoint derives it itself).
The same warning (retry_after at or below the execution timeout) is logged
once when queue:listen or queue:work --once starts, not only for a daemon
queue:work. The short-lived queue:work --once processes that queue:listen
spawns do not repeat it.
In the repository’s docker-compose.yml, the separate worker example no
longer hard-codes --timeout=310: it passes QUERYPROXY_WORKER_TIMEOUT, which
the image entrypoint derives as QUERYPROXY_EXECUTION_TIMEOUT + 10. Set
QUERYPROXY_EXECUTION_TIMEOUT on that service and the worker follows. This
needs the image’s default entrypoint, so do not override entrypoint: on the
worker service.
Result storage on S3
Set QUERYPROXY_RESULT_DISK=s3 and the standard AWS_* variables. Result
files are written as NDJSON, streamed — memory use stays flat regardless of
result size.
The disk must be private. Result paths are predictable
(results/{team_id}/{id}.ndjson), so a publicly served disk would hand out
every team’s rows straight from the web root, bypassing the download
authorization entirely. QueryProxy therefore refuses to write results when
QUERYPROXY_RESULT_DISK points at the public disk or at any disk configured
with visibility => public, and the request fails with an explicit message
instead.
SQL Server targets
The default Docker image ships pdo_pgsql, pdo_mysql and pdo_sqlite. To
proxy Microsoft SQL Server databases, extend the Dockerfile with Microsoft’s
ODBC driver and the sqlsrv / pdo_sqlsrv PECL extensions, then build and run
your own image in place of queryproxy/queryproxy. The extensions have to be
present in the PHP build that runs both the web process and the queue worker,
because approved queries execute on the worker.