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:
- drangler, with Node 24 or newer:
npm i -g @drupflare/drangler. It usessshandwrangler, whichnpm i -g wranglerinstalls. - SSH access to the server as a user who can run
drushin 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. - A Cloudflare account, signed in through wrangler.
wrangler loginopens a browser to approve access, anddrangler cf whoamiconfirms which account drangler will use. On a machine with no browser, such as CI, setCLOUDFLARE_API_TOKENinstead.
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/andlibraries/. Composer libraries the duplicate lacks are installed from the registry after the claim; any that fail are listed as ready commands. - Settings: literal
$configoverrides, module settings written as literal$settingsvalues, 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
\Redisstand-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.txtand.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.
| Flag | What it does |
|---|---|
--host | the server, typed the way you would for ssh: user@host, or user@host:port |
--root | the Drupal root on that server, when you would rather not have it found |
--site | which site to take when the server runs several |
--identity | an SSH key file, when it is not your default key |
--dry-run | list every step and command, and run none of them |
--full | run 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-interval | minutes between polls of a followed repository; 5 by default, and the least |
--carry-key <name...> | bring a blanked credential into the duplicate |
--hold-mail | on 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 |
--deploy | put the duplicate on your Cloudflare account, behind Access, not your computer |
--access-email | with --deploy, who may open the copy; the wrangler login by default |
--source-url | the live site's address, so each checked page must match its status there |
--out | where the copy is kept; .drangler/preview/<host> by default |
--port | the 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.
| Variable | Same 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.