How to Migrate from Hosted OpenSEO to Self-Hosted: Complete Guide

Migrate from hosted OpenSEO to self-hosted by cloning the repository, configuring DataForSEO API credentials, and deploying via Docker for local infrastructure or Cloudflare Workers for edge hosting, optionally migrating existing D1 data to Postgres using the provided migration script.

OpenSEO is an open-source SEO analytics platform delivered as a Cloudflare Workers service backed by D1 (SQLite) databases. When you migrate from hosted OpenSEO to self-hosted, you replace the managed runtime with either a Docker container on your own servers or a Cloudflare Workers deployment under your account, enabling full control over authentication, data residency, and third-party integrations.

Understanding the Architecture Changes

The hosted version runs on Cloudflare’s edge network with managed D1 databases. A self-hosted deployment shifts the runtime responsibility to your infrastructure while maintaining compatibility with the same external APIs.

Key architectural differences:

  • Runtime: Hosted uses Cloudflare Workers (managed); self-hosted offers Docker (Linux containers) or Cloudflare Workers (your own account).
  • Database: Hosted uses D1 (SQLite); self-hosted Docker can use D1 via the same image or Postgres via migration, while self-hosted Workers continue using D1.
  • Authentication: Hosted relies on Cloudflare Access; Docker deployments use AUTH_MODE=local_noauth (no internal auth, requires reverse proxy protection), while self-hosted Workers use Cloudflare Access managed by your deploy script.
  • External APIs: Both require DATAFORSEO_API_KEY (mandatory) and optionally GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET for Google Search Console integration.

Step 1: Preparation and Environment Setup

Begin by cloning the repository and installing dependencies. The project uses pnpm managed via Corepack.

git clone https://github.com/every-app/open-seo.git
cd open-seo
corepack enable
pnpm install

Copy the appropriate environment template based on your target deployment:


# For Docker deployments

cp .env.example .env

# For Cloudflare Workers deployments

cp .env.selfhost.example .env.selfhost

Edit the environment file and configure the following required variables:

  • DATAFORSEO_API_KEY: Base64-encoded email:password from your DataForSEO account (see docs/DATAFORSEO_API_KEY.md).
  • GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET: Required only for Google Search Console integration (see docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md).
  • BETTER_AUTH_SECRET: Random string of at least 32 characters for encrypting OAuth tokens.
  • PORT: HTTP port for Docker (defaults to 3001).
  • ALLOWED_HOST: Hostname for Vite preview when behind a reverse proxy.

Step 2: Deploy with Docker (Local Self-Hosting)

Docker is the fastest path to run OpenSEO on private infrastructure. The compose.yaml file injects all .env variables into the container and runs the official image.

docker compose up -d
docker compose logs -f

The application will be available at http://localhost:3001. Because Docker runs with AUTH_MODE=local_noauth, there is no internal authentication mechanism; you must place the container behind a reverse proxy or VPN for production security.

To pin a specific version instead of using latest:

OPEN_SEO_IMAGE=ghcr.io/every-app/open-seo:v1.2.3 docker compose up -d

Step 3: Deploy to Cloudflare Workers (Edge Self-Hosting)

For production-grade, internet-facing deployments, use the Cloudflare Workers path with Alchemy provisioning. This creates your own D1 database, KV namespaces, and R2 bucket.

pnpm alchemy login
pnpm alchemy cloudflare bootstrap

# Edit .env.selfhost with your DATAFORSEO_API_KEY and ACCESS_ALLOWED_EMAILS

pnpm deploy:selfhost --yes

After deployment, open the printed Worker URL. Access is controlled via Cloudflare Access; add authorized emails to ACCESS_ALLOWED_EMAILS in .env.selfhost and redeploy to grant teammates entry.

To upgrade later, pull the latest changes and rerun pnpm deploy:selfhost --yes.

Migrating Existing Data from D1 to Postgres

If your hosted installation contains historical data in D1 that you want to preserve in a Postgres-backed self-hosted deployment, use the migration script at scripts/migrate-d1-to-postgres.ts. This script reads directly from the D1 API and writes via Drizzle-ORM to Postgres, performing an FK-safe copy with sequence resets.

Set up the migration environment:


# Create a local env file for the migration

CLOUDFLARE_ACCOUNT_ID=your_account_id
CLOUDFLARE_API_TOKEN=your_api_token
POSTGRES_DATABASE_URL=postgres://user:pass@host:5432/db

Run a dry-run to verify row counts:

pnpm exec tsx scripts/migrate-d1-to-postgres.ts --dry-run

Execute the full migration:

pnpm exec tsx scripts/migrate-d1-to-postgres.ts

For partial syncs (e.g., before final cut-over), add the --update flag to copy only recent changes from the last 12 hours.

After migration, set DATABASE_PROVIDER=postgres in your Docker or Cloudflare environment to connect to the new database.

Post-Deployment Verification

Validate your self-hosted instance using the built-in health checks in src/server/lib/setup-status.ts:

  1. Health endpoint: Verify GET /api/health returns database connectivity and migration status.
  2. API validation: Open any keyword research page to confirm the DataForSEO key functions correctly.
  3. OAuth flow: If using Google Search Console, test the connection at Integrations → Connect with Google.
  4. Reverse proxy headers: For Docker, ensure your proxy forwards the correct HOST header required for Vite preview.

Disabling Telemetry

Self-hosted instances send anonymous usage heartbeats by default. Disable telemetry by adding one of the following to your .env (Docker) or .env.selfhost (Cloudflare):

OPENSEO_TELEMETRY_DISABLED=1

# or

DO_NOT_TRACK=1

Restart the container or redeploy the Worker after changing these values.

Summary

  • Clone and configure: Use corepack enable and pnpm install, then set DATAFORSEO_API_KEY in the appropriate .env file.
  • Choose deployment: Docker offers quick local hosting with compose.yaml, while Cloudflare Workers provides managed edge hosting via pnpm deploy:selfhost --yes.
  • Authentication: Docker requires external reverse proxy protection (no internal auth); Cloudflare uses Access policies.
  • Data migration: Use scripts/migrate-d1-to-postgres.ts to transfer data from D1 to Postgres with --dry-run verification.
  • Verification: Check /api/health, test DataForSEO connectivity, and validate Google OAuth flows before production use.

Frequently Asked Questions

Can I keep using D1 with a self-hosted Docker deployment?

Yes. While the Docker image defaults to D1-compatible SQLite, you can continue using D1 by maintaining the SQLite configuration. However, if you want to switch to Postgres for better concurrency or backup tools, run the migration script and set DATABASE_PROVIDER=postgres in your environment.

Is authentication required for Docker self-hosting?

No internal authentication is enforced when running in Docker (AUTH_MODE=local_noauth). You must place the container behind a reverse proxy (like Nginx or Traefik) with authentication or restrict access via VPN/network policies. The Cloudflare Workers self-hosted path, however, enforces authentication via Cloudflare Access.

How do I update my self-hosted instance?

For Docker, pull the new image tag and restart: OPEN_SEO_IMAGE=ghcr.io/every-app/open-seo:latest docker compose up -d. For Cloudflare Workers, pull the latest changes from the repository and rerun pnpm deploy:selfhost --yes to push updates to your Worker.

What happens to my data if I skip the D1 to Postgres migration?

If you deploy self-hosted without migrating, you start with a fresh database. Your historical keyword data, rankings, and Search Console metrics from the hosted instance remain in the original D1 database but will not appear in your self-hosted instance. You can run the migration script later, but it is easiest to migrate before switching production traffic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →