Skip to Content

Bring the Site You Already Have

Most organisations do not need a new website. They need the one they have to stop costing a server, a patch window and a support contract. Drupflare takes an existing Drupal site as it is, checks it before anything moves, and leaves the door open in both directions.

๐Ÿงฐ Before You Start

Everything runs on your own computer. drangler is installed on your laptop or workstation, not on the server. It connects to the server over SSH, runs a short list of read-only commands there, and copies the results back.

You need three things:

  1. drangler, with Node 24 or newer: npm i -g @drupflare/drangler. It uses ssh and wrangler, which npm i -g wrangler installs.
  2. SSH access to the server as a user who can run drush in the Drupal root. This is the test:
    ssh [email protected] 'cd /var/www/html && drush status'
    

    If it prints the Drupal version, drangler can read the site.
  3. A Cloudflare account, signed in through wrangler. wrangler login opens a browser to approve access, and drangler cf whoami confirms which account drangler will use. On a machine with no browser, such as CI, set CLOUDFLARE_API_TOKEN instead.

Then check everything at once. This checks your machine, then runs the read-only survey against the server and names anything missing:

drangler doctor --source [email protected] --root /var/www/html

Finding the Drupal root. You do not have to. Without --root, drangler preview reads the server's Apache or nginx configuration, picks the document root that holds Drupal's index.php, and finds the Composer project above it, the project's own drush, the web server's user and the sites/ directory, so a site that lives somewhere unusual is found from what is on the server. If you want to name it, --root is the directory with index.php and sites/ in it, usually /var/www/html or /var/www/<site>/web. drush status prints it as Drupal root. When one server runs several Drupal sites, --site <hostname> says which one.

If your code is in git. A preview copies the code on the server's disk, including edits nobody committed. To see whether the server matches your repository first:

ssh [email protected] 'cd /var/www/html && git status --short && git log -1 --oneline'

An empty git status means the server holds exactly the commit printed below it.

โšก One Command: drangler preview

drangler preview does the whole move for you. It surveys the current server, explains what will change, converts the database, copies the files, and brings up a working duplicate of the site, either on your own machine or on your Cloudflare account, so you can click through it before deciding anything.

drangler preview --host [email protected]

A preview is a duplicate, never a replacement. Every command it runs on the old server is read-only, the database and files are streamed back, and the live site keeps serving the whole time. It asks before each step. --full runs every step after a single confirmation, --confirm <host> runs every step with no prompt, and --dry-run shows every step and command without running any of them.

What the duplicate carries:

  • The database, from MySQL, MariaDB or PostgreSQL, with the administrator accounts and passwords it already had.
  • Uploaded files, public and private. Image styles and aggregated CSS and JS are rebuilt instead of copied.
  • Custom and contrib code in modules/, themes/, profiles/ and libraries/. Composer libraries the duplicate lacks are installed from the registry after the claim; any that fail are listed as ready commands.
  • Settings: literal $config overrides, module settings written as literal $settings values, and a Redis connection as a Redis URL.
  • Application state kept in Redis, such as mailed tokens and markers that stop a notification repeating. A database dump cannot hold it, so preview reads the keys with one fixed read-only script and stores them in the duplicate's own database, where a \Redis stand-in serves them.
  • Server rules: unconditional redirects and response headers from .htaccess. Anything more involved is listed for you to review.
  • Docroot files a browser fetches by name, such as search-engine verification pages, ads.txt and .well-known/.

It ends by checking the duplicate against the source: content and file counts, then the front page, the login page, a node, a public file and any page you add with --check. Given the live site's address with --source-url, each page must answer with the same status there, and a JSON page must return the same keys. A page the source answers from your own Cloudflare Access is reported as not compared, because the edge answered there and Drupal did not. drangler preview needs drangler 0.4 or later and Drupflare 1.0.4 or later, which sends the request headers a headless site reads. Discovery, credential handling, --follow and deploys behind Cloudflare Access arrived in drangler 0.4; the command itself has been there since 0.3.

Reading a Dry Run

Add --dry-run to see the whole plan before anything happens. Nothing is executed and nothing is copied:

$ drangler preview --host [email protected] --dry-run
dry run against [email protected]; nothing was executed

1. Find the Site on the Host
2. Survey the Source
...
12. Check the Duplicate

read-only commands on the host
  $ php -v
  $ cd /var/www/html && drush status --format=json
  $ tar -cf - -C /var/www/html/sites/default files
  ...

local commands
  $ drangler migrate install --db .drangler/preview/old.example/site.sqlite --repack ...
  $ bunx wrangler dev --port 8787

The numbered list is the twelve steps it will ask about. Read-only commands on the host run on the server over SSH, and every one passes an allow-list first, so a command that could write is refused before it leaves your computer. Local commands run on your computer.

FlagWhat it does
--hostthe server, typed the way you would for ssh: user@host, or user@host:port
--rootthe Drupal root on that server, when you would rather not have it found
--sitewhich site to take when the server runs several
--identityan SSH key file, when it is not your default key
--dry-runlist every step and command, and run none of them
--fullrun every step after one confirmation, instead of asking before each
--confirm <host>run every step with no prompt; the value must name the host, as typing it would
--check <path...>pages to compare with the live site, beside the front page and the login page
--follow <module=repo...>keep a module current from a git repository, as mymodule=my-org/mymodule
--follow-intervalminutes between polls of a followed repository; 5 by default, and the least
--carry-key <name...>bring a blanked credential into the duplicate
--hold-mailon your own machine, queue the duplicate's mail and never send it
--edge [branch]use a prerelease worker: the tip of a branch (master when bare), from the CDN
--plan <free or paid>the Workers plan the duplicate models
--deployput the duplicate on your Cloudflare account, behind Access, not your computer
--access-emailwith --deploy, who may open the copy; the wrangler login by default
--source-urlthe live site's address, so each checked page must match its status there
--outwhere the copy is kept; .drangler/preview/<host> by default
--portthe local port for the duplicate; 8787 by default

Answering Without a Prompt

Every choice has a flag, a DRANGLER_PREVIEW_<NAME> variable and a prompt on a terminal. A flag wins over a variable, a list is comma separated, a switch is 1, and a question nobody can answer is refused instead of left waiting, so a CI job never hangs.

VariableSame as
DRANGLER_PREVIEW_ROOT--root
DRANGLER_PREVIEW_SITE--site
DRANGLER_PREVIEW_CONFIRM--confirm
DRANGLER_PREVIEW_PLAN--plan
DRANGLER_PREVIEW_CHECK--check
DRANGLER_PREVIEW_FOLLOW--follow
DRANGLER_PREVIEW_FOLLOW_INTERVAL--follow-interval
DRANGLER_PREVIEW_CARRY_KEYS--carry-key
DRANGLER_PREVIEW_ACCESS_EMAIL--access-email
DRANGLER_PREVIEW_DEPLOY--deploy (1)
DRANGLER_PREVIEW_HOLD_MAIL--hold-mail (1)
DRANGLER_PREVIEW_EDGE--edge

What Preview Decides for You

A copy of your site should not be able to act as your site. Sites often keep secrets in Drupal's own configuration: payment keys, the key to a backend API, a mail login, OAuth client secrets. They travel in the database, and a copy that holds them can charge cards, write to your API or push to your users' phones. Preview finds every one of them, empties them in the duplicate while the database stays as it was, and lists them by name. Reads that need no key still work and anything that needs one fails closed. --carry-key mantle2_api_key brings one back on purpose.

Stylesheets and scripts. Drupal's own CSS and JS aggregation cannot run on a Worker, and a site that had it on loses its theme without it. Preview turns it off in the duplicate and tells you when your site had it on. The worker combines files at the edge instead.

Mail. A mail setup that opens its own connection to a mail server, such as the SMTP module under Mail System, cannot run on a Worker, so preview moves it onto the worker's mail layer. With the logins blank that layer has no relay and the copy sends nothing until you give it one. On your own machine --hold-mail queues each message instead, so you can read a verification code from the queue.

Cloudflare Access. --deploy first creates an Access application on the workers.dev address, with a policy for one email address and a service token for drangler, and waits until the edge turns strangers away. Only then does it upload. Nothing in DNS changes. The API token needs Access edit rights as well as Workers; without them preview stops before it creates or uploads anything and says which permission is missing.

A slow first start. A new site unpacks its whole database the first time it starts, and on a large one the local runtime can end during it. Preview starts it again if it ends, up to five starts in all, and counts the site up after two good answers in a row.

Your own modules. --follow mymodule=my-org/mymodule adds the repository once the copy is claimed and the site polls it every five minutes, so a push to the repository reaches the copy. A public repository needs no token. The repository's vendor/ folder is never delivered, so a change that adds a Composer package needs the module's own CI to run drangler build --project.

The steps below are what it runs, and each is also a command of its own.

๐Ÿ”Ž 1. Survey, Without Touching Anything

drangler migrate survey connects to the current server over SSH and runs a short list of read-only commands: PHP and Drupal versions, the database, enabled modules, files, and content counts. Nothing is written. Add --dry-run to see every command before you hand over a key.

drangler migrate survey --host [email protected] --root /var/www/html --out survey.json

--out saves the result as a file the next steps read. If the connection drops part-way, --resume re-runs only the steps that did not finish. In a dry run, a step marked (optional) is one whose failure does not stop the survey: a missing file_managed count, for example, is recorded and the survey carries on.

๐Ÿšฆ 2. Get a Verdict Before You Commit

drangler migrate eligibility turns the survey into one of three answers: GO, GO WITH CHANGES, or NO, and it names the reason behind every finding. Anything it could not measure is reported as unmeasured, never quietly passed.

drangler migrate eligibility --survey survey.json
drangler migrate plan --survey survey.json --to workers

migrate plan orders the work: which modules move as they are, which need a service provisioned, which cannot run on Workers and what replaces them.

A Drupal 10 site is planned as a major upgrade on the way in. The plan names every enabled contrib module that has no Drupal 11 release, and the database updates run after the move with drangler site updb. Drupal 10 reaches end of life on December 9, 2026.

๐Ÿ” 3. Convert the Database

A MySQL, MariaDB or PostgreSQL dump converts to the SQLite a Drupflare site runs on (--from pgsql reads a pg_dump). The dump comes from the server and the rest runs on your computer, which needs sqlite3 installed:

ssh [email protected] 'cd /var/www/html && drush sql:dump --extra-dump=--hex-blob' > site.sql

The converter refuses what it cannot translate exactly and names the statement, instead of producing a dump that looks complete and fails later.

drangler migrate convert --from mysql --to sqlite --in site.sql --out site.sqlite.sql
sqlite3 site.sqlite < site.sqlite.sql
drangler migrate install --db site.sqlite --repack

Every file it replaces is backed up and verified first, and drangler migrate restore puts a backup set back with one command.

๐Ÿ“ฆ 4. Bring the Code

The database names the modules a site runs, and the code arrives separately, in two parts. The contrib packages in the project's composer.lock go up with drangler build --project, which runs composer on your machine, applies the project's patches, including patched Drupal core, reaches its private repositories, and uploads only what the shipped pack does not already carry. The install profile, custom modules and themes go up with drangler modify upload, which boots Drupal on the site before a revision goes live and rolls back one that would fatal.

drangler site claim https://my-site.example --title "My Site"
drangler build --project ~/work/my-site --site https://my-site.example --yes
drangler modify upload --dir ~/work/my-site/web/profiles/custom/my_profile --site https://my-site.example --yes
drangler site updb https://my-site.example --steps 200 --no-snapshot --yes

๐Ÿš€ 5. Deploy and Cut Over

Deploy to your Cloudflare account, then switch traffic with a short read-only window. drangler migrate cutover prints the checklist a person confirms, and drangler migrate delta carries the content that changed during the move, and prints the statement that re-seeds the id counters so the first new node on the new site does not collide with an old one.

๐Ÿงช Check It Yourself

Thunder is one of the Verified codebases on the fixtures page. You can rebuild that result on your own machine, from an empty checkout to a scored site, with the steps in Reproducing Thunder.

๐Ÿšช Leaving Is Part of the Plan

A site can move back to an ordinary server at any time. drangler migrate export pulls the database out, migrate convert turns it back into MySQL, and migrate eligibility --to vps lists the few settings a conventional host needs instead. Uploaded files are copied separately. No lock-in is a promise you can test on day one.

drangler migrate export --url my-site.example --out worker.sql
drangler migrate convert --from sqlite --to mysql --in worker.sql --out vps.sql

๐Ÿ›๏ธ Built for Organisations That Cannot Rebuild

Universities, councils and non-profits often run dozens of small Drupal sites that were built years ago and still matter. Rewriting them is not on anyone's roadmap. Drupflare keeps them as they are, removes the servers underneath, and makes every step reversible.