You've decided on Temporal for durable workflows. The next decision is who runs the server. This post is the inventory: every piece a self-hosted Temporal Service is made of, straight from Temporal's own documentation, so you can size the job before you take it on. We build on Temporal Cloud, both for our own platform and for our users, so we have an opinion about where the line is, but the list comes first.
The four server services
A Temporal Server is four services, usually deployed and scaled separately:
- Frontend: the gRPC gateway "for rate limiting, routing, authorizing". It is stateless, so it scales differently from the rest.
- History: "maintains data (mutable state, queues, and timers)". This is the sharded core that owns workflow state.
- Matching: "hosts Task Queues for dispatching" work to your workers.
- Worker: the server's own internal worker, "for internal background Workflows" such as replication and system workflows. Not to be confused with the workers you write.
History, Matching, and Worker scale horizontally. Each one is a deployment with its own resource limits, health checks, and metrics to watch.
Persistence
Workflow state and history live in a database you provide. Temporal tests against:
- Cassandra v3.11, v4.0, and 5.0.4 and later
- PostgreSQL 13.18, 14.15, 15.10 and 16.6
- MySQL v5.7 and v8.0 (8.0.19 or later because of a MySQL bug)
- SQLite v3.x, "meant only for development and testing, not production usage"
One setting deserves its own paragraph: numHistoryShards. From the configuration reference: "This value is immutable and will be ignored after the first run. Please ensure you set this value appropriately high enough to scale with the worst case peak load for this Service." You pick the shard count on day one, for the load you expect in year three.
Visibility
Visibility is the store behind "list all running workflows" and any search in the Web UI. It is separate from persistence and has its own support matrix: Elasticsearch v7 and v8, OpenSearch 2 and later (server 1.30.1+), MySQL 8.0.17+, PostgreSQL 12+, and SQLite. Cassandra is not supported as a visibility store.
Temporal's recommendation is short. Elasticsearch and OpenSearch are "recommended for any setup that spawns more than a few Workflow Executions". SQL visibility works at small scale; past that you are also running a search cluster.
Upgrades
Temporal releases regularly, and the upgrade guide is specific about the procedure:
- "Temporal Server should be upgraded sequentially, one minor version at a time." First move to the highest patch of your current minor version.
- Upgrade the database schema before the binaries, with
temporal-sql-tool,temporal-cassandra-tool, ortemporal-elasticsearch-tooldepending on your stores. - "Allow approximately 10 minutes on each version" for the History service to load all shards and update shard metadata before moving to the next one.
- Skipping versions "may lead to older data formats becoming unreadable".
If you are three minor versions behind, that is three upgrade windows, each with a schema migration.
Security
Out of the box, there is none. The security guide is explicit: "If you do not explicitly configure an Authorizer, Temporal uses the default noopAuthorizer. This default allows every API request, with no authentication or access control."
The pieces you configure yourself:
- mTLS between the server's own nodes (internode) and on the Frontend's public endpoints, with certificates you issue and rotate.
- Authorization through two plugin interfaces: a
ClaimMapperthat extracts claims from JWTs, and anAuthorizerwhoseAuthorizemethod runs on every API call. - Encryption at rest for workflow payloads through a custom Payload Codec in your SDK's data converter.
- Network placement: Temporal services "should run on hosts that are not accessible from the public internet".
API-key authentication, the thing most people expect from a hosted service, is a Temporal Cloud feature; the self-hosted security guide covers mTLS and JWT-based authorization.
Ways to deploy it
Temporal documents four routes:
- Docker Compose from the
temporalio/docker-composerepository, with PostgreSQL and Elasticsearch, gRPC on port 7233 and the Web UI on port 8080. Good for evaluating; the compose file becomes yours to maintain. - Helm charts for Kubernetes, deploying each server service to its own pods. Server images 1.30 and later require chart version 0.73.1 or later.
- Each service separately, under systemd or behind a reverse proxy such as Nginx or Envoy.
- Two Go binaries: the core server and the UI server.
There is a fifth thing that looks like a route and isn't: temporal server start-dev. The CLI dev server "is not intended for production use" and "skips certain HTTP security checks to make local use simpler". It's excellent locally, and we use it for exactly that.
Add to all of this the parts every production system needs and Temporal only points at: metrics collection for the server and SDKs, alerting on task queue backlog and schedule-to-start latency, backups of the persistence store, and archival of closed histories to blob storage if retention matters to you.
When to stop
Everything above is real work, and none of it is your application. Self-hosting makes sense when you have the team for it, when data residency rules require it, or when your workflow volume makes a managed bill larger than an SRE. Below that line, two options remove the whole list.
Temporal Cloud runs the server side for you: the four services, persistence, visibility, upgrades, and the security layer. You still create namespaces per environment, issue and distribute credentials, and run workers somewhere.
Specific manages both sides. One block in specific.hcl is a local dev server in development and a provisioned Temporal Cloud namespace in production, with the address, namespace, and API key injected into the worker you define next to it:
temporal "jobs" {}
build "worker" {
base = "node"
command = "npm run build"
}
service "worker" {
build = build.worker
command = "node dist/worker.js"
env = {
TEMPORAL_ADDRESS = temporal.jobs.url
TEMPORAL_NAMESPACE = temporal.jobs.namespace
TEMPORAL_API_KEY = temporal.jobs.api_key
}
}No Temporal Cloud account, no certificates, no upgrade windows. Your workflow code stays standard Temporal, so it is portable if you ever do want to run the list above yourself. The details of what gets provisioned are in managed Temporal hosting; this post is the "why".
Common questions
Can I self-host Temporal on a single node?
Yes, for small volumes: the docker-compose setup is effectively that. You give up high availability, and you still own upgrades, backups, and security configuration.
Is SQLite fine for a small production deployment?
No. Temporal lists SQLite as "meant only for development and testing, not production usage". Use PostgreSQL or MySQL for a small deployment.
Do I need Elasticsearch?
Not at first. PostgreSQL or MySQL visibility works for small volumes. Temporal recommends Elasticsearch or OpenSearch "for any setup that spawns more than a few Workflow Executions".
Can I move from self-hosted to Temporal Cloud later?
Your workflow and activity code moves unchanged; it's standard Temporal pointed at a new address. Running workflow history does not transfer automatically, so the usual approach is to drain in-flight workflows on the old cluster while starting new ones on the new one.
Try it
A complete working project with workflows, activities, and a worker: the durable workflows example (live demo, source).
To start in your own project, give your agent this prompt:
Help me get started with Specific by following: https://docs.specific.dev/for-ai/onboardingOr install the CLI yourself:
curl -fsSL https://specific.dev/install.sh | shThen run specific init and ask your agent for durable workflows.
For more depth, see the Temporal guide in our docs, how to run Temporal locally, and our explainer on what durable execution is.