Document Engine with Docker

You can integrate Document Engine with Nutrient Web SDK. This integration enables you to offload document processing, rendering, and management to the server, enabling features such as real-time collaboration, annotation synchronization, and improved performance for large or complex documents.

This guide shows how to build a complete document viewing application using Document Engine deployed with Docker, integrated with Nutrient Web SDK in a React frontend and Node.js backend. We’ll set up secure authentication, handle document uploads, and create a modern web interface for viewing and annotating documents.

We've demonstrated the implementation using React + Vite for the frontend and Node.js for the backend, but the same concepts can be adapted to other supported web framework or technology stack.

Prerequisites

Document Engine is distributed as a Docker container. To run it, install(opens in a new tab) a container runtime distribution for your operating system (Docker, OrbStack, or others).

Setting up Document Engine

Copy the code snippet below and save it in a file named docker-compose.yml:

services:
document_engine:
image: pspdfkit/document-engine:1.11.0
environment:
PGUSER: de-user
PGPASSWORD: password
PGDATABASE: document-engine
PGHOST: db
PGPORT: 5432
API_AUTH_TOKEN: secret
SECRET_KEY_BASE: secret-key-base
JWT_PUBLIC_KEY: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2gzhmJ9TDanEzWdP1WG+
0Ecwbe7f3bv6e5UUpvcT5q68IQJKP47AQdBAnSlFVi4X9SaurbWoXdS6jpmPpk24
QvitzLNFphHdwjFBelTAOa6taZrSusoFvrtK9x5xsW4zzt/bkpUraNx82Z8MwLwr
t6HlY7dgO9+xBAabj4t1d2t+0HS8O/ed3CB6T2lj6S8AbLDSEFc9ScO6Uc1XJlSo
rgyJJSPCpNhSq3AubEZ1wMS1iEtgAzTPRDsQv50qWIbn634HLWxTP/UH6YNJBwzt
3O6q29kTtjXlMGXCvin37PyX4Jy1IiPFwJm45aWJGKSfVGMDojTJbuUtM+8P9Rrn
AwIDAQAB
-----END PUBLIC KEY-----
JWT_ALGORITHM: RS256
DASHBOARD_USERNAME: dashboard
DASHBOARD_PASSWORD: dashboard-password
ports:
- 5001:5000
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: de-user
POSTGRES_PASSWORD: password
POSTGRES_DB: document-engine
POSTGRES_INITDB_ARGS: --data-checksums
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:

Starting Document Engine

Go to the directory where you saved the docker-compose.yml file and run the following command in your terminal:

Terminal window
docker compose up

This command might take a while to run, depending on your internet connection speed. Wait until you see the following message:

document_engine_1 | Access the web dashboard at http://localhost:5001/dashboard

Document Engine is now up and running!

Uploading a document to Document Engine

Visit http://localhost:5001/dashboard and authenticate using "dashboard" as the username and "dashboard-password" as the password. Choose Add Document and upload the document you want to work with.

Screenshot showing the create document modal window in the Document Engine Dashboard

Once the document is uploaded, visit http://localhost:5001/dashboard/documents to see a list of available documents. Each document is identified by an ID. Copy this document ID - you’ll need to enter it in your React application to view the document. The ID looks similar to 7KPS8X13JRB2G841X4V7EQ3T2J.

Screenshot showing the uploaded document in the list of available documents

Building the backend server

Create a Node.js server that generates JWTs for secure document access. This server acts as the bridge between your frontend application and Document Engine. For a comprehensive understanding of JWT-based authentication with Document Engine, refer to our client authentication guide.

Setting up the Node.js project

Create a new directory for your backend and initialize a Node.js project:

Terminal window
mkdir document-viewer-backend
cd document-viewer-backend
npm init -y
npm install express jsonwebtoken cors

Creating the server

Create a file called server.js with the following content:

const express = require('express');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const app = express();
const port = 3001;
// Enable CORS for all requests
app.use(cors());
app.use(express.json());
// The private key from your Docker Compose configuration
const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA2gzhmJ9TDanEzWdP1WG+0Ecwbe7f3bv6e5UUpvcT5q68IQJK
P47AQdBAnSlFVi4X9SaurbWoXdS6jpmPpk24QvitzLNFphHdwjFBelTAOa6taZrS
usoFvrtK9x5xsW4zzt/bkpUraNx82Z8MwLwrt6HlY7dgO9+xBAabj4t1d2t+0HS8
O/ed3CB6T2lj6S8AbLDSEFc9ScO6Uc1XJlSorgyJJSPCpNhSq3AubEZ1wMS1iEtg
AzTPRDsQv50qWIbn634HLWxTP/UH6YNJBwzt3O6q29kTtjXlMGXCvin37PyX4Jy1
IiPFwJm45aWJGKSfVGMDojTJbuUtM+8P9RrnAwIDAQABAoIBAQDSKxhGw0qKINhQ
IwQP5+bDWdqUG2orjsQf2dHOHNhRwJoUNuDZ4f3tcYzV7rGmH0d4Q5CaXj2qMyCd
0eVjpgW0h3z9kM3RA+d7BX7XKlkdQABliZUT9SUUcfIPvohXPKEzBRHed2kf6WVt
XKAuJTD+Dk3LjzRygWldOAE4mnLeZjU61kxPYriynyre+44Gpsgy37Tj25MAmVCY
Flotr/1WZx6bg3HIyFRGxnoJ1zU1MkGxwS4IsrQwOpWEHBiD5nvo54hF5I00NHj/
ccz+MwpgGdjyl02IGCy1fF+Q5SYyH86DG52Mgn8VI9dseGmanLGcgNvrdJFILoJR
SZW7gQoBAoGBAP+D6ZmRF7EqPNMypEHQ5qHHDMvil3mhNQJyIC5rhhl/nn063wnm
zhg96109hVh4zUAj3Rmjb9WqPiW7KBMJJdnEPjmZ/NOXKmgjs2BF+c8oiLQyTQml
xB7LnptvBDi8MnEd3uemfxNuZc+2siuSzgditshNru8xPG2Sn99JC271AoGBANp2
xj5EfdlqNLd11paLOtJ7dfREgc+8FxQCiKSxbaOlVXNk0DW1w4+zLnFohj2m/wRr
bBIzSL+eufoQ9y4BT/AA+ln4qxOpC0isOGK5SxwIjB6OHhCuP8L3anj1IFYM+NX0
Xr1/qdZHKulgbS49cq+TDpB74WyKLLnsvQFyINMXAoGABR5+cp4ujFUdTNnp4out
4zXasscCY+Rv7HGe5W8wC5i78yRXzZn7LQ8ohQCziDc7XXqadmYI2o4DmrvqLJ91
S6yb1omYQCD6L4XvlREx1Q2p13pegr/4cul/bvvFaOGUXSHNEnUKfLgsgAHYBfl1
+T3oDZFI3O/ulv9mBpIvEXUCgYEApeRnqcUM49o4ac/7wZm8czT5XyHeiUbFJ5a8
+IMbRJc6CkRVr1N1S1u/OrMqrQpwwIRqLm/vIEOB6hiT+sVYVGIJueSQ1H8baHYO
4zjdhk4fSNyWjAgltwF2Qp+xjGaRVrcYckHNUD/+n/VvMxvKSPUcrC7GAUvzpsPU
ypJFxsUCgYEA6GuP6M2zIhCYYeB2iLRD4ZHw92RfjikaYmB0++T0y2TVrStlzXHl
c8H6tJWNchtHH30nfLCj9WIMb/cODpm/DrzlSigHffo3+5XUpD/2nSrcFKESw4Xs
a4GXoAxqU44w4Mckg2E19b2MrcNkV9eWAyTACbEO4oFcZcSZOCKj8Fw=
-----END RSA PRIVATE KEY-----`;
// Generate JWT for a given document ID
app.post('/generate-jwt', (req, res) => {
const { documentId } = req.body;
if (!documentId) {
return res.status(400).json({ error: 'Document ID is required' });
}
const payload = {
document_id: documentId,
permissions: ['read-document', 'write', 'download'],
exp: Math.floor(Date.now() / 1000) + (60 * 60) // Expires in 1 hour
};
try {
const token = jwt.sign(payload, PRIVATE_KEY, { algorithm: 'RS256' });
res.json({ jwt: token });
} catch (error) {
res.status(500).json({ error: 'Failed to generate JWT' });
}
});
app.listen(port, () => {
console.log(`Backend server running at http://localhost:${port}`);
});

Starting the backend server

Start your backend server:

Terminal window
node server.js

Your backend server is now running on http://localhost:3001 and ready to generate JWTs for document access.

Building the React frontend

Create a React application that fetches JWTs from your backend and displays documents using Nutrient Web SDK.

Setting up the React project

Create a new React app with Vite:

Terminal window
npm create vite@latest document-viewer-frontend -- --template react
cd document-viewer-frontend
npm install

Installing Nutrient Web SDK

Add the Nutrient Web SDK script to public/index.html:

<script src="https://cdn.cloud.pspdfkit.com/pspdfkit-web@1.7.0/nutrient-viewer.js"></script>

Creating the document viewer component

Replace the contents of src/App.jsx with the following:

import React, { useEffect, useRef, useState } from 'react';
import './App.css';
function App() {
const viewerRef = useRef(null);
const [documentId, setDocumentId] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const loadDocument = async () => {
if (!documentId.trim()) {
setError('Please enter a document ID');
return;
}
setIsLoading(true);
setError('');
try {
// Fetch JWT from backend
const response = await fetch('http://localhost:3001/generate-jwt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ documentId: documentId.trim() }),
});
if (!response.ok) {
throw new Error('Failed to generate JWT');
}
const { jwt } = await response.json();
// Clear previous viewer instance if it exists
if (viewerRef.current) {
viewerRef.current.innerHTML = '';
}
// Load document with Nutrient Web SDK
const { NutrientViewer } = window;
if (NutrientViewer) {
await NutrientViewer.load({
serverUrl: 'http://localhost:5001/',
container: viewerRef.current,
documentId: documentId.trim(),
authPayload: { jwt },
instant: true,
});
} else {
throw new Error('NutrientViewer not loaded from CDN');
}
console.log('Document loaded successfully');
} catch (err) {
setError(err.message || 'Failed to load document');
console.error('Error loading document:', err);
} finally {
setIsLoading(false);
}
};
return (
<div className="App">
<header className="App-header">
<h1>Document Viewer</h1>
<div className="document-controls">
<input
type="text"
placeholder="Enter Document ID (for example, 7KPS8X13JRB2G841X4V7EQ3T2J)"
value={documentId}
onChange={(e) => setDocumentId(e.target.value)}
disabled={isLoading}
/>
<button onClick={loadDocument} disabled={isLoading || !documentId.trim()}>
{isLoading ? 'Loading...' : 'Load Document'}
</button>
</div>
{error && <div className="error">Error: {error}</div>}
</header>
<div
ref={viewerRef}
className="viewer-container"
style={{ width: '100vw', height: '100vh' }}
/>
</div>
);
}
export default App;

Required CSS changes

Nutrient Web SDK requires that the mounting container has explicit width and height before calling NutrientViewer.load(). The container cannot be 0x0 pixels or the SDK will fail to initialize.

  1. Remove conflicting CSS from your src/index.css file. The default Vite React template includes CSS that interferes with container dimensions:

    /* src/index.css - Remove these conflicting styles */
    body {
    display: flex; /* Remove this line */
    place-items: center; /* Remove this line */
    }
  2. Ensure your viewer container has explicit dimensions. The React component above uses inline styles (style={{ width: '100vw', height: '100vh' }}) which is our recommended approach for most projects.

Starting the React application

Start your React development server:

Terminal window
npm run dev

Your React application will be running at http://localhost:5173.

With all three services running (Document Engine, backend server, and React app), you can now open http://localhost:5173, enter the document ID, and click Load Document. You should see your document rendered. Any changes you make will be automatically saved and synchronized across sessions.