I spent years avoiding "serverless" architecture. Deploying my applications to a server that I configured is a point of personal pride. And I do that... sometimes. But more often than not, I stop myself from building personal projects because I don't want to manage yet another server, and I absolutely don't want to pay for hosting.
Cloudflare's Developer Platform has a generous free-tier for serverless apps, a relational database built on top of SQLite, and a simple path to deployment. I already use Cloudflare for DNS on most of my projects, so I decided I'd give their platform a shot.
This post will cover 12 things I learned about building and deploying serverless sites using Cloudflare. The post was written in December 2024, so if you're reading this from the distant future some details may have changed.
1. Cloudflare has two competing offerings for building serverless websites: Workers and Pages
Cloudflare Workers is a serverless functions platform capable of running code and serving static assets. Cloudflare Pages is a static site platform that is capable of running serverless functions.
The differences between these two offerings is subtle because they have such similar capabilities. Each has its quirks though, and some of those will be discussed later in this post.
If you're like me, you'll try both and never be sure if you picked the right one for any given project.
2. If you want to put your Pages or Workers site on an apex domain, Cloudflare must manage its DNS
If you owned the domain example.com
and you wanted https://example.com
to point to your Cloudflare-hosted website, the DNS for that domain must be managed by Cloudflare.
You can point to Cloudflare sites on subdomains like www.example.com
or blog.example.com
without Cloudflare managing that domain's DNS. However, Cloudflare must manage a domain's DNS in order to point the apex domain (a domain without a subdomain) to a Workers or Pages site.
3. If you know that you're going to deploy a site to Cloudflare, it's worth using Cloudflare's tools to create it
Cloudflare has documentation for hosting various frameworks on both Workers and Pages. Each guide has two sets of instructions:
- How to set up a new project in your framework of choice to run on Cloudflare by using
npm create cloudflare ...
. - How to retrofit an existing project in your framework of choice to run on Cloudflare.
Having tried both, I'd recommend using npm cloudflare create
if you know that you're going to deploy your site to Cloudflare. It gives you everything you need out of the box while leaving most of the framework scaffolding untouched.
4. When you scaffold a site using npm cloudflare create
, you can run local versions of Cloudflare services
When I began building an Astro site that needed Cloudflare D1 to store form submissions, I wasn't yet sure if I could use D1 locally.
It turns out that when I scaffolded Astro using npm create cloudflare
, some kind of local emulation for Cloudflare's services was installed into my site. This enables the site to run Cloudflare's D1, R2, and KV locally. However, these services must be added to wrangler.toml
before they can be used.
Since I've only deployed Astro to Cloudflare, I'm not 100% certain that every framework works this way. That said, I'd guess that any framework with a Cloudflare adapter installed gets access to these tools regardless of how it was created.
5. To use a Cloudflare service locally, that service must have an ID in wrangler.toml
Before I was certain that I wanted to use Cloudflare's Developer Platform, I wanted to set up a local D1 instance for experimentation. I added the following to my wrangler.toml
file:
[[d1_databases]]
binding = "MY_DB"
database_name = "my-database"
When I started my site with npm run dev
, it crashed with the following error:
Processing wrangler.toml configuration:
- "d1_databases[0]" bindings must have a "database_id" field but got {"binding":"MY_DB","database_name":"my-database"}.
It turns out that databases must have a database_id
in wrangler.toml
in order to run locally. However, the ID doesn't necessarily need to be a valid ID of a D1 database that exists on Cloudflare. You can set database_id
to any non-blank string and the site will boot without issue:
[[d1_databases]]
binding = "MY_DB"
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
After that, you can create your first migration with the following command, replacing <DATABASE_NAME>
with the name of the database defined in wrangler.toml
:
npx wrangler d1 migrations create <DATABASE_NAME> <MIGRATION_NAME>
After you've filled in your generated migration file with the SQL statements that you want executed, you can run the migration on your local database with the command below. You must include the --local
flag to ensure it runs against the local database instead.
npx wrangler d1 migrations apply <DATABASE_NAME> --local
Once created, you can find the SQLite database file from the main project directory at .wrangler/state/v3/d1/<FILENAME>.sqlite
. The filename will be long and machine generated, but it will end with the .sqlite
extension. You can use a client like TablePlus to connect to it.
When it comes time to deploy your site, you'll want to create a real D1 database on Cloudflare and replace database_id
in wrangler.toml
with the ID of the real production database. You can create a database through Cloudflare's UI, or by running the following wrangler command:
npx wrangler d1 create <DATABASE_NAME>
When the command runs, it will give you toml to copy and paste into your wrangler.toml
file.
NOTE: If you know that you're going to deploy a D1 database to Cloudflare, it's worth creating the production database at the start of a project and putting its info into
wrangler.toml
. Cloudflare D1 is charged by rows written, rows read, and storage instead of a monthly fee. The free tier is generous, and creating an empty database costs nothing.
6. Compatibility with Node.js isn't 100%
Cloudflare Workers and Pages don't provide the Node runtime APIs by default. You can enable some of Node's runtime APIs by setting the following top-level configuration option in wrangler.toml
:
compatibility_flags = ["nodejs_compat"]
Unfortunately, this only enables a small subset of Node's runtime APIs. I wasn't able to use Nodemailer in one of my projects because it relied on Node features that aren't available in Cloudflare workers. This isn't just limited to Nodemailer: there is no way to do SMTP mailing with any library from a Cloudflare Worker: the protocol requires Node features that Workers don't implement–even when Node compatibility is enabled. I had to rewrite my email code to use Mailgun's API directly.
Hosting on Cloudflare means that you will occasionally run into compatibility issues. This is the cost of free hosting.
7. Cloudflare Pages do not allow you to set production environment variables in the Cloudflare UI
When deploying to Cloudflare pages, you must set any environment variables that will be used by the site in wrangler.toml
. You may set secrets in the Cloudflare UI, but secrets cannot be retrieved in the UI after they are written: only replaced.
This limitation can be frustrating if your project has data that's not quite a secret but you'd rather keep out of a public repository. For example, a site I built notifies a list of email addresses when a user submits a form. The recipients email addresses aren't quite a secret, but I don't want to commit them to version control. I'd also like to be able to easily check what email addresses currently receive notifications.
If I deploy this site to Pages, I'll have to set the email recipient list in one of the following ways:
- Put the recipient email addresses in my public repo where anyone could see them. While this is the simplest approach, the notification recipients might prefer to keep their email addresses private.
- Set the recipient email addresses as a secret in the Cloudflare UI. If I did this, I would not be able to check what emails are currently in the recipient list. Secrets cannot be retrieved via the Cloudflare UI after they are written: only replaced.
- Store recipient email addresses in D1 or KV. This feels like a lot of work for something that should be simple, but it might be the best option.
At the time of writing, Cloudflare Workers do not have this limitation. In Workers, you can set environment variables from within Cloudflare's UI.
8. Worker environment variables set in Cloudflare's UI will be overwritten by variables in wrangler.toml
With Workers, you can set environment variables in the Cloudflare UI. However, the default behavior is to overwrite them with variables set in wrangler.toml
on each deployment.
To prevent wrangler.toml
from overwriting your variables on deploy, set the following top-level configuration option in wrangler.toml
:
keep_vars = true
I wouldn't be surprised if keep_vars
disappears at some point in the future. Cloudflare appears to be moving towards configuration-as-code, and having environment variables stored outside of Git is at odds with this approach.
9. Environment variables aren't actually environment variables
In Cloudflare Workers, environment variables that are set via [vars]
in wrangler.toml
or in the Cloudflare UI aren't accessible via process.env
or import.meta.env
: they are provided to the site as a part of Cloudflare's top-level handler function.
How your framework makes these variables available may differ. In Astro, you can access these variables via context.locals.runtime.env
. Other frameworks may provide other options.
However, it's important to note that wrangler.toml
should not be used to store secrets. Locally, secrets should be added to a Git ignored .dev.vars
file in the main project directory. In production, secrets are added via the Cloudflare UI.
You can also use .dev.vars
to override variables set in wrangler.toml
when developing locally. Changes made to .dev.vars
may require restarting the server locally to resolve the correct value.
10. Pages deployments create publicly accessible "preview deployments" that stick around for awhile
When you push changes to a Pages site, it creates a "preview" URL on a subdomain of pages.dev
. Anyone with the URL can view that version of the site, and they are not deleted automatically when a new version of the site is released.
If that makes you nervous, Cloudflare has documentation that lays out how to restrict access to preview deployments through the Cloudflare UI. You can also manually delete previous preview deployments through the UI.
11. Previous deployments of workers are URL accessible
Similar to Pages' "preview deployments," previous worker deployments are URL accessible via a subdomain of workers.dev
. Anyone with the URL can view that deployed version of the site.
If that makes you nervous, you can disable them by adding the following top-level configuration option in wrangler.toml
:
preview_urls = false
There is no way to delete previous Worker deployments within the Cloudflare UI, but I believe that they are automatically removed after some duration of time (though I'm not 100% sure).
12. Initial deployments are SLOW
After running npx wrangler depoy
for the first time, Cloudflare will create a Worker or Page and provide you a preview URL immediately. If you visit that URL right away, you'll see a browser error.
It takes Cloudflare several minutes to set up a new project. Come back to it in a few minutes and you should have a working website.
I hope that you've found this post helpful as you explore hosting your own projects on Cloudflare. It has its quirks, but I've had a good experience so far.
The platform still seems to be evolving rapidly, so please comment to let me know if any of the information in the post has changed and I'll get it updated.
Top comments (0)