Defining and Using Multiple Flyway Environments in TOML
This article shows how to define environments in your TOML files, use resolvers to provide secure connection details, and configure per-environment overrides and placeholders. It explains how this approach simplifies automation, makes CI/CD pipelines easier to manage, and helps teams work consistently and securely across multiple databases.
This article is part of a series on using TOML configuration, environments, and resolvers in Flyway:
- Getting Started with TOML Configuration in Flyway
- Managing Credentials Securely with Flyway Resolvers
- Defining and Using Multiple Flyway Environments in TOML – you are here
- Using Flyway Environments and Resolvers to Manage Many Databases – coming soon
- Double‑checking your Current Flyway Database Environment – coming soon
Flyway’s support for TOML-based configuration simplifies how development teams manage multiple database environments. When used with resolvers, we can safely define environments as project-level resources, while maintaining individual user logins. This greatly simplifies team development while keeping it secure.
Instead of juggling separate config files or repeating settings, we can now use the [environments]
namespace to define all our database environments, such as ‘development’, ‘test’, and ‘staging’, each with different credentials and connection details, in a single TOML file. After that, we can then reference them in the project simply by name.
What makes environments even more powerful is the nested [environments.<name>.flyway]
namespace, where you can override or extend Flyway’s default settings for each environment. This lets you tailor configuration to the database’s capabilities, the stage of development, or the specific needs of your team, all without rewriting scripts or duplicating settings.
Defining and configuring database environments
Flyway now reads its configuration from flyway.toml files, which support several namespaces. Alongside the familiar [flyway]
namespace (carried over from the old .conf format), you can use the [environments]
namespace to define all your environments – such as dev, test, or staging – and their settings, within a single TOML file in the user area. Previously, the old flyway.user.conf allowed only a single connection, now represented as the default environment.
By keeping configuration in one place, and letting it be overridden or extended per environment, it’s much easier to adapt Flyway’s behavior across databases and platforms
Defining multiple environments
Flyway makes it easy to define multiple environments, each representing a different stage of the workflow, simply by specifying values directly in the [environments]
namespace in your flyway.toml file. You can configure distinct settings, credentials, and behaviors for each environment. This gives teams the flexibility to support local development, customer-specific testing, automated CI/CD pipelines, or production-like staging, all from a single, structured configuration file. For instance, you might have:
- Develop: A local development database with debugging enabled.
- Tenant2Dev: An isolated development database for a specific customer tenant or feature.
- Test: A shared testing environment managed by automated CI/CD pipelines.
- Staging: A near-identical replica of the production environment.
You can define environments in either your flyway.user.toml file or, for teamwork, your project-level flyway.toml.
Here’s a simple example where we define two environments, dev and test. We specify the connection details for each database environment (URL, user, password) and the schemas we want Flyway to manage. We can also override the defaults for certain connection behaviors like number of retries, but we won’t do that here.
Since we’re hardcoding credentials in this example, this configuration would need to be stored in the flyway.user.toml file in the secure user-area of the user’s workstation:
1 2 3 4 5 6 7 8 9 10 |
[environments.dev] url = "jdbc:sqlserver://localhost:1433;databaseName=MyDevDB;encrypt=true;trustServerCertificate=true" user = "dev_user" password = "dev_password" schemas = ["dbo", "classic", "people", "accounting"] [environments.test] url = "jdbc:sqlserver://test-db.company.com:1433;databaseName=MyTestDB;encrypt=true;trustServerCertificate=true" user = "test_user" password = "test_password" schemas = ["dbo", "classic", "people", "accounting"] |
Defining environments securely
Instead of storing sensitive configuration values directly in your configuration files, as in the simple example above, it is far more secure if your TOML files instead contain resolvers that tell Flyway how to fetch from a secure store. Here is the dev environment, defined more securely using resolvers:
1 2 3 4 5 |
[environments.dev] url = "${localSecret.Pubs_SQLServer_dev_localhost_url}" user = "${localSecret.Pubs_SQLServer_dev_localhost_user}" password = "${localSecret.Pubs_SQLServer_dev_localhost_password}" schemas = ["dbo", "classic", "people", "accounting"] |
I use long names for each resolver, to distinguish the project (Pubs), RDBMS (SQL Server), branch (dev), server (localhost) and the actual secret I’m storing, such as the password. It is otherwise easy to get in a muddle.
Flyway will, at runtime, substitute in the actual values from the specified secrets store (in this case, Credentials Manager in Windows). Using resolvers also means database environments can be safely defined in a shared project directory, keeping the team in sync with configuration changes while each user still resolves their own credentials locally.
By segregating configurations into environment-specific namespaces, and using resolvers, sensitive data such as production database credentials can be isolated and protected within the definition of the ‘environment’. This separation also helps with compliance with your organization’s regulatory requirements. Perhaps the greatest advantage is that the actual credentials can be changed regularly in line with current security guidelines without altering the TOML configuration file.
Configuring environment-specific behaviors
Within the nested [environments.<name>.flyway]
namespace, we can override or extend Flyway’s default settings for an environment, as well as establish environment-specific behaviors. For example, we can:
- Set per-environment migration locations — perhaps to point to a different list of Migration folders for a feature branch or for a regional database variant.
- Override general Flyway behavior — such as selectively enabling the
clean
command only in development environments, by settingcleanDisabled = false
. - Define environment-specific placeholder values — for example, a
${schemaname}
placeholder in your migration scripts, defined within the Flyway namespace by placeholder calledschemaName
, could resolve to different schemas in dev and prod.
Here’s how this might look in a TOML file, for the development (dev
) environment:
1 2 3 4 5 6 7 8 9 10 |
[environments.dev] url = "${localSecret.Pubs_SQLServer_dev_localhost_url}" user = "${localSecret.Pubs_SQLServer_dev_localhost_user}" password = "${localSecret.Pubs_SQLServer_dev_localhost_password}" schemas = ["dbo", "classic", "people", "accounting"] [environments.dev.flyway] locations = ["filesystem:./Branches/develop/Migrations/Sql"] cleanDisabled = false placeholders.branch = "develop" placeholders.schemaName = "dev_schema" |
What you can and can’t override
Generally, Flyway settings within the [flyway]
namespace can be overridden in [environments.<name>.flyway]
. However, there are some exceptions. Some settings that you might want to set per-environment, like environment
, cherryPick
, check
, email
, token
, licenseKey
, and undoSqlMigrationPrefixare
can’t be overridden because they are processed before Flyway evaluates the selected environment.
On top of the official list there are some other settings that you can’t override, either because they aren’t in the flyway
workspace, or because it’s impossible within the context:
- Logging/output settings – e.g., log level or format settings that are processed too early to be environment-specific
- Configuration file paths – such as
configFiles
,workingDirectory
, andconfigFileEncoding
, which control how and where Flyway loads its config files - Display settings – like
flyway.outputType
andflyway.color
, which affect how command output is shown - RDBMS-specific configuration blocks – like
[flyway.oracle]
,[flyway.sqlserver]
,[flyway.postgresql]
. These are not merged or resolved per environment - Secrets manager integrations – including Vault (
vaultSecrets
,vaultToken
,vaultUrl
), Dapr (daprSecrets
,daprUrl
), and GCP Secret Manager. These are configured globally
Making the most of Flyway environments
Flyway environments don’t just simplify secure, per-environment configuration, they also make many aspects of Flyway-based development and deployment easier to manage and scale.
Simplified CLI Commands
Having configured our environments, it becomes very simple to switch between them in a series of Flyway commands. Instead of redefining configurations for each command, we specify the desired environment using a parameter, such as:
flyway -environment=test migrate
Easy integration with CI/CD pipelines
With the [Environments]
namespace, and the ability to customize Flyway’s default settings for each environment, Flyway fits naturally into a scripted development process or CI/CD pipeline.
Instead of injecting environment-specific settings via scripts or environment variables, you can define all necessary configurations (connection details, placeholder values, migration locations, etc.) for each environment in your TOML file. Then, when your pipeline runs Flyway, it simply specifies the appropriate environment name, and Flyway uses the correct settings automatically.
This is very useful when you require automated database migrations, tailored to the specific environment being deployed.
Handling rapidly changing authentication
I like to use short-term database containers for development, which means credentials can change frequently. This was once awkward for Flyway because TOML files can’t be updated automatically in place. With resolvers, Flyway can now cope easily with rapidly changing authentication: you just update the value in your secrets store manually, via the UI, or through a script that automatically updates the credentials when you create a new container. I’ve shown how to update Credential Manager values using PowerShell in Managing Many Databases with Flyway Environments and Resolvers.
Working with multiple environments
Flyway environments are flexible. You can define them once and reuse them, or even generate them on demand. Where you define them depends on your workflow:
- User-level TOML files are handy for personal setups or cross-project automation.
- Project-level TOML files (under source control) are ideal for shared team environments.
- Dynamic configs are useful for ephemeral setups, like containers or one-off tasks.
Flyway loads configuration in order – system-level first, then user-level, then project-level – so you can always override settings if needed, either in the project TOML or by supplying an external config file dynamically.
User-level or project-level configuration
Before Flyway introduced resolvers, Flyway Environments that defined credentials or connection information had to be stored in the flyway.user.toml file within the user area. With resolvers, you no longer need to hardcode secrets, so you can safely store most shared environments in the project directory under source control. This way, everyone uses the same configuration, but developers still get individual logins.
Even so, I still like to define some environments entirely in my user-level TOML because it gives me a central list of all the environments I care about, across multiple projects. This makes it much easier to script cross-project automation operations like:
- Running flyway
info
on every dev database - Backing up all Staging databases across projects
- Verifying the schema version across all test environments
If you stored only project-level configs, you’d have to iterate through each project folder, read its TOML individually, and merge separate environment lists.
Nevertheless, some tasks remain awkward. Flyway expects commands to run from the project directory, so automation scripts must move into the right folder first. The only way to automate that cleanly is to store the project path as a placeholder in each environment definition and retrieve it programmatically. It would be useful to be able to query the list of all defined environments, but Flyway doesn’t currently offer a built-in way to do this.
Generating the config dynamically
For more ephemeral configs, such as when working with short-lived containers or for one-off tasks, you can generate TOML files on the fly in scripts and point Flyway to them with the -configFiles
parameter. These ephemeral configs don’t live in source control – they’re created as needed, used once, and discarded.
Here’s an example of my own environment definitions that I generated dynamically using PowerShell. I then run flyway
info
against each of them to record their status (not shown here – I’ll cover that in the next article).
1 2 3 4 5 6 7 8 9 10 11 |
[environments.developSQL2017] url = "`${localSecret.Pubs_SQLServer_Main_Philf01_url}" user = "`${localSecret.Pubs_SQLServer_Main_Philf01_user}" password = "`${localSecret.Pubs_SQLServer_Main_Philf01_password}" schemas = [ "dbo", "classic", "people", "accounting" ] flyway.locations = ["filesystem:$($env:FlywayWorkPath -replace '\\','\\')\\Pubs\\Migrations"] flyway.placeholders.branch = "Main" flyway.placeholders.DSN = "PubsDSN" flyway.placeholders.canDoStringAgg = "1=1" flyway.placeholders.projectDescription = "A sample team-based Flyway project" flyway.placeholders.projectName = "Pubs |
While Flyway expects override settings to live in [environments.<name>.flyway]
, as demonstrated earlier, it also supports placing certain settings directly within the [environments.<name>]
block, as I’ve done here.
Overriding values at runtime
Sometimes, you find that some or all values for an environment aren’t known until runtime. Flyway can handle this in a few ways, without editing static TOML files.
You can pass or overwrite their values at the command line using conventional dotted notation (one long line)
flyway info -environment=develop -'environments.develop.url'='myURL -'environments.develop.user'=PhilFactor -'environments.develop.password'=ratherSecret
Alternatively, you can create a temporary TOML file dynamically (e.g., in a script) and pass it to Flyway using the -configfiles
parameter.
flyway -configFiles=/path/to/temp.toml info
You can also pass them in the STDIN stream of Flyway. This allows you to ‘ use encrypted TOML files, files with temporary values such as passwords, and to iterate through a list of environments. This is particularly useful when you need to customize paths, connection details, or other settings per environment, without manually editing configuration files.
Adapting to the environment using placeholders
So far, we’ve discussed how environments help manage connection details and Flyway settings across multiple environments. We can take things a step further by using environment placeholders to adapt the actual behavior of migrations for each environment.
Placeholders are variables Flyway replaces at runtime. By defining them differently per environment, you can write one migration script that runs safely everywhere, even when different environments have slightly different requirements.
This is useful, for example, when:
- A newer SQL feature is supported only in later versions of your RDBMS
- A linked-server exists in staging but not in development
- You need to create variants, such as variants of an accounting package, for different legislative areas
For example, SQL Server 2017 and later supports STRING_AGG()
, but earlier versions don’t. Instead of maintaining two separate scripts, you can define a simple canDoStringAgg
placeholder that resolves to 1=1 (true) in environments that support STRING_AGG
, and 1=0 (false) otherwise.
1 2 3 4 5 |
[flyway.placeholders] canDoStringAgg = "1=0" # safe default works on all supported versions [environments.dev.flyway.placeholders] region = "US" canDoStringAgg = "1=1" # override |
In the flyway.placeholders
namespace we define a default ” of ‘false’, so the code will be a legacy XML technique that works on any SQL Server version we support. The[environment.<name>.flyway.placeholder]
namespace can override this, if the environment is running SQL Server 2017 or later.
Then we can use a single migration script, with the $canDoStringAgg
placeholder, to select the appropriate variant of the code of a view, procedure or function, depending on the version of SQL Server that is running in the environment. This is all done inline and is invisible to the user.
The ShouldExecute technique
I’ve demonstrated in a previous article how to do the same trick using Flyway’s ShouldExecute
script configuration. Here, the legacy XML version of the code is used by default and the String_Agg
version is overlaid using the ALTER
syntax, if canDoStringAgg
is set to true
. It requires a special version file and a conf file for each new database feature, which can get cluttered. With environment placeholders, it all becomes much simpler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
DECLARE @sql NVARCHAR(MAX); IF (${canDoStringAgg}) BEGIN SET @sql = ' ALTER VIEW dbo.TitlesAndEditionsByPublisher AS /* A view to provide the number of each type of publication produced */ SELECT publishers.pub_name AS publisher, title, STRING_AGG(Publication_type + '' ($'' + Convert(VARCHAR(20), price) + '')'', '', '') AS ListOfEditions FROM dbo.publishers INNER JOIN dbo.publications ON publications.pub_id = publishers.pub_id INNER JOIN editions ON editions.publication_id = publications.Publication_id INNER JOIN dbo.prices ON prices.Edition_id = editions.Edition_id WHERE prices.PriceEndDate IS NULL GROUP BY publishers.pub_name, title;'; END ELSE BEGIN SET @sql = ' ALTER VIEW dbo.TitlesAndEditionsByPublisher /* Titles And Editions By Publisher */ (Publisher,Title,ListofEditions) AS /* A view to provide the number of each type of publication produced Select * from [dbo].[TitlesAndEditionsByPublisher] by each publisher*/ SELECT publishers.pub_name AS publisher, publications.title, Stuff( ( SELECT '', '' + editions.Publication_type + '' ($'' + Convert(VARCHAR(20), prices.price) + '')'' FROM editions INNER JOIN dbo.prices ON prices.Edition_id = editions.Edition_id WHERE prices.PriceEndDate IS NULL AND editions.publication_id = publications.Publication_id FOR XML PATH(''''), TYPE ).value(''.'', ''nvarchar(max)''), 1, 2, '''' ) AS ListOfEditions FROM dbo.publishers INNER JOIN dbo.publications ON publications.pub_id = publishers.pub_id;'; END EXEC sp_executesql @sql; |
Conclusion
Flyway’s [environments]
namespace provides a straightforward, practical way to manage configuration across multiple databases and environments. By defining connection details and settings in one place and separating out secrets, it becomes much easier to work consistently and securely as a team.
It’s a feature that requires a bit of initial discipline but where the reward is fewer errors, better automation, and a configuration setup that’s easier to understand and maintain. Whether you’re managing a single development database or a more complex enterprise system, this approach makes life simpler.
In the next article, I show how to define a canonical source of environment data, sync it with Windows Credential Manager, and run Flyway commands securely across several environments, without hardcoding anything.