---
title: "Deploy bisibility"
description: "Self-hosted open-source SEO platform with a PostgreSQL database you own."
category: "Analytics"
url: https://railway.com/deploy/bisibility-1
---

# Deploy bisibility

Self-hosted open-source SEO platform with a PostgreSQL database you own.

**[Deploy bisibility on Railway](https://railway.com/template/bisibility-1)**

- **Creator:** Bisibility
- **Category:** Analytics
- **Total deploys:** 2

## Template content

### temporal https://cdn.simpleicons.org/temporal/444CE7

- **Image:** temporalio/server:1.31.2
- **Start command:** `sh -eu -c 'mkdir -p /tmp/temporal-dynamic; printf "{}\n" > /tmp/temporal-dynamic/docker.yaml; attempt=1; while ! /etc/temporal/entrypoint.sh; do if [ "$attempt" -ge 30 ]; then echo "temporal startup retries exhausted after $attempt attempts" >&2; exit 1; fi; echo "temporal startup prerequisite not ready; retrying in 2s ($attempt/30)" >&2; attempt=$((attempt + 1)); sleep 2; done'`

### worker https://bisibility.com/icon.svg

- **Image:** corgicorner/bisibility-worker:0.8.1

### redis https://devicons.railway.com/i/redis.svg

- **Image:** redis:8.2.1
- **Start command:** `/bin/sh -c 'rm -rf $RAILWAY_VOLUME_MOUNT_PATH/lost+found/ && exec docker-entrypoint.sh redis-server --requirepass $REDIS_PASSWORD --save 60 1 --dir $RAILWAY_VOLUME_MOUNT_PATH'`

### temporal-postgres https://devicons.railway.app/i/postgresql.svg

- **Image:** ghcr.io/railwayapp-templates/postgres-ssl:18

### temporal-schema https://cdn.simpleicons.org/temporal/444CE7

- **Image:** temporalio/admin-tools:1.31.2
- **Start command:** `sh -eu -c 'timeout 300 sh -c "until nc -z $POSTGRES_SEEDS $DB_PORT; do echo Waiting for Temporal PostgreSQL; sleep 2; done"; export SQL_PLUGIN=$DB; export SQL_HOST=$POSTGRES_SEEDS; export SQL_PORT=$DB_PORT; export SQL_USER=$POSTGRES_USER; export SQL_PASSWORD=$POSTGRES_PWD; ensure_database() { database=$1; if output=$(temporal-sql-tool --db "$database" create 2>&1); then printf "%s\n" "$output"; return 0; fi; if printf "%s\n" "$output" | grep -qi "already exists"; then printf "Database %s already exists; applying versioned updates.\n" "$database"; return 10; fi; printf "%s\n" "$output" >&2; return 1; }; if ensure_database temporal; then temporal-sql-tool --db temporal setup-schema -v 0.0; elif [ $? -ne 10 ]; then exit 1; fi; temporal-sql-tool --db temporal update-schema -d /etc/temporal/schema/postgresql/v12/temporal/versioned; if ensure_database temporal_visibility; then temporal-sql-tool --db temporal_visibility setup-schema -v 0.0; elif [ $? -ne 10 ]; then exit 1; fi; temporal-sql-tool --db temporal_visibility update-schema -d /etc/temporal/schema/postgresql/v12/visibility/versioned'`

### web https://bisibility.com/icon.svg

- **Image:** corgicorner/bisibility:0.8.1
- **Health check:** /api/v1/readiness
- **Public domain:** Yes

### app-postgres https://devicons.railway.app/i/postgresql.svg

- **Image:** ghcr.io/railwayapp-templates/postgres-ssl:18

### temporal-namespace https://cdn.simpleicons.org/temporal/444CE7

- **Image:** temporalio/admin-tools:1.31.2
- **Start command:** `sh -eu -c 'timeout 300 sh -c "until temporal operator cluster health --address $TEMPORAL_ADDRESS | grep -q SERVING; do echo Waiting for the Temporal frontend; sleep 2; done"; if temporal operator namespace describe --address "$TEMPORAL_ADDRESS" --namespace "$TEMPORAL_NAMESPACE" >/dev/null 2>&1; then temporal operator namespace update --address "$TEMPORAL_ADDRESS" --namespace "$TEMPORAL_NAMESPACE" --retention "$TEMPORAL_NAMESPACE_RETENTION"; else temporal operator namespace create --address "$TEMPORAL_ADDRESS" --namespace "$TEMPORAL_NAMESPACE" --retention "$TEMPORAL_NAMESPACE_RETENTION" --description "bisibility application namespace"; fi; timeout 120 sh -c "until temporal operator namespace describe --address $TEMPORAL_ADDRESS --namespace $TEMPORAL_NAMESPACE >/dev/null 2>&1; do echo Waiting for the namespace cache; sleep 2; done"; list_search_attributes() { deadline=$(( $(date +%s) + 120 )); until current=$(temporal operator search-attribute list --address "$TEMPORAL_ADDRESS" --namespace "$TEMPORAL_NAMESPACE" --output json 2>/dev/null); do if [ "$(date +%s)" -ge "$deadline" ]; then echo "Timed out waiting for Temporal search-attribute namespace visibility." >&2; return 1; fi; echo "Waiting for Temporal search-attribute namespace visibility..." >&2; sleep 2; done; printf "%s" "$current" | tr -d "[:space:]"; }; has_keyword_attribute() { printf "%s" "$1" | grep -o "$2[^,}]*" | grep -Fq "INDEXED_VALUE_TYPE_KEYWORD"; }; ensure_search_attribute() { attribute=$1; current=$(list_search_attributes); if has_keyword_attribute "$current" "$attribute"; then echo "Temporal search attribute $attribute already exists as Keyword."; return 0; fi; if output=$(temporal operator search-attribute create --address "$TEMPORAL_ADDRESS" --namespace "$TEMPORAL_NAMESPACE" --name "$attribute" --type Keyword 2>&1); then printf "%s\n" "$output"; return 0; fi; current=$(list_search_attributes); if has_keyword_attribute "$current" "$attribute"; then echo "Temporal search attribute $attribute was created concurrently."; return 0; fi; printf "%s\n" "$output" >&2; return 1; }; for attribute in keywordId projectId provider; do ensure_search_attribute "$attribute"; done'`

## Documentation

# Deploy and Host bisibility on Railway

bisibility is a self-hosted, open-source SEO platform (AGPL-3.0) for researching keywords, inspecting backlinks, and tracking Google rankings. This template deploys the complete scheduled-check stack with the web/API, worker, application PostgreSQL, Redis, Temporal Server, Temporal PostgreSQL, and idempotent bootstrap jobs.

## About Hosting

Railway keeps every service on private networking and exposes only the web application. Both PostgreSQL services use independent persistent volumes. The application and worker are pinned to the same immutable bisibility release, while Temporal Server is pinned independently.

Before serving users, configure `EMAIL_FROM` and `RESEND_API_KEY`, connect a SERP provider, and complete the production checklist.

## Why Deploy bisibility on Railway?

The template generates deployment secrets, initializes both Temporal schemas and the deployment namespace, applies application migrations, and starts the matching worker automatically. Temporal Cloud is not required.

## Common Use Cases

- Research keywords and inspect backlinks through your own SERP provider account.
- Track keyword positions with manual and scheduled checks.
- Keep rank history and provider credentials inside your own Railway project.
- Run the bisibility API, CLI, MCP, webhooks, and Temporal-backed automation.

## Dependencies for bisibility Hosting

- A verified transactional email sender and Resend API key for sign-in codes.
- A supported SERP provider connected after deployment.
- Railway resources for six persistent roles and three completed bootstrap jobs.

### Deployment Dependencies

- bisibility web and worker images pinned to the same release.
- PostgreSQL 18 for application data and separate Temporal persistence.
- Redis 8 for rate limits, idempotency, notifications, and worker liveness.
- Temporal Server and admin tools pinned to 1.31.2.

Full setup, upgrade, backup, and rollback steps: https://bisibility.com/docs/self-hosting#railway

## Similar templates

- [Matomo Analytics + MariaDB](https://railway.com/deploy/matomo-analytics-mariadb) — Privacy-friendly analytics with MariaDB and persistent volumes.
- [Bugsink](https://railway.com/deploy/bugsink) — Self-hosted Error Tracking. Sentry-SDK compatible
- [SubTrackr](https://railway.com/deploy/bscottsubtrackr) — Self-hosted Subscription Tracker

Open this page in a browser: https://railway.com/deploy/bisibility-1
