SQLite3

While Elixir applications on Fly.io normally run on Postgres databases, you can choose to run them on SQLite3. This guide will assume you have setup and configured Phoenix Application using ecto_sqlite3 running locally.

To make this work, you will need to place your databases on persistent Volumes as your deployment image will get overwritten the next time you deploy.

Volumes are limited to one host, this currently means that fly.io hosted Elixir applications that use SQLite3 for their database can’t be deployed to multiple regions.

But if you are okay using beta software, LiteFS could work for multi-region sync, check it out! But this guide is going to assume you have one node and one volume.

This guide is heavily influenced by the guide from Phoenix Core Team Member, Michael Crumm.

Following are the steps required to make this work:

Create volume

fly volumes create name

Replace name with your desired volume name. Only alphanumeric characters and underscores are allowed in names.

Optionally, you may specify the size of the volume, in gigabytes, by adding a --size int argument. The default volume size is 3 gigabytes.

Mount and Prep for Deployment

Add the following to your fly.toml, once again replacing the name with what you selected, this time in two places:

[mounts]
  source="name"
  destination="/mnt/name"

Next remove the release step from the [deploy] section of your fly.toml and add a DATABASE_PATH variable:

[env]
+ DATABASE_PATH = "/mnt/name/name.db"

-[deploy]
-  release_command = "/app/bin/migrate"

This step is required because a volume may not be ready once your application release runs, so to fix this we need to run migrations on application start. This can be accomplished by adding the following line to your lib/name/application.ex:

  @impl true
  def start(_type, _args) do
+   Name.Release.migrate()
    children = [

Deploy

These changes can be deployed:

fly deploy

Converting to SQLite3

Creating a new project with SQLite3 as the default is as simple as

mix phx.new my_app --database sqlite3

But if you’re working with existing code, here are the steps you’ll need to make on a Phoenix 1.6+ application.

Append the following your .gitignore to ignore SQLite database files:

# Database files
*.db
*.db-*

Replace the :postgrex dep from your mix.exs with ecto_sqlite.

def deps do
  [
    {:ecto_sqlite3, "~> 0.9.1"}, # Latest version at time of publication.
    # deps..
  ]
end

Update the repo configuration in config/dev.exs:

config :name, Name.Repo,
  database: Path.expand("../name_dev.db", Path.dirname(__ENV__.file)),
  pool_size: 5,
  show_sensitive_data_on_connection_error: true

Update the repo configuration in config/test.exs:

config :name, Name.Repo,
  database: Path.expand("../name_test.db", Path.dirname(__ENV__.file)),
  pool_size: 5,
  pool: Ecto.Adapters.SQL.Sandbox

And replace the DATABASE_URL in config/runtime.exs for production:

if config_env() == :prod do
  database_path =
    System.get_env("DATABASE_PATH") ||
      raise """
      environment variable DATABASE_PATH is missing.
      For example: /data/name/name.db
      """

  config :name, Name.Repo,
    database: database_path,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")

Finally, update your Repo to use the SQLite adapter in lib/name/repo.ex:

defmodule Name.Repo do
  use Ecto.Repo,
    otp_app: :name,
    adapter: Ecto.Adapters.SQLite3
end

SQLite3 Tips

Limitations

The ecto_sqlite3 documentation includes a good guide on the limits of using Ecto with SQLite3. We recommend at least skimming this before putting it into production.

Transferring Data from Postgres/MySQL to SQLite3

This section is something to give you a starting point on how to get from X to SQLite. Make sure to back up your data and be vigilant, because this kind of thing is fraught at best.

There is really no very easy way to do this since the data types between the databases are different…

But here is one way that has worked for me in the past and might work for you! The Ruby Sequel project comes with a command line tool for copying databases. This method should help when transferring between ADO, Amalgalite, IBM_DB, JDBC, MySQL, Mysql2, ODBC, Oracle, PostgreSQL, SQLAnywhere, and TinyTDS to SQLite3. That said it has limits! This is directly from the documentation:

This copies the table structure, table data, indexes, and foreign keys from the MySQL database to the PostgreSQL database.

Note that the support for copying is fairly limited. It doesn’t handle database views, functions, triggers, schemas, partial indexes, functional indexes, and many other things. Also, the data type conversion may not be exactly what you want. It is best designed for quick conversions and testing. For serious production use, use the database’s tools to copy databases for the same database type, and for different database types, use the Sequel API.

So YMMV on how useful this is for you. If you have a relatively simple database it might work great!

Install Ruby if you don’t already have it on your machine.

macOS

brew install ruby

or

Ubuntu

apt-get install ruby-full

The Ruby database adapter Sequel comes with a really slick command line tool for copying databases; let’s install that

gem install sequel 

Depending on your database, you may need to install an extra adapter. In this example we use Postgres, so let’s install that:

gem install pg

Next all we need to do is run the Sequel Copy command, where the first database is the source and the second database is the destination:

sequel -C postgres://localhost/database sqlite://name.db

And that’s it! If you open up your SQLite database using the command line it should have your tables and data all moved over. If you get an error here about connecting you will need to figure out which ruby gem handles your adapter and install it like we did for pg.

sqlite3 name.db

Note on types

SQLite doesn’t have official support for Postgres Arrays or Hstores, and most special datatypes! These will be copied into strings in the resulting SQLite table, so for Arrays the data will look like {item1, item2, item3} if you want to still use this as an array you need to use string manipulation to convert them to json and then it should work just fine. This is just one example, but the positive is that it’s all just strings in SQLite, so if you can make the string look like json you are set!

Copying an existing database to a fly volume

If you’ve exported or copied your database to SQLite you will need to get your database file up to Fly.io. To do this we will use the fly sftp command.

First open an sftp shell

fly sftp shell
»

And use the put command to transfer your file to the volume path.

NOTE: Because our server is running, we first need to give the database a new name. Do not try to put this file in the same place as your current DATABASE_PATH.

» put ./name.db mnt/name-prod.db

Check that it’s there:

» ls /mnt
name.db
name-prod.db
.... other files

Then ctrl-c to exit.

Finally, update your env in fly.toml:

[env]
- DATABASE_PATH = "/mnt/name.db"
+ DATABASE_PATH = "/mnt/name-prod.db"

And to persist that change:

fly deploy

Next time it boots it should use your new database!

Download a local copy of your production database

You can use the fly sftp get command to download your database locally.

fly sftp get /mnt/name/name-prod.db prod.db

Replace /mnt/name/name-prod.db with the path to your database (DATABASE_PATH).