The self-hosted CMS for multilingual blogs
Write posts in a structured editor, publish directly or through review, and deliver each translation through built-in reading pages, a REST API or webhooks. Open source, one container, no other services.
- Any BCP 47 code for your content languages
- 6 panel languages English, Turkish, French, German, Japanese and Simplified Chinese
- OWASP ASVS 5.0 reviewed at levels 1 and 2
- One container with an embedded SQLite database
A focused editor with a safety net
Structured content instead of raw HTML, saved while you type and kept in a history you can always go back to.
-
Block editor
Headings, nested and task lists, quotes, tables, highlighted code, math, images from the media library and YouTube or Vimeo embeds.
-
Autosave and revisions
The working draft saves itself a few seconds after you stop typing. Every save goes into a revision history you can restore.
-
Private previews
Open unpublished versions and pending submissions the way readers will see them, without making them public.
-
Media library
Uploads are re-encoded to WebP in several sizes with their metadata removed, and every image keeps alternative text per language.
-
Categories and tags
Categories carry a name and a slug in every language, and tags are entered per translation.
-
Search and sharing fields
Each translation has its own meta title, meta description and sharing image, and its reading time is calculated on save.
Every translation is a first-class page
A post holds one translation per content language. Each has its own address, fields and publishing state, so every language can move at its own pace.
- Add content languages in the panel with any BCP 47 code, such as
de,pt-BRorzh-Hans. - Publish the English version while the German one is still in review.
- Reading pages link translations with
hreflang, including anx-defaultentry for your default language. - API clients can fall back to the default language when a translation is missing.
- The panel speaks English, Turkish, French, German, Japanese and Simplified Chinese, whatever your content languages are.
<link rel="canonical" href="https://cms.example.com/blog/en/designing-for-readers">
<link rel="alternate" hreflang="en" href="https://cms.example.com/blog/en/designing-for-readers">
<link rel="alternate" hreflang="tr" href="https://cms.example.com/blog/tr/okurlar-icin-tasarlamak">
<link rel="alternate" hreflang="x-default" href="https://cms.example.com/blog/en/designing-for-readers">
Publish directly or go through review
Roles decide who may publish. Everyone else submits their work, and what readers see stays unchanged until a change is approved.
-
Draft
Write
Authors write in the editor while the working draft saves itself.
-
Pending review
Submit
Authors without direct publishing send their changes to the review queue.
-
Approved
Review
An admin or the founder approves, or rejects with a note that the author sees.
-
Published
Publish
The translation goes live right away or at its scheduled time, and webhooks fire.
The founder, admins and authors allowed to publish directly skip the queue and publish or schedule right away.
-
Founder
Exactly one per installation. Everything an admin can do, plus system settings, managing admins and changing roles.
-
Admin
Manages authors, reviews their submissions, moderates posts, and looks after languages, categories, API keys, webhooks and CORS.
-
Author
Writes their own posts and manages their own images. Publishing needs approval unless the author may publish directly.
Nobody edits another person's post, not even the founder. Staff can hide a post with a reason that its owner sees, and an append-only audit log records sign-ins, account changes, publishing, moderation and configuration.
Use the built-in blog or go headless
Readers can get posts straight from Servitor, your own front end can pull them through the API, or both at once. Turn the reading pages off to run headless.
-
Reading pages
Server-rendered pages under
/blogwithout JavaScript, with canonical and hreflang links, Open Graph, JSON-LD, RSS feeds and sitemaps. -
REST API
Read-only JSON with API keys limited to languages and categories, full-text search, per-key rate limits, ETags and an OpenAPI 3.1 description.
-
Webhooks
Signed with HMAC-SHA256 and sent from a persistent queue with retries, so a static site can rebuild whenever published content changes.
curl -H "Authorization: Bearer $SERVITOR_API_KEY" \
"https://cms.example.com/api/v1/posts?lang=tr&per_page=10"
{
"data": [
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"published_at": "2026-09-24T09:00:00.000Z",
"translations": [
{
"language": "tr",
"slug": "okurlar-icin-tasarlamak",
"title": "Okurlar için tasarlamak",
"reading_time_minutes": 4,
"content_html": "<p>…</p>"
}
]
}
],
"meta": { "page": 1, "per_page": 10, "total": 42, "total_pages": 5 }
}
POST /hooks/servitor HTTP/1.1
Host: build.example.com
Content-Type: application/json
User-Agent: Servitor-Webhooks/1.0
X-Servitor-Event: post.published
X-Servitor-Delivery: 0f6c9a55-7a4e-4f21-9f8d-2d0f6f3b9b1a
X-Servitor-Signature: t=1790251200,v1=5d2a9c…
{
"event": "post.published",
"delivery_id": "0f6c9a55-7a4e-4f21-9f8d-2d0f6f3b9b1a",
"timestamp": "2026-09-24T12:00:00.000Z",
"post_id": "8b1d1c52-5d0e-4e0f-9c1b-3f7a2b6e4d10",
"languages": ["en", "tr"],
"slugs": {
"en": "designing-for-readers",
"tr": "okurlar-icin-tasarlamak"
}
}
Secure by default, reviewed in the open
Servitor is checked against every level 1 and 2 requirement of OWASP ASVS 5.0, and the protections below are on from the first start.
-
No public sign-up
Accounts come only from the founder seed and invitations, and sign-in answers never reveal whether an account exists.
-
Two-factor authentication
TOTP with single-use backup codes, and a setting that requires it for the founder and admins.
-
Careful password handling
12 to 128 characters, checked against bundled lists of common passwords and hashed with scrypt.
-
One permission module
Every page, action and endpoint is checked on the server, and a test covers every cell of the permission matrix.
-
Sanitized content
Editor JSON is validated against an allowlist and rendered to sanitized HTML on the server before it is stored.
-
Strict browser policies
A nonce-based Content Security Policy on every response, and reading pages that ship without any script.
-
Scoped, hashed API keys
Stored only as SHA-256 hashes, limited to languages and categories, and refused when sent in a query string.
-
SSRF-safe webhooks
Targets are resolved and checked before every delivery, so webhooks cannot reach into your internal network.
-
Hardened container
An unprivileged user, a read-only root filesystem and no capabilities. Every image carries a software bill of materials and a signed build attestation.
Up and running with Docker Compose
All you need is a server with Docker and Docker Compose. Every release is published as a signed, prebuilt image for amd64 and arm64 on the GitHub Container Registry.
-
Get the Compose file
Clone the repository and create your configuration from the example file.
Terminalgit clone https://github.com/justhasanuknow/servitor-cms.git cd servitor-cms cp .env.example .env -
Set the secret and the founder
Set
ORIGINto your public address, generate the secret withopenssl rand -hex 32and fill in the founder account. For a quick local test, useORIGIN=http://localhost:3000and leaveADDRESS_HEADERandXFF_DEPTHempty..envSERVITOR_VERSION=0.1 ORIGIN=https://cms.example.com BETTER_AUTH_SECRET=<output of openssl rand -hex 32> FOUNDER_EMAIL=you@example.com FOUNDER_NAME=Your Name FOUNDER_PASSWORD=<12 to 128 characters> -
Start the container
Compose pulls the image in the version set by
SERVITOR_VERSION, where0.1follows the newest 0.1.x release, and starts Servitor. Open/panel/login, sign in as the founder and choose a new password when the panel asks for it.Terminaldocker compose up -d
-
Automatic migrations
The database schema is updated on start, before the server accepts requests.
-
Health check
A
/healthzendpoint reports whether the database is reachable, for Docker and your monitoring. -
Backup and restore
One command writes a consistent snapshot of the database and uploads, and restores check an archive before replacing anything.
-
Behind your proxy
Documented setups for Caddy, Nginx and Coolify, with TLS and real client addresses for rate limiting.
Servitor runs as a single instance: rate limits, the publishing scheduler and the webhook worker live inside the application process, so never start more than one container on the same data.
Apache 2.0, built on proven tools
Read the code, run it on your own infrastructure and adapt it to your needs. Bug reports, fixes and translations of the panel are welcome.
- SvelteKit
- Svelte 5
- TypeScript
- Tailwind CSS
- shadcn-svelte
- Tiptap
- Better Auth
- Drizzle ORM
- SQLite
- Paraglide
- sharp
- Vitest
- Playwright
- Node.js 24
Frequently asked questions
Is Servitor CMS free to use?
Yes. Servitor CMS is open source under the Apache License 2.0. You can use it commercially, change it and run it on your own servers, as long as derivative works keep the license and the NOTICE file.
What do I need to run it?
A server with Docker and Docker Compose. Servitor runs as one container with an embedded SQLite database and needs no other services. In production, put it behind a reverse proxy such as Caddy, Nginx or Coolify for TLS.
Can I use Servitor as a headless CMS?
Yes. Turn off the public reading site in the settings and use the read-only REST API and webhooks with any front end, such as a static site generator or a mobile app.
Which languages can I write in?
Any language you can describe with a BCP 47 code, such as en, de, pt-BR or zh-Hans. Every post can have one translation per content language. The panel itself is available in English, Turkish, French, German, Japanese and Simplified Chinese.
Can I run several instances behind a load balancer?
No. Rate limits, the publishing scheduler and the webhook worker live inside the application process, so exactly one container runs per data directory.
How do backups work?
A command inside the container writes a timestamped archive with a consistent database snapshot and all uploads. Restoring checks the archive before it replaces anything and keeps the previous data aside.
How do I update Servitor?
Take a backup, check out the tag of the new release so that docker-compose.yml and .env.example match it, set SERVITOR_VERSION and run docker compose pull and docker compose up -d. Migrations run automatically on start, and with a minor line such as 0.1, docker compose pull alone picks up the newest patch release.
Can I verify the Docker image?
Yes. Every published image carries a signed attestation of the workflow and commit it was built from. The GitHub CLI checks it with gh attestation verify oci://ghcr.io/justhasanuknow/servitor-cms:0.1 --owner justhasanuknow.
Is Servitor ready for production?
Version 0.1.1 is the current release. Servitor follows semantic versioning: before 1.0.0, a minor version may change the configuration, the API or the data in incompatible ways, and its release notes say so.
Where is the documentation?
The documentation lives in the docs folder of the repository, and every installation serves it at /docs. The panel links to it under Help, Documentation.
Start publishing in the languages your readers speak
Install Servitor CMS with Docker Compose, invite your team and publish your first translation.