Deploy · Start here

Single instance

One container or process, with its store on SQLite on a volume or on PostgreSQL. It runs the same image and code as the other two shapes, so moving on later is a configuration change, not a migration project.

Apps · partners · agentsReverse proxyTLSQueryAPIGate:5000 REST · Console · :5001 MCPrestarting…Your databasesStoreSQLite on a volume
01…
Start here

Single instance

✓One container or process; SQLite store on a volume, or PostgreSQL.
✓Restart on failure; back up the store on a schedule.
✓Redis optional, for a cache that survives restarts.
StoreSQLite on a volume, or PostgreSQL
RedisOptional
Live eventsGET /events on the instance
AvailabilityRestart on failure
ConfigurationEnvironment, Console or a file
Tested in CIEvery push
The files

Three files in one folder

Only the proxy publishes ports, so QueryAPIGate is reachable only through it.

docker-compose.yml
services:
  queryapigate:
    image: ghcr.io/anantharajuc/queryapigate:0.47.0   # pin a version: upgrades should be deliberate
    restart: unless-stopped
    env_file: .env
    environment:
      QUERYAPIGATE_TRUST_PROXY: "1"                   # one proxy in front: Caddy
    volumes:
      - queryapigate-data:/data                       # queryapigate.db lives here
    networks: [internal]

  proxy:
    image: caddy:2
    restart: unless-stopped
    ports: ["443:443", "80:80"]
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
    networks: [internal]
    depends_on: [queryapigate]

networks:
  internal:
volumes:
  queryapigate-data:
  caddy-data:
Caddyfile
api.example.com {
    reverse_proxy queryapigate:5000
}
.env · never committed, chmod 600
QUERYAPIGATE_API_KEY=<openssl rand -hex 32>
QUERYAPIGATE_SECRET_KEY=<a Fernet key>       # encrypts connection passwords in the store
WAREHOUSE_PASSWORD=<your database's password> # referred to as ${WAREHOUSE_PASSWORD}
The admin key isn't optional in a container.With no key, the server answers only local clients. Inside Docker the caller's address is the container network, so every request is refused with 403 open_access_remote until a key is set.
Start it

Up, healthy, and behind TLS

Caddy gets and renews a certificate by itself once DNS points at the host and ports 80 and 443 are open. Then open /console, sign in with the admin key, add a connection, and create scoped keys for each caller.

shell
docker compose run --rm queryapigate queryapigate init   # optional: inactive template connections
docker compose up -d
docker compose ps                                         # becomes "healthy"
curl https://api.example.com/health

What's already in the image

  • Runs as a non-root user (uid 1000) and has a HEALTHCHECK on /health every 30 seconds.
  • gunicorn, one worker, eight gthread threads, a 120-second timeout.
  • Drivers for PostgreSQL, MySQL, ClickHouse, DuckDB, SQLite and MongoDB, plus JWT, the Redis cache and queryapigate mcp. The -h2 tag adds Java for H2 and generic JDBC.
  • One catch: a SQLite or DuckDB file must exist inside the container. Put it on /data; relative paths resolve against it.
Day two

Logs, upgrades, monitoring

How
Logsdocker compose logs -f queryapigate: one line per request, with the request id and key name
UpgradesRead the changelog, back up, bump the pinned tag, docker compose pull && docker compose up -d. The store is migrated on first start.
Monitoring/metrics for Prometheus. Set QUERYAPIGATE_METRICS_TOKEN, since its labels name your keys, queries and connections.
Optional servicesSame image, other commands: redis:7-alpine + QUERYAPIGATE_REDIS_URL for a cache that survives restarts; queryapigate events --host 0.0.0.0 for live events to many clients; queryapigate mcp --host 0.0.0.0 for AI agents. Give each the same .env and volume.
Backups

queryapigate backup, not cp

It uses SQLite's backup API, so the copy is one consistent moment, including changes still in the write-ahead log. Measured on a running server: a cp taken moments earlier was missing the last two keys created (30 keys against 32).

shell
docker compose exec queryapigate queryapigate backup /data/backup.db --force
docker compose cp queryapigate:/data/backup.db "./backups/queryapigate-$(date +%F).db"
docker compose exec queryapigate rm /data/backup.db

To restore, stop the server, put the backup in place as queryapigate.db, and delete the old -wal and -shm files.

What a backup doesn't holdQUERYAPIGATE_SECRET_KEY (without it, encrypted passwords can't be read; keep it in your secrets manager, not next to the backup), the admin key, the ${VAR} values connections refer to, and the databases QueryAPIGate queries. Scoped keys are in the store as hashes, so they keep working after a restore.
In CI on every pushtests/test_backup_restore.py fills a store, backs it up, loses it, restores it and checks the admin API sees the same.
Without Docker

pip and gunicorn

Always one worker per process: /metrics is per process, and without Redis so are rate limits. Put a TLS proxy in front and set QUERYAPIGATE_TRUST_PROXY to the number of proxies.

shell
pip install "queryapigate[server]"
QUERYAPIGATE_API_KEY=… gunicorn --bind 127.0.0.1:5000 --workers 1 --worker-class gthread --threads 8 \
  --timeout 120 "queryapigate.app:create_app()"
When to move on

Outgrowing one instance

  • Don't raise the replica count of this Compose file. Each copy would get its own SQLite volume and be a separate server with its own keys.
  • For capacity or availability, move the store to PostgreSQL and add Redis (High availability). queryapigate migrate-to-postgres copies the SQLite store across once, before you switch traffic.
  • For simple redundancy without PostgreSQL, run one active instance and a standby restored from backups.

What changes when you move

Moving between shapes is a configuration change. This is what each step adds; the step for this page is highlighted.

01Single instance
QUERYAPIGATE_API_KEY=… QUERYAPIGATE_SECRET_KEY=… One process or container, with the store as SQLite on a volume. The key is required in a container; the secret key encrypts connection passwords at rest.
02+ High availability
QUERYAPIGATE_DATABASE_URL=postgres://… QUERYAPIGATE_REDIS_URL=redis://… A shared PostgreSQL store and Redis, so any instance answers any request and limits are counted once. Run queryapigate events behind the proxy for live events. Guide 42 →
03+ Kubernetes
store.type: postgres · replicas: 3 existingSecret: queryapigate The same two URLs and the key live in one Secret; the chart refuses several pods on SQLite, several without Redis, PostgreSQL without a database URL, and secrets put in config. Guide 54 →
Before go-live The production checklist, the ports and the measured numbers They apply to every shape, so they live on the Deploy overview.