Getting started with TinyMCE AI on-premises
This guide sets up a fully working local stack in roughly five minutes on any machine with Docker:
-
MySQL 8.0 — conversation history and metadata
-
Redis — caching and session state
-
TinyMCE AI service — the on-premises AI back end
-
A minimal token server (Node.js) — signs JWTs for the editor
-
A browser page with TinyMCE — validates the end-to-end flow
The quick start is designed to validate the stack components before moving to a production deployment. Production engineers can review this section to understand the conceptual flow before continuing to Production deployment.
Prerequisites
-
Docker 20.10+ (or Podman 4+)
-
Node.js 18+ and npm (for the demo token server)
-
A TinyMCE license key and container registry credentials (from the Tiny account representative)
-
At least one LLM provider API key (OpenAI, Anthropic, or Google)
Five-minute demo with Docker Compose
Authenticate with the container registry
The service image lives at registry.containers.tiny.cloud/ai-service-tiny.
For Docker:
docker login -u 'TINY_REGISTRY_USERNAME' https://registry.containers.tiny.cloud
# Docker prompts for the password; this avoids leaking it in shell history.
For Podman:
podman login -u 'TINY_REGISTRY_USERNAME' registry.containers.tiny.cloud
Replace TINY_REGISTRY_USERNAME with the username supplied by the Tiny account representative. If credentials have not been received, contact support@tiny.cloud.
Pull the AI service image
docker pull registry.containers.tiny.cloud/ai-service-tiny:latest
For Podman, substitute podman pull. For production, pin a specific version tag (for example :5.1.0) rather than :latest for repeatable deployments and to avoid unexpected breaking changes.
Create docker-compose.yml (data layer)
This compose file starts the data layer services (MySQL and Redis) that the AI service depends on. The AI service itself is started separately in the next step, which allows upgrading or reconfiguring it independently.
Create the file with exactly the contents below. Indentation is two spaces, never tabs.
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-changeme}
MYSQL_DATABASE: ai_service
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mysql_data:
Pin mysql:8.0, not mysql:8. The :8 tag resolves to the latest MySQL minor version, which may use authentication plugins or SQL modes incompatible with the AI service schema migrations. See MySQL version pinning for details.
|
PostgreSQL is equally supported. See Database, Redis, and storage for an equivalent compose file. Review the PostgreSQL schema prerequisite before switching.
|
If the AI service needs to reach the host machine (for example a self-hosted Ollama running on the host), add |
Create the .env file
# --- Required: provided by Tiny ---
LICENSE_KEY=<paste-license-key-here>
# --- Required for this demo (loads TinyMCE from cdn.tiny.cloud) ---
# Omit only when using a self-hosted editor bundle with license_key.
TINYMCE_API_KEY=<paste-api-key-here>
# --- Required: strong secret used to log into the Management Panel ---
MANAGEMENT_SECRET=<replace-with-strong-secret>
# --- Required: database password (must match docker-compose.yml) ---
DB_PASSWORD=<replace-with-db-password>
# --- Required: at least one LLM provider key ---
OPENAI_API_KEY=<paste-openai-key-here>
# ANTHROPIC_API_KEY=<paste-anthropic-key-here>
# GOOGLE_API_KEY=<paste-google-key-here>
# --- Filled in after creating an environment (see "Create an environment and access key" below). Used by the token server, not the AI service. ---
AI_ENV_ID=
AI_API_SECRET=
LICENSE_KEY and TINYMCE_API_KEY are different credentials. LICENSE_KEY is the long string from the account representative that activates the AI service. TINYMCE_API_KEY is the short string from the tiny.cloud dashboard used to load TinyMCE from the CDN — it is not required for self-hosted editor bundles. See the Credentials section on the Overview page.
|
Start MySQL and Redis
docker compose up -d
Wait ~15 seconds for MySQL to initialize, then verify:
docker compose ps
Both data layer containers (MySQL and Redis) should report healthy in the STATUS column. If MySQL still shows starting, wait another 10 seconds and re-run.
Launch the AI service
The AI service runs as a standalone container outside of the Docker Compose stack. This separation allows upgrading or reconfiguring the AI service without restarting the database and Redis.
Run from the same folder as the .env file:
set -a && source .env && set +a
PROVIDERS='{'
if [ -n "$OPENAI_API_KEY" ]; then
PROVIDERS+='"openai":{"type":"openai","apiKeys":["'"$OPENAI_API_KEY"'"]}'
fi
if [ -n "$ANTHROPIC_API_KEY" ]; then
[ "$PROVIDERS" != '{' ] && PROVIDERS+=','
PROVIDERS+='"anthropic":{"type":"anthropic","apiKeys":["'"$ANTHROPIC_API_KEY"'"]}'
fi
if [ -n "$GOOGLE_API_KEY" ]; then
[ "$PROVIDERS" != '{' ] && PROVIDERS+=','
PROVIDERS+='"google":{"type":"google","apiKeys":["'"$GOOGLE_API_KEY"'"]}'
fi
PROVIDERS+='}'
# Resolve the compose network name (varies across Docker versions and folder names)
NETWORK=$(docker network ls --format '{{.Name}}' | grep -E "^$(basename "$PWD" | tr '[:upper:]' '[:lower:]')[_-]default$" | head -1)
if [ -z "$NETWORK" ]; then
echo "ERROR: Could not find the Docker Compose network. Run 'docker network ls' and pass the network name with --network=<name>."
exit 1
fi
docker run --init -d -p 8000:8000 \
--network "$NETWORK" \
--name ai-service \
-e LICENSE_KEY="$LICENSE_KEY" \
-e ENVIRONMENTS_MANAGEMENT_SECRET_KEY="$MANAGEMENT_SECRET" \
-e DATABASE_DRIVER='mysql' \
-e DATABASE_HOST='mysql' \
-e DATABASE_USER='root' \
-e DATABASE_PASSWORD="$DB_PASSWORD" \
-e DATABASE_DATABASE='ai_service' \
-e REDIS_HOST='redis' \
-e PROVIDERS="$PROVIDERS" \
-e STORAGE_DRIVER='database' \
-e ALLOWED_ORIGINS='http://localhost:3000' \
-e ENABLE_METRIC_LOGS='true' \
registry.containers.tiny.cloud/ai-service-tiny:latest
For Podman, replace docker run with podman run and use a Podman pod instead of a compose network. See Production deployment for Podman-specific guidance. See Podman deployment for a full example.
For native databases (the database runs on the host or in a managed service rather than in Docker), drop the --network flag and set DATABASE_HOST=host.docker.internal (Docker Desktop and Podman 4+). On native Linux Docker, additionally pass --add-host=host.docker.internal:host-gateway.
Wait five seconds, then verify:
curl http://localhost:8000/health
Expected response:
{"serviceName":"on-premises-http","uptime":5.123}
|
If the container exits immediately, run |
Create an environment and access key
The AI service isolates users into Environments. Each environment has its own access keys.
-
Open the Management Panel: http://localhost:8000/panel/
-
Sign in using the
MANAGEMENT_SECRETfrom.env. -
Click Create Environment and give it a name (for example "Development").
-
Note the Environment ID displayed (a short identifier like
viOu8BnjJHb0HGK091p). -
Inside the environment, click Create Access Key.
-
Copy the API Secret immediately. The Management Panel shows it only once.
Update .env with the new values:
AI_ENV_ID=PASTE_ENVIRONMENT_ID_HERE
AI_API_SECRET=PASTE_API_SECRET_HERE
|
Always create environments through the Management Panel UI. Environments created through the raw Management API are not fully registered and cause |
Create the token server
The token server signs JSON Web Tokens (JWTs) for the editor. The Node.js example below is for the demo only; the JWT authentication guide contains production-ready endpoints in 8 languages (Node, Django, Flask, Laravel, Rails, .NET, Go, Spring Boot).
Create package.json:
{
"name": "tinymce-ai-onpremise-demo",
"private": true,
"scripts": {
"start": "node token-server.js"
},
"dependencies": {
"dotenv": "^16.0.0",
"express": "^4.18.0",
"jsonwebtoken": "^9.0.0"
}
}
Create token-server.js:
Full token-server.js listing
require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');
const PORT = process.env.PORT || 3000;
const AI_ENV_ID = process.env.AI_ENV_ID;
const AI_API_SECRET = process.env.AI_API_SECRET;
const AI_SERVICE_URL = process.env.AI_SERVICE_URL || 'http://localhost:8000';
const TINYMCE_API_KEY = process.env.TINYMCE_API_KEY || 'no-api-key';
if (!AI_ENV_ID || !AI_API_SECRET) {
console.error('ERROR: AI_ENV_ID and AI_API_SECRET must be set in .env');
console.error('Create an environment first: visit http://localhost:8000/panel/');
process.exit(1);
}
const app = express();
app.use(express.json());
app.post('/api/ai-token', (req, res) => {
const token = jwt.sign({
aud: AI_ENV_ID,
sub: 'demo-user-001',
user: { name: 'Demo User', email: 'demo@example.com' },
auth: {
ai: {
permissions: [
'ai:conversations:*',
'ai:models:agent',
'ai:actions:system:*',
'ai:reviews:system:*'
]
}
}
}, AI_API_SECRET, { algorithm: 'HS256', expiresIn: '1h' });
res.json({ token });
});
app.get('/', (req, res) => {
res.send(`<!DOCTYPE html>
<html>
<head>
<title>TinyMCE AI on-premises Demo</title>
<!-- Replace with the path to self-hosted TinyMCE, or use the CDN for quick testing -->
<script src="https://cdn.tiny.cloud/1/${TINYMCE_API_KEY}/tinymce/8/tinymce.min.js" referrerpolicy="origin"></script>
</head>
<body style="max-width: 900px; margin: 40px auto; font-family: system-ui;">
<h1>TinyMCE AI on-premises Demo</h1>
<p>Select text and use the AI toolbar, or open the AI chat sidebar.</p>
<textarea id="editor"><p>Select this text and try the AI features above. Ask the AI to rewrite it, summarize it, or change the tone.</p></textarea>
<script>
tinymce.init({
selector: '#editor',
plugins: 'tinymceai',
toolbar: 'undo redo | blocks | bold italic | tinymceai-chat tinymceai-review tinymceai-quickactions',
height: 500,
tinymceai_service_url: '${AI_SERVICE_URL}',
tinymceai_token_provider: () =>
fetch('/api/ai-token', { method: 'POST' })
.then(r => r.json())
.then(data => ({ token: data.token }))
});
</script>
</body>
</html>`);
});
app.listen(PORT, () => {
console.log('Editor: http://localhost:' + PORT);
console.log('Token API: http://localhost:' + PORT + '/api/ai-token');
console.log('AI Service: ' + AI_SERVICE_URL);
});
Open the demo
Open http://localhost:3000 in a browser. The editor loads with the AI toolbar. Select text and try the AI features. Responses stream in real time from the chosen large language model (LLM) provider, processed entirely within the local infrastructure.
The TinyMCE AI on-premises service is now running.
Verifying the installation
After completing the quick start, exercise the pipeline end-to-end from the command line.
# 1. Health check
curl http://localhost:8000/health
Expected:
{"serviceName":"on-premises-http","uptime":12.345}
# 2. Generate a token
curl -s -X POST http://localhost:3000/api/ai-token | python3 -m json.tool
Expected:
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
# 3. Create a conversation and send a message
TOKEN=$(curl -s -X POST http://localhost:3000/api/ai-token | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
curl -s -X POST http://localhost:8000/v1/conversations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"id":"verify-1","title":"Verification"}'
The command below uses the built-in agent-1 model. If MODELS has been explicitly configured, replace agent-1 with the id of one of the configured models. See Defining the model list.
|
curl -s -N -X POST http://localhost:8000/v1/conversations/verify-1/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"prompt":"Say hello in five words.","model":"agent-1"}'
The message endpoint returns a Server-Sent Events stream:
event: message-metadata
data: {"messageId":"abc123"}
event: text-delta
data: {"textDelta":"Hello "}
event: text-delta
data: {"textDelta":"there, "}
event: text-delta
data: {"textDelta":"friend!"}
event: done
data: {}
If the stream emits event: error, inspect the data payload. Provider errors (invalid API key, IAM denial, model unavailable) ride inside the Server-Sent Events (SSE) response. The HTTP status stays 200. See the LLM provider errors section in the Troubleshooting guide for details.
A successful round-trip confirms: container health, database connectivity, Redis connectivity, JWT signing, JWT verification, permissions checking, environment registration, LLM provider authentication, and SSE streaming. If problems persist after these checks, focus on the editor configuration next.
Updating configuration
After changing .env values, containers must be recreated to pick up new environment variables. A simple restart (docker restart or docker compose restart) preserves the old values.
|
# Recreate the data layer (MySQL, Redis):
docker compose up -d --force-recreate
# Recreate the standalone AI service:
docker stop ai-service && docker rm ai-service
# Then re-run the launch script from "Launch the AI service" above.
For Kubernetes, update the Secret and trigger a rollout restart:
kubectl rollout restart deployment/ai-service -n tinymce-ai
Stopping and cleaning up
# Stop the AI service (standalone Docker)
docker stop ai-service && docker rm ai-service
# Stop the Docker Compose stack
docker compose down
# Remove all data including volumes (destructive)
docker compose down -v
For Kubernetes, scale the deployment to zero or delete it. Persistent volumes for the database are retained unless explicitly deleted.
kubectl delete deployment ai-service -n tinymce-ai