Introduction
Database development often creates a gap between the local development environment and the cloud environment where an application eventually runs.
A .NET developer might build an application against SQL Server locally and then deploy it to Azure SQL Database. The application may work correctly during development, but database behavior, available features, configuration, or deployment assumptions can differ once the application reaches the cloud.
That creates the familiar problem:
Works on my machine
|
v
Deploy to cloud
|
v
Unexpected database behavior
Azure SQL Developer is designed to address part of this problem by providing the Azure SQL Database engine in a local container. The current offering is in preview and is intended for local development and CI scenarios. The goal is to let developers work against an Azure SQL Database-compatible engine without requiring a continuously available cloud database during the development loop.
This creates an interesting development model:
.NET Application
|
v
Azure SQL Developer Container
|
v
Local Development
|
v
Azure SQL Database
For .NET developers, the important question is not simply whether a database can run inside a container. SQL Server containers have been available for development for years.
The interesting question is:
How closely can a local Azure SQL development environment match the database behavior that the application will eventually use in Azure?
The Local Development Problem
A typical cloud-oriented .NET application might use:
ASP.NET Core
|
v
Entity Framework Core
|
v
Azure SQL Database
During local development, developers have several choices.
Use a Shared Cloud Database
Developer
|
v
Azure SQL Database
This can provide a realistic database environment, but it introduces:
Use SQL Server Locally
Another option is:
Developer
|
v
SQL Server
|
v
EF Core
This is convenient, but it may not reproduce every Azure SQL-specific behavior.
Use Azure SQL Developer Locally
The newer approach is:
Developer
|
v
Azure SQL Developer Container
|
v
EF Core
The objective is to bring the Azure SQL Database engine closer to the developer's local inner loop.
What Is Azure SQL Developer?
Azure SQL Developer is a containerized Azure SQL Database engine intended for local development and CI use.
The preview announcement describes it as the Azure SQL Database engine running locally in a container, rather than a generic SQL Server image intended to approximate Azure SQL. The stated goal is to provide the same engine behavior used in Azure SQL Database for local development scenarios.
This distinction is important.
Conceptually:
Traditional Local Development
.NET
|
v
SQL Server Container
|
v
Hope behavior matches Azure SQL
versus:
Azure SQL Developer
.NET
|
v
Azure SQL Database Engine
|
v
Local Container
|
v
Azure SQL Database
The second model is designed to reduce the gap between local and cloud database development.
Why Engine Parity Matters
Consider a .NET application using Entity Framework Core.
Your application may execute:
var products = await db.Products
.Where(x => x.IsActive)
.OrderBy(x => x.Name)
.ToListAsync();
The C# code looks identical regardless of where the database runs.
But the application depends on more than C#.
It also depends on:
EF Core
|
v
Database Provider
|
v
SQL Generation
|
v
Database Engine
|
v
Database Behavior
If the local database behaves differently from the target database, developers may discover problems only after deployment.
A local environment that uses the same database engine can reduce that gap.
A Typical .NET Architecture
A local application might look like:
+----------------------+
| ASP.NET Core API |
+----------+-----------+
|
| EF Core
v
+----------------------+
| Azure SQL Developer |
| Container |
+----------------------+
The application can use a normal SQL connection string.
For example:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Database=OrdersDb;User Id=sa;Password=YourPassword;TrustServerCertificate=True;"
}
}
The exact connection string depends on how the container is configured.
The important architectural principle is that the application should not need separate database access code simply because development is local.
Containerized Database Development
Containers are particularly useful for database development because the environment can be created and removed without requiring a permanent database installation on the developer's machine.
The basic workflow is:
Pull Image
|
v
Start Container
|
v
Create Database
|
v
Run Application
|
v
Run Tests
|
v
Stop Container
A simplified Docker workflow could look like:
docker pull <azure-sql-developer-image>
and:
docker run \
--name azure-sql-dev \
-e ACCEPT_EULA=Y \
-e MSSQL_SA_PASSWORD="StrongPasswordHere" \
-p 1433:1433 \
-d <azure-sql-developer-image>
The exact image name, environment variables, licensing requirements, and supported configuration should always be verified against the version being used because the Azure SQL Developer container is a preview offering.
Why Offline Development Is Useful
A major benefit of a local container is that developers can continue working without a live cloud database connection.
Consider:
Internet Available
|
v
Cloud Database
versus:
Internet Unavailable
|
v
Local Database Container
|
v
Continue Development
This can be particularly useful for:
Once the required container image and dependencies are available locally, database development can continue without sending queries to a remote development database.
The Inner Loop
The inner loop is the repeated development cycle:
Write Code
|
v
Build
|
v
Run
|
v
Test
|
v
Change
|
+---------> Repeat
Database development is part of this loop.
A cloud database can make the loop dependent on:
Network
Cloud Availability
Shared Resources
Cloud Configuration
A local container moves the database closer to the developer:
.NET Application
|
v
Local Database
|
v
Immediate Feedback
That can make local iteration simpler.
The Outer Loop
The outer loop covers collaboration and deployment:
Developer
|
v
Git Repository
|
v
CI
|
v
Staging
|
v
Production
The local database should fit into this process rather than becoming a completely different environment.
A good architecture is:
INNER LOOP
.NET
|
v
Azure SQL Developer
|
v
Tests
OUTER LOOP
Git
|
v
CI
|
v
Azure SQL Database
The database engine remains consistent while the infrastructure around it changes.
EF Core Integration
For a .NET application using EF Core, the database provider remains responsible for communication with SQL Server-compatible databases.
A typical registration looks like:
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(
builder.Configuration
.GetConnectionString("DefaultConnection"));
});
The application code does not need to know whether the database is:
Local Container
or:
Azure SQL Database
The environment-specific connection string can change through configuration.
This is one of the biggest advantages of containerized database development.
Environment-Based Configuration
Instead of hard-coding connection information, use environment-specific configuration.
For development:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Database=OrdersDb;..."
}
}
For a cloud environment:
ConnectionStrings__DefaultConnection
can be supplied through the deployment environment.
The application code remains:
var connectionString =
builder.Configuration.GetConnectionString(
"DefaultConnection");
This keeps infrastructure configuration outside the application logic.
Running EF Core Migrations
A local container is also useful for testing EF Core migrations.
For example:
dotnet ef migrations add InitialCreate
Then:
dotnet ef database update
The migration runs against the local database.
A useful workflow is:
Create Migration
|
v
Apply Locally
|
v
Run Tests
|
v
Review SQL
|
v
Apply in CI
|
v
Deploy
This gives developers a chance to catch schema problems before deployment.
Testing Database Migrations
Suppose a migration adds:
migrationBuilder.AddColumn<decimal>(
name: "Price",
table: "Products",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
The migration should be tested against the same database engine used by the target environment.
Test:
Migration Up
|
v
Schema Validation
|
v
Application Tests
|
v
Migration Down
where rollback is supported and appropriate for the application's migration strategy.
Integration Testing
A local database container is particularly useful for integration tests.
Instead of:
Integration Test
|
v
Shared Cloud Database
you can use:
Integration Test
|
v
Temporary Database Container
This improves test isolation.
A test suite might:
Start Database
|
v
Apply Migrations
|
v
Seed Test Data
|
v
Run Tests
|
v
Clean Up
This approach is especially useful in CI.
Example Integration Test
Consider:
[Fact]
public async Task Can_Create_Order()
{
var order = new Order
{
CustomerId = 1001,
Total = 250m
};
db.Orders.Add(order);
await db.SaveChangesAsync();
var result = await db.Orders
.SingleAsync(x => x.CustomerId == 1001);
Assert.Equal(250m, result.Total);
}
The test becomes more valuable when the database behind it behaves like the production database.
It is still an integration test, not a replacement for unit testing.
Local Container vs Shared Database
| Area | Local Container | Shared Cloud Database |
|---|
| Internet dependency | Low after setup | High |
| Developer isolation | High | Lower |
| Setup | Container required | Cloud resource required |
| Shared data | No | Usually yes |
| Local iteration | Strong | Depends on network |
| Cloud configuration | Can be reproduced locally | Direct |
| CI suitability | Strong | Strong |
| Production parity | Designed for Azure SQL parity | Direct |
| Cloud cost during local work | Avoided | Possible |
| Cleanup | Container removal | Resource/data cleanup |
Neither option is universally better.
The local container is particularly attractive for development and CI, while shared cloud environments remain useful for testing real cloud infrastructure and deployment behavior.
Local Container vs SQL Server Express
SQL Server Express has historically been a popular local development option.
The difference is important:
| Area | SQL Server Express | Azure SQL Developer |
|---|
| Local | Yes | Yes |
| Container option | Yes | Yes |
| Azure SQL engine | No | Designed for Azure SQL |
| Offline development | Yes | Yes |
| Cloud database parity | Not exact | Primary goal |
| Free local development | Yes | Preview offering |
| CI usage | Yes | Intended use |
| Best use | General SQL Server development | Azure SQL-focused development |
The choice should depend on the target database.
If the production system is Azure SQL Database, testing against an Azure SQL-oriented local environment can reduce some compatibility assumptions.
Dev Containers
Azure SQL development can also be combined with development containers.
A development container can package:
.NET SDK
Azure SQL Tools
SQLCMD
Database Project Tools
Docker Tools
Application Dependencies
A simplified structure might look like:
.devcontainer/
|
+-- devcontainer.json
+-- Dockerfile
The development environment can then be recreated by other team members.
This helps address the classic problem:
Developer A
"Works on my machine."
Developer B
"Missing three tools."
Developer C
"Different database version."
Instead:
Repository
|
v
Development Container
|
v
Consistent Tooling
Current Azure SQL development guidance also provides Dev Container templates intended to simplify local setup and support consistent development environments.
Example devcontainer.json
A simplified configuration might look like:
{
"name": ".NET Azure SQL Development",
"image": "mcr.microsoft.com/devcontainers/dotnet:latest",
"features": {},
"forwardPorts": [
1433
],
"postCreateCommand": "dotnet restore"
}
This is only an illustrative starting point.
A real project may need:
The goal is reproducibility rather than simply putting everything into one container.
Running Database and Application Together
For a .NET application, a local development environment can contain:
Docker
│
├── Azure SQL Developer
│
├── Redis
│
└── Other Dependencies
|
v
.NET Application
This is especially useful for distributed applications.
A developer can bring up the entire environment instead of manually installing every dependency.
CI/CD Usage
The same container concept can be useful in CI.
A pipeline could look like:
Pull Request
|
v
Start Database Container
|
v
Apply Migrations
|
v
Build .NET Application
|
v
Run Integration Tests
|
v
Destroy Container
This gives every CI run a clean database environment.
It also reduces dependence on a shared development database.
Testing Offline Behavior
If offline development is one of the goals, test it deliberately.
For example:
1. Pull required images
2. Start database
3. Disconnect network
4. Start application
5. Run migrations
6. Execute tests
7. Run application features
This verifies that the development workflow really works offline.
It also identifies hidden dependencies such as:
Package Downloads
Cloud Authentication
Remote APIs
External Configuration
A local database alone does not automatically make the entire application offline-capable.
Security Considerations
A local database container still contains data.
Do not treat it as automatically safe.
Avoid putting real production data into local development environments unless there is a clear, approved process for doing so.
Prefer:
Production Data
|
v
Sanitization
|
v
Anonymized Dataset
|
v
Local Development
rather than:
Production Database
|
v
Developer Laptop
Also use development-only credentials.
For example:
Development Password
≠
Production Password
Secrets should not be committed to source control.
Data Persistence
Containers can be temporary.
If the container is removed:
Container
|
v
Deleted
|
v
Data May Be Lost
For persistent local development, use a Docker volume or another appropriate persistence mechanism.
Conceptually:
Azure SQL Container
|
v
Persistent Volume
|
v
Database Files
However, disposable databases are often preferable for automated integration tests.
That gives you:
Clean Test
|
v
Fresh Database
instead of inheriting state from previous runs.
Common Mistakes
Mistake 1: Assuming Local and Cloud Are Automatically Identical
The purpose of the container is to reduce differences, but the complete cloud environment includes more than the database engine.
Mistake 2: Using Production Data Locally
Use safe, sanitized development data.
Mistake 3: Hard-Coding Connection Strings
Keep connection configuration external to application code.
Mistake 4: Sharing One Local Database Between Unrelated Projects
Separate databases or containers can improve isolation.
Mistake 5: Forgetting Database Persistence
A disposable container can lose data when removed.
Mistake 6: Using Persistent Data in Every Test
Integration tests often benefit from a clean database per test suite or test run.
Mistake 7: Assuming Containerization Solves Every Environment Difference
Cloud networking, identity, firewall rules, backups, scaling, and other platform services still need separate testing.
Troubleshooting
| Problem | What to Check |
|---|
| Container will not start | Check Docker/Podman availability and container configuration |
| Port 1433 unavailable | Check for another SQL Server process |
| EF Core cannot connect | Verify server, credentials, port, and database name |
| Migration fails | Inspect generated SQL and database compatibility |
| Data disappears | Configure a persistent volume if persistence is required |
| Tests interfere with each other | Use isolated databases or reset state |
| Application works locally but fails in Azure | Test cloud-specific identity, networking, and configuration |
| Container is slow | Check CPU, memory, disk, and host resources |
| Offline development fails | Identify hidden external dependencies |
Best Practices
Pin Your Versions
Avoid allowing local environments to silently change versions.
For example:
.NET SDK
EF Core
Database Provider
Container Image
Database Project Tools
should be intentionally selected.
Keep Connection Strings External
Use configuration and environment variables.
Use Disposable Databases for Tests
Clean environments produce more reliable integration tests.
Use Persistent Volumes Only When Needed
Local development may need persistence, while CI generally benefits from disposable environments.
Use Sanitized Test Data
Never treat a developer laptop as a production security boundary.
Test Migrations Locally
Run migrations against the same database engine used by the target environment.
Include the Database in CI
Do not rely exclusively on developer machines for integration testing.
Keep Cloud Testing
Local engine parity does not replace testing the actual Azure environment.
Advantages
Better Local Development
Developers can work with a local database without depending on a shared cloud instance.
Reduced Cloud Dependency
Local database development can continue without an active cloud database connection.
Improved Database Parity
The Azure SQL Developer approach is specifically designed around the Azure SQL Database engine.
Useful for CI
A containerized database can provide an isolated database environment for automated testing.
Easier Onboarding
Development containers can package database tooling and application dependencies into a repeatable environment.
Works Well With .NET
EF Core can continue using the standard SQL Server provider while the connection target changes through configuration.
Disadvantages and Limitations
Preview Status
Azure SQL Developer is a preview offering, so teams should validate its capabilities and operational characteristics before depending on it for critical workflows.
Resource Requirements
Running a database container requires sufficient CPU, memory, and disk resources on the developer or CI machine.
Not a Complete Azure Environment
A local database does not reproduce cloud networking, identity, scaling, monitoring, backups, or other platform services.
Container Management Adds Complexity
Teams need to understand images, ports, volumes, credentials, and lifecycle management.
Data Management Still Matters
Developers must decide when data should persist and when databases should be disposable.
A Practical .NET Development Workflow
A clean local workflow can look like:
Clone Repository
|
v
Start Development Environment
|
v
Start Azure SQL Developer
|
v
Apply EF Core Migrations
|
v
Start ASP.NET Core
|
v
Run Tests
|
v
Make Code Changes
|
v
Repeat
The outer workflow then becomes:
Local Development
|
v
Commit
|
v
Pull Request
|
v
CI
|
v
Integration Tests
|
v
Staging
|
v
Azure SQL Database
This gives the team a clear separation between local development and cloud deployment without forcing developers to use a completely different database engine locally.
Conclusion
Azure SQL Developer is an interesting approach to a common .NET development problem: building locally against a database that behaves like the cloud database the application will eventually use. Running the Azure SQL Database engine in a local container can make the development inner loop more predictable, reduce dependence on shared cloud databases, and provide a useful environment for EF Core migrations and integration tests. It does not eliminate the need to test the actual Azure environment, because networking, identity, scaling, monitoring, and other cloud services still matter. For teams building .NET applications specifically for Azure SQL Database, a local container combined with Dev Containers and CI can provide a practical development workflow where developers can build, test, and experiment locally before moving changes into the cloud.