SQL Server  

Azure SQL Developer Container: Building Offline .NET Development Environments

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:

  • Network dependency

  • Shared development state

  • Potential cloud costs

  • Data management concerns

  • Slower feedback for some operations

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:

  • Travel

  • Workshops

  • Classrooms

  • Demonstrations

  • Local integration testing

  • Restricted development environments

  • CI environments

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

AreaLocal ContainerShared Cloud Database
Internet dependencyLow after setupHigh
Developer isolationHighLower
SetupContainer requiredCloud resource required
Shared dataNoUsually yes
Local iterationStrongDepends on network
Cloud configurationCan be reproduced locallyDirect
CI suitabilityStrongStrong
Production parityDesigned for Azure SQL parityDirect
Cloud cost during local workAvoidedPossible
CleanupContainer removalResource/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:

AreaSQL Server ExpressAzure SQL Developer
LocalYesYes
Container optionYesYes
Azure SQL engineNoDesigned for Azure SQL
Offline developmentYesYes
Cloud database parityNot exactPrimary goal
Free local developmentYesPreview offering
CI usageYesIntended use
Best useGeneral SQL Server developmentAzure 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:

  • Specific .NET SDK version

  • SQL tooling

  • Database project tooling

  • Environment variables

  • Additional services

  • Application-specific dependencies

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

ProblemWhat to Check
Container will not startCheck Docker/Podman availability and container configuration
Port 1433 unavailableCheck for another SQL Server process
EF Core cannot connectVerify server, credentials, port, and database name
Migration failsInspect generated SQL and database compatibility
Data disappearsConfigure a persistent volume if persistence is required
Tests interfere with each otherUse isolated databases or reset state
Application works locally but fails in AzureTest cloud-specific identity, networking, and configuration
Container is slowCheck CPU, memory, disk, and host resources
Offline development failsIdentify 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.