Skip to main content

Using Atlas with Fly.io

Fly.io is a platform for running full stack apps and databases close to the users. Under the hood, Fly converts Docker images (or other OCI-compliant image formats) to Firecracker microVMs. Fly allows the deployment of the app in different regions and route the requests based on the app load and user closeness.

Apps on Fly can be deployed in one of three ways: using a Dockerfile, a Docker image or a buildpack.

In this guide, we will demonstrate how Atlas can be used to perform database schema migrations for Fly.io deployments process using a Dockerfile. We will assume that you have already have a Fly project and are able to deploy it, if you are new to Fly, check the getting started guide.

Release command

When you configure your Fly project, you can define a release command. This command is executed during the release phase before the new version of your application is deployed. If it fails, i.e exits with a status code other than zero, Fly marks the deployment as failed as well. The release command is the recommended way to run database migrations on Fly.

note

The release command only allows executing commands that are present in the application image, meaning that we have to embed the Atlas binary with our application. We usually suggest a separate step for handling the migration, but since Fly currently does not support providing a separate image for the release phase, we recommend this solution.

Defining the Dockerfile

Using Docker multi-stage builds, we can compose lightweight images from multiple steps that use heavier base images. In this guide, we will use a Go app as an example. Because Go is a compiled language, so we can use a separate step for building the target binary and another for producing the runtime container. This way the runtime environment can be smaller, omitting the build environment.

Suppose our project structure is similar to the one below:

.
├── fly.toml
├── go.mod
├── go.sum
├── main.go
└── migrations
├── 20221220000101_create_users.sql
└── atlas.sum

Our objective is to build an image that contains the Atlas binary, the database migrations and our application code. For our Go app the Dockerfile can be defined as:

Dockerfile
FROM arigaio/atlas:latest-alpine as atlas

# build stage
FROM golang:1.19.2-bullseye as build
WORKDIR /build
ADD go.mod /build/go.mod
ADD go.sum /build/go.sum
ADD main.go /build/main.go
RUN CGO_ENABLED=0 go build -o app main.go

# runtime stage
FROM alpine
COPY --from=atlas /atlas /atlas
COPY migrations /migrations

COPY --from=build /build/app /app
CMD ["/app"]

If you are using another compiled programming language, most of the time you will only have to change the build stage. If your application requires a runtime you may have to change the final stage as well.

info

It's important for the final image to have a shell that is capable of environment variable interpolation, like sh or bash. This is mostly due to behavior on Fly's side, where expanding environment variables don't currently work correctly on the release command.

Setting the database URL secret

While running the migration, Atlas needs to know the URL for the database. Fly has support for defining secrets, sensitive values that are available during runtime as environment variables. We can define the database URL using the command below:

flyctl secrets set DATABASE_URL="postgres://postgres:pass@0.0.0.0:5432/database?sslmode=disable"

If you use the flyctl command postgres attach the secret will be created automatically for you.

Configuring fly.toml

To tell Fly to execute the release command during a deployment, we need to add a deploy block with the release command provided:

fly.toml
[deploy]
release_command = "sh -c '/atlas migrate apply --url $DATABASE_URL'"

With the release command defined, during new deployments a new temporary VM will be created and will execute the release command. If the commands succeed the deployment will continue, in case of failures the deployment will be aborted.

Deploying the app

We can deploy the app with the command flyctl deploy. Fly will use a Docker installation (or a remote builder) to build the Docker image and push to the Fly registry.

The output of the release command will be presented to you on your terminal, but if you missed it, you can use the Monitoring page of your app or the fly logs command to see the previous logs entries.

Atlas will provide helpful information during the execution, here are a few examples of logs outputs:

Preparing to run: `/atlas migrate apply --env prod` as root
Migrating to version 20221220000101 (1 migrations in total):
migrating version 20221220000101
-> CREATE TABLE "public"."users" ("id" integer NOT NULL, "name" character varying(100) NULL, PRIMARY KEY ("id"));
-- ok (6.204945ms)
-------------------------
-- 12.69747ms
-- 1 migrations
-- 1 sql statements

Improving the deployment pipeline

You can always improve the deployment pipeline by leveraging Atlas and Fly GitHub Actions. For additional insights on the database schema and migrations, we recommend giving Atlas Cloud a try.