Multi tenant applications

I’m looking for a simple way to run multi tenant apps on fly. By multi tenancy I mean the following: I have the same docker container for each tenant, the only difference is configuration. So for each tenant I want to create an independent Application from the same docker image but with different env variables.

One stupid way I could imagine is: Create an app per tenant via the cli. Deploy it and configure regions for the tenant. This would mean I have to build the docker image multiple times etc…

Ideally, I’d like to just make an API call and clone an app with different settings but the same image and I’m done. No extra build steps etc. I had a look at your GraphQL API but a lot of the fields are not well documented. There are some Strings like e.g. “source” on some type without any explanation.

Would be nice if you can improve your API docs and add some example use cases like described above.

1 Like

It sounds like one app per customer is the way to go. You can deploy prebuilt images from flyctl directly with flyctl deploy -i image-name where image-name is either a publicly accessible docker image or an image tag on the local machine. You can then use either secrets or env variables in fly.toml files to configure each app.

An API to clone apps is interesting, we’ll think about that. In the meantime, we’ve made our GraphQL API available to folks but it’s still early and evolving. We’re hoping to have it documented with stability guarantees before long.

Let’s think this through.
A user signs up with my service and wants to get started.
For that to happen I need to deploy a container for them.
So they click a button and I need to deploy.
I don’t want to build a workaround to use your cli in that scenario.
I just want to make an API call to your service and, hopefully, after a few seconds, the pre-built image is deployed and I have my IP.
I guess your cli + the fly.toml will do nothing else but make API calls in the end so I assume it should be possible without the cli. I just don’t want to reverse engineer it.

That totally makes sense. Here’s roughly the queries and mutations you’ll need to get the job done along with a link to the flyctl source as a reference. This is certainly a lot of steps, but your use case is neat and we’d like to make it easier someday.

1. Get your organization ID (flyctl)

You’ll need this node id when creating the app

query {
  organization(slug: "your-org") {
    id
  }
}

2. Create App (flyctl)

This creates a pending app. Note the app id and name are the same right now, but it’s best to pass the value from the ID field when calling mutations.

Variables:

{
 "organizationId": "your-org-id", 
 "name": "app-name"
}

Mutation:

mutation($input: CreateAppInput!) {
  createApp(input: $input) {
    app {
      id
      name
    }
  }
}

3. Prepare Image (flyctl)

Before deploying a docker image you’ll need to prep it with the optimizeImage mutation. This generates a flattened rootfs file from the image in the background. Since it’s async you’ll need to poll until you get the READY status. These rootfs files are reused for identical images, so you typically only see a delay when the image changes.

Variables:


{
  "appId": "your-app",
  "image": "your/image:latest"
}

Mutation:

mutation($input: OptimizeImageInput!) {
  optimizeImage(input: $input) {
    status
  }
}

4. Deploy (flyctl)

All you need to deploy is the app id, image reference, and definition. The definition is the JSON representation of the fly.toml file.

This mutation returns immediately while the app deploys in the background.

Variables:

{
  "appId": "your-app",
  "image": "your/image:latest",
  "definition": {
    "env": {
      "LOG_LEVEL": "info",
      "DATABASE_URL": "postgres://...",
    },
    "services": [
      {
        "protocol": "tcp",
        "internal_port": 8080,
        "concurrency": {
          "soft_limit": 20,
          "hard_limit": 25
        },
        "ports": [
          {
            "port": "80",
            "handlers": ["http"]
          },
          {
            "port": "443",
            "handlers": ["http", "tls"]
          }
        ],
       "http_checks": [
          {
            "interval": 10000,
            "timeout": 2000,
            "method": "get",
            "protocol": "http",
            "path": "/status",
            "tls_skip_verify": false,
            "headers": {}
          }
        ],
      }
    ]
   }
}

Mutation

mutation($input: DeployImageInput!) {
  deployImage(input: $input) {
    release {
      id
    }
  }
}

5. Check Deployment Status (flyctl)

You’ll need to poll to monitor the deployment. You can use an empty string as the deployment id to query the most recent deployment. Look at inProgress and successful to determine if the deployment is complete. Note that sometimes it takes a few seconds for the deployment to be created after calling the deployImage mutation.

Variables:

{
  "appName": "your-app",
  "deploymentId": ""
}

Query:

query ($appName: String!, $deploymentId: ID!) {
   app(name: $appName) {
   	deploymentStatus(id: $deploymentId) {
   		id
   		inProgress
   		status
   		successful
   		description
   		version
   		desiredCount
   		placedCount
   		healthyCount
   		unhealthyCount
   	}
  }
}

Thanks for taking the time. This looks very helpful. Is it possible to use a private image and supply a pull secret somehow?

Not yet but it’s something on our radar. In the meantime, you can use the fly registry to store the source image in an app in the same org since those are protected by auth.

For example…

flyctl auth docker
docker tag source-image registry.fly.io/source-app-name:latest
docker push registry.fly.io/source-app-name:latest

Then use registry.fly.io/source-app-name:latest as the image when deploying to another app in the same org.

That would work, thanks.

1 Like