---
title: "Deploy Temporal | Durable Workflows, No Elasticsearch"
description: "Durable workflows on Postgres alone - 650 MiB less than the ES version"
category: "Queues"
url: https://railway.com/deploy/temporal-or-durable-workflows-no-elastic
---

# Deploy Temporal | Durable Workflows, No Elasticsearch

Durable workflows on Postgres alone - 650 MiB less than the ES version

**[Deploy Temporal | Durable Workflows, No Elasticsearch on Railway](https://railway.com/template/temporal-or-durable-workflows-no-elastic)**

- **Creator:** SuperSlowSloth
- **Category:** Queues

## Template content

### temporal-auth

- **Image:** ghcr.io/brody192/railway-caddy-basic-auth:main
- **Public domain:** Yes

### postgres

- **Image:** postgres:18.4-alpine

### temporal-ui

- **Image:** temporalio/ui:2.53.1

### temporal

- **Image:** temporalio/auto-setup:1.29.7

## Documentation

# Deploy and Host Temporal on Railway

Temporal is a durable execution platform: you write ordinary code, and Temporal makes it survive
crashes, restarts and deploys. Workflows keep their state, retries are automatic, and long-running
processes can span days or months without you building a job table and a cron to babysit it.

## About Hosting Temporal

Most self-hosted Temporal setups run **Elasticsearch** next to Postgres, because that is what the
official `docker-compose` files do. Elasticsearch is there for one feature: *advanced visibility* —
custom search attributes and filtered workflow queries such as `OrderTier="gold"`.

Since Temporal 1.20 that feature works on **PostgreSQL 12+** directly. Elasticsearch is no longer
required for it, but the popular Railway templates still ship it, and it is by far the largest
thing in the project.

This template drops Elasticsearch and keeps the feature. Measured on Railway on 2026-08-15, both
stacks idling in the same environment on the same plan:

| | this template | the most-deployed Temporal template |
|---|---:|---:|
| Postgres | 239 MiB | 154 MiB |
| Temporal | 668 MiB | 75 MiB |
| Elasticsearch | — | **1305 MiB** |
| UI (+ auth proxy) | 9 MiB | 31 MiB |
| **Total** | **915 MiB** | **1565 MiB** |

**650 MiB less, about 42%**, and the Elasticsearch container alone is larger than this entire
stack. Two things to be straight about: the other template's Temporal container was still warming
up when it was sampled, so the real gap is a little smaller than the table suggests; and this
template's Temporal runs heavier on purpose (see history shards below).

Verified on the running deploy, not assumed:

- `operator cluster health` reports `SERVING`
- a custom search attribute (`OrderTier`, Keyword) was created against Postgres
- two workflows were started and `workflow list --query 'OrderTier="gold"'` returned exactly one

## Why Deploy Temporal on Railway?

Railway runs all four pieces — server, database, UI and the TCP proxy your workers dial — as one
project with private networking already wired, so there is no compose file, no VPC and no TLS
plumbing to write. Because this template leaves Elasticsearch out, it fits comfortably on the
small plans where a 1.5 GB stack would not, and the bill tracks a Postgres rather than a JVM
search cluster you were only using to filter a list.

## Common Use Cases

- Background jobs that must not be lost when a container restarts mid-run
- Multi-step order, payment, onboarding or provisioning flows with automatic retries
- Long-running processes measured in hours, days or months (trials, dunning, scheduled reminders)
- Orchestrating calls across several services or third-party APIs, with each step made durable
- Replacing a hand-rolled jobs table, cron and retry loop with something that keeps its own state

## Dependencies for Temporal Hosting

- **PostgreSQL 12 or newer** — included in this template, with a volume. Holds both the main and
  the visibility database.
- **A Railway TCP proxy** on the Temporal service — included, and how workers outside Railway
  reach gRPC on port 7233.
- Nothing else. There is deliberately no Elasticsearch, and no external message broker or cache.

### Deployment Dependencies

- [Temporal server (`temporalio/auto-setup`)](https://hub.docker.com/r/temporalio/auto-setup)
- [Temporal Web UI (`temporalio/ui`)](https://hub.docker.com/r/temporalio/ui)
- [PostgreSQL](https://hub.docker.com/_/postgres)
- [Temporal self-hosting documentation](https://docs.temporal.io/self-hosted-guide)
- [Visibility on SQL databases](https://docs.temporal.io/self-hosted-guide/visibility)

## What you get

- **Temporal server** (`temporalio/auto-setup`, pinned) — schema setup and the `default` namespace
  are created on first boot, so there is no manual `temporal-sql-tool` step.
- **PostgreSQL** with a volume, holding both the main and the visibility database.
- **Temporal Web UI**, private, reachable only through the auth proxy below.
- **Basic-auth proxy** on the public HTTPS URL, with a generated 32-character password. The UI is
  never exposed unauthenticated — anyone who finds the URL without credentials gets a 401, and
  the Web UI can terminate workflows, so this is not optional.
- **gRPC on a Railway TCP proxy**, so real workers outside Railway can connect.

## Settings chosen for you, and why

- **`NUM_HISTORY_SHARDS=512`.** This value **cannot be changed after the cluster is created** —
  changing it means starting over and losing your workflow history. Temporal's own default in the
  container image is 4, which is fine for a demo and a wall you hit later. 512 is Temporal's
  production recommendation. It costs some memory now and saves a migration you cannot do.
- **`BIND_ON_IP=::`.** Railway's private network is IPv6-only; binding IPv4 leaves the UI unable to
  reach the server, and because Temporal has no HTTP healthcheck the deploy still goes green while
  the cluster is dead.
- **Retention 72h** on the default namespace, so a busy queue does not quietly grow the database
  forever. Raise it per namespace when you know what you need.

## Connecting a worker

Your workers connect over the TCP proxy, not the web URL:

```
temporal --address : operator cluster health
```

Both values are on the Temporal service's Variables tab. In an SDK, that host:port is the
`Client` target address, and the namespace is `default`.

## Custom search attributes

```
temporal --address  operator search-attribute create --name OrderTier --type Keyword
temporal --address  workflow list --query 'OrderTier="gold"'
```

No Elasticsearch involved — these are columns in Postgres.

## When you should use the Elasticsearch version instead

Honest limits. SQL visibility is the right default, not the right answer for everyone:

- Very high workflow volume. Elasticsearch scales visibility queries past what a single Postgres
  will comfortably serve.
- Full-text search over `Text` search attributes with real relevance ranking.
- An existing Elasticsearch you already run and want Temporal to index into.

For the great majority of Railway deploys — internal tooling, background jobs, order and
onboarding flows — Postgres visibility is enough, and the 1.3 GB is pure cost.


## Similar templates

- [smoothmq](https://railway.com/deploy/AJv-64) — A drop-in replacement for AWS SQS
- [Kafka UI](https://railway.com/deploy/kafka-ui) — Kafbat UI — Open-source web UI to monitor and manage Apache Kafka clusters
- [Hatchet Lite](https://railway.com/deploy/hatchet-lite) — Hatchet Lite with postgres

Open this page in a browser: https://railway.com/deploy/temporal-or-durable-workflows-no-elastic
