Deployment
This guide installs the full QSign stack on a single Linux host with Docker Compose. For the meaning of every environment variable, see 05-configuration. For sizing, see 03-system-requirements.
Replace every
<PLACEHOLDER>and use strong, unique secrets. Never commit real secrets to source control.
1. Prepare the host
Section titled “1. Prepare the host”# Docker Engine + Compose plugin must be installed and running:docker --version && docker compose version
# Create a working directory + config/secret folders:sudo mkdir -p /opt/qsign/{config,secrets,nginx,certs}cd /opt/qsign2. Obtain the images
Section titled “2. Obtain the images”Use the image references Quoqo provides. Either:
# (a) Pull from the registry Quoqo grants you access to:docker login <REGISTRY> # credentials from Quoqodocker pull <REGISTRY>/qsign-backend:<version>docker pull <REGISTRY>/qsign-frontend:<version>docker pull <REGISTRY>/esign:<version>docker pull <REGISTRY>/esign-api:<version>
# (b) …or import offline archives (air-gapped):docker load -i qsign-backend-<version>.tardocker load -i qsign-frontend-<version>.tardocker load -i esign-<version>.tardocker load -i esign-api-<version>.tar3. Create the environment files
Section titled “3. Create the environment files”Create three env files under /opt/qsign/config/ (see
05-configuration for the full reference). Minimum to start:
config/backend.env
# --- Core (REQUIRED) ---SECRET_KEY=<STRONG_RANDOM_50+_CHARS>DEBUG=FalseDJANGO_SETTINGS_MODULE=quoqo_dr.settings.baseALLOWED_HOSTS=sign.example.comSERVER_BASE_URL=https://sign.example.com/backendFRONTEND_URL=https://sign.example.comBACKEND_LOGIN_PASSWORD_KEY=<STRONG_RANDOM> # MUST equal the frontend's REACT_APP_LOGIN_PASSWORD_KEY
# --- Database (REQUIRED) ---DB_HOST=dbDB_PORT=3306DB_NAME=qsignqsign_DB_NAME=qsignDB_USER=qsignDB_PASSWORD=<DB_PASSWORD>
# --- Redis (REQUIRED) ---REDIS_SERVER_URL=redis://redis:6379
# --- Solr (REQUIRED) ---SOLR_BASE_URL=http://solr:8983/solr/SOLR_CORE=qsign
# --- Document storage (local filesystem default) ---LOCAL_STORAGE_PATH=/app/secure_storage/qsigncontainerSTORAGE_SERVER_URL=https://sign.example.com/backend/contract/storage
# --- Email (recommended) ---RESEND_API_KEY=<EMAIL_PROVIDER_API_KEY> # or configure Zeptomail; see 05-config
# --- Optional integrations: leave blank/unset to disable ---# Aadhaar (India), DSC, Stripe/Razorpay, LLM, SMS, SSO, Sentry — see 05-configuration.mdconfig/frontend.env
REACT_APP_BACKEND_BASE_URL=https://sign.example.com/backendREACT_APP_FRONT_END_BASE_URL=https://sign.example.comREACT_APP_LOGIN_PASSWORD_KEY=<SAME_AS_BACKEND_LOGIN_PASSWORD_KEY>REACT_APP_VERSION=onprem# Optional UI integrations (SSO, Stripe, Aadhaar) — see 05-configuration.md.config/db.env
MYSQL_DATABASE=qsignMYSQL_USER=qsignMYSQL_PASSWORD=<DB_PASSWORD> # same as backend DB_PASSWORDMYSQL_ROOT_PASSWORD=<DB_ROOT_PASSWORD>chmod 600 /opt/qsign/config/*.env # restrict secret files4. Reverse-proxy config
Section titled “4. Reverse-proxy config”nginx/default.conf (genericized; uses Docker service-name upstreams):
server { listen 80; server_name sign.example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$host$request_uri; }}
server { listen 443 ssl; server_name sign.example.com; client_max_body_size 25M; # max upload size
ssl_certificate /etc/letsencrypt/live/sign.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/sign.example.com/privkey.pem;
# Liveness probe location = /health { return 200 "OK\n"; add_header Content-Type text/plain; }
# Frontend SPA (+ websockets) location / { proxy_pass http://qsign-frontend:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
# Backend API (strip the /backend prefix) location /backend/ { proxy_pass http://qsign-backend:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 600s; # long for large-doc processing }
# E-signature services location /esign { proxy_pass http://esign:8080; proxy_set_header Host $host; } location /qsign-esign-api/ { proxy_pass http://qsign-esign-api:8090/; proxy_set_header Host $host; }
# NOTE: do NOT expose Solr (search index) externally.}Hardening note: terminate TLS here, keep
proxy_set_header X-Forwarded-Protocorrect, and never proxy/solr/to the public listener. If you front QSign with your own load balancer/WAF, point it at this nginx (or directly at the frontend/backend services) and keepX-Forwarded-*headers intact.
5. The Compose file
Section titled “5. The Compose file”docker-compose.yml (genericized — replace <REGISTRY>/…:<version> with Quoqo’s
image references):
name: qsign
networks: qsign_net: { driver: bridge }
volumes: qsign_mysql: qsign_solr: qsign_storage: qsign_logs:
services: db: image: mysql:8.0 restart: always env_file: [./config/db.env] volumes: ["qsign_mysql:/var/lib/mysql"] networks: [qsign_net] healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 10
redis: image: redis:alpine restart: always networks: [qsign_net]
solr: image: solr:9.3.0 restart: always command: ["solr-precreate", "qsign"] volumes: ["qsign_solr:/var/solr"] networks: [qsign_net]
esign: image: <REGISTRY>/esign:<version> restart: always env_file: [./secrets/esign.env] # ASP_ID, ESIGN_* URLs, P12_* (if Aadhaar/DSC) volumes: - ./secrets/signing-cert.p12:/run/secrets/signing-cert.p12:ro - qsign_storage:/app/secure_storage networks: [qsign_net]
qsign-esign-api: image: <REGISTRY>/esign-api:<version> restart: always env_file: [./secrets/esign.env] networks: [qsign_net]
qsign-backend: image: <REGISTRY>/qsign-backend:<version> restart: always env_file: [./config/backend.env] depends_on: db: { condition: service_healthy } redis: { condition: service_started } solr: { condition: service_started } volumes: - qsign_storage:/app/secure_storage - qsign_logs:/var/log networks: [qsign_net]
qsign-frontend: image: <REGISTRY>/qsign-frontend:<version> restart: always env_file: [./config/frontend.env] networks: [qsign_net]
nginx: image: nginx:1.27 restart: always depends_on: [qsign-frontend, qsign-backend] ports: ["80:80", "443:443"] volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro - ./certs/conf:/etc/letsencrypt - ./certs/www:/var/www/certbot networks: [qsign_net]
# --- Optional: antivirus scanning of uploads --- clamav: image: clamav/clamav:stable restart: always networks: [qsign_net]
# --- Optional: automated Let's Encrypt renewal --- certbot: image: certbot/certbot volumes: - ./certs/conf:/etc/letsencrypt - ./certs/www:/var/www/certbot entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"If you enable DSC or Aadhaar, create
secrets/esign.env(see 05-configuration → “E-signature service”) and mount the signing certificate as shown. If you use neither, you can omit the cert mount; theesignservice still applies the platform tamper seal.
6. First start
Section titled “6. First start”cd /opt/qsign
# (Let's Encrypt only) obtain the initial certificate first — see step 8.
docker compose up -d db redis solr # bring up the data tierdocker compose up -d esign qsign-esign-apidocker compose up -d qsign-backend qsign-frontend nginxdocker compose ps # all should be healthy/upThe backend entrypoint runs database migrations automatically on start. Watch logs:
docker compose logs -f qsign-backend7. Seed the application
Section titled “7. Seed the application”Run the one-time bootstrap commands inside the backend container:
# Create the first administrator (super-admin):docker compose exec qsign-backend /opt/venv/bin/python manage.py createsuperuser
# Provision the plan/quota catalogue (consumer + API tiers):docker compose exec qsign-backend /opt/venv/bin/python manage.py setup_core_pro_plansdocker compose exec qsign-backend /opt/venv/bin/python manage.py setup_api_plans
# (If using search) build the Solr index:docker compose exec qsign-backend /opt/venv/bin/python manage.py reindex_solr
# (Optional) register in-container cron jobs (reminders, quota housekeeping):docker compose exec qsign-backend /opt/venv/bin/python manage.py crontab addNote: run Python in the backend container via the venv interpreter (
/opt/venv/bin/python), not a barepythonshell, so the app’s environment loads correctly. The plan-setup commands are idempotent and support--dry-run.
8. TLS
Section titled “8. TLS”Option A — Let’s Encrypt (bundled certbot): with port 80 reachable and the nginx http server block in place, issue the first cert:
docker compose run --rm certbot certonly --webroot -w /var/www/certbot \ -d sign.example.com --email admin@example.com --agree-tos --no-eff-emaildocker compose restart nginxThe certbot service then auto-renews.
Option B — your own / internal CA: place fullchain.pem + privkey.pem where the
nginx config expects them (or adjust the paths) and docker compose restart nginx.
9. Verify
Section titled “9. Verify”curl -sI https://sign.example.com/health # 200curl -sI https://sign.example.com/ # 200 (frontend)curl -s https://sign.example.com/backend/othercompanyapi/openapi.json | head # API specThen open https://sign.example.com/ in a browser and sign in as the super-admin you
created. Create a test user, upload a document, and complete a signature to validate the
full pipeline end-to-end.
See 06-operations for backups, monitoring, and upgrades, and 09-troubleshooting if something doesn’t come up.