Skip to content

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.

Terminal window
# 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/qsign

Use the image references Quoqo provides. Either:

Terminal window
# (a) Pull from the registry Quoqo grants you access to:
docker login <REGISTRY> # credentials from Quoqo
docker 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>.tar
docker load -i qsign-frontend-<version>.tar
docker load -i esign-<version>.tar
docker load -i esign-api-<version>.tar

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=False
DJANGO_SETTINGS_MODULE=quoqo_dr.settings.base
ALLOWED_HOSTS=sign.example.com
SERVER_BASE_URL=https://sign.example.com/backend
FRONTEND_URL=https://sign.example.com
BACKEND_LOGIN_PASSWORD_KEY=<STRONG_RANDOM> # MUST equal the frontend's REACT_APP_LOGIN_PASSWORD_KEY
# --- Database (REQUIRED) ---
DB_HOST=db
DB_PORT=3306
DB_NAME=qsign
qsign_DB_NAME=qsign
DB_USER=qsign
DB_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/qsigncontainer
STORAGE_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.md

config/frontend.env

REACT_APP_BACKEND_BASE_URL=https://sign.example.com/backend
REACT_APP_FRONT_END_BASE_URL=https://sign.example.com
REACT_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=qsign
MYSQL_USER=qsign
MYSQL_PASSWORD=<DB_PASSWORD> # same as backend DB_PASSWORD
MYSQL_ROOT_PASSWORD=<DB_ROOT_PASSWORD>
Terminal window
chmod 600 /opt/qsign/config/*.env # restrict secret files

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-Proto correct, 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 keep X-Forwarded-* headers intact.

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; the esign service still applies the platform tamper seal.

Terminal window
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 tier
docker compose up -d esign qsign-esign-api
docker compose up -d qsign-backend qsign-frontend nginx
docker compose ps # all should be healthy/up

The backend entrypoint runs database migrations automatically on start. Watch logs:

Terminal window
docker compose logs -f qsign-backend

Run the one-time bootstrap commands inside the backend container:

Terminal window
# 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_plans
docker 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 add

Note: run Python in the backend container via the venv interpreter (/opt/venv/bin/python), not a bare python shell, so the app’s environment loads correctly. The plan-setup commands are idempotent and support --dry-run.

Option A — Let’s Encrypt (bundled certbot): with port 80 reachable and the nginx http server block in place, issue the first cert:

Terminal window
docker compose run --rm certbot certonly --webroot -w /var/www/certbot \
-d sign.example.com --email admin@example.com --agree-tos --no-eff-email
docker compose restart nginx

The 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.

Terminal window
curl -sI https://sign.example.com/health # 200
curl -sI https://sign.example.com/ # 200 (frontend)
curl -s https://sign.example.com/backend/othercompanyapi/openapi.json | head # API spec

Then 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.