Azure SQL Database
Management-plane operations work without Docker by default. Set the data-plane provider to
managed to use mssql-jdbc, pyodbc, System.Data.SqlClient, or another TDS-speaking client
against a real SQL Server 2025 container.
Features
- Servers: create, get, list, delete; immediate ARM state by default;
minimalTlsVersion,publicNetworkAccessandrestrictOutboundNetworkAccessare stored and read back - Databases: create, update (
PUTandPATCH), get, list, delete; guarded against droppingmaster; the SKU,maxSizeBytes,requestedBackupStorageRedundancy,zoneRedundantand tags are stored and read back - Firewall rules: full CRUD; metadata-only (no actual IP filtering in dev mode)
- Settings: the connection policy and Express vulnerability assessment of a server, and the transparent data encryption, security alert policy, short and long term backup retention and geo backup policies of a database. Each reads Azure's defaults until it is written, is stored on its server or database, and goes with it on delete. None of them changes how the data plane behaves
- Restorable dropped databases: always an empty list, since a deleted database is not kept
- Terraform:
azurerm_mssql_serverandazurerm_mssql_databaseapply, refresh with no changes, and destroy - Name availability check:
POST .../checkNameAvailability - Data-plane providers:
none(default),managed, and reservedexternal - Asynchronous managed provisioning: server
PUTreturns202with AzureLocationpolling - Connection strings: managed mode returns JDBC, ADO.NET, pyodbc, and EF Core strings
- EULA guard: managed server creation requires
FLOCI_AZ_SERVICES_SQL_ACCEPT_EULA=Y
A server name is a global DNS name: the first subscription to create it owns it, and a create of the same name
from another subscription or resource group is 400 NameAlreadyExists until the owner deletes it. A server created
through the /{account}-sql/ paths holds its name too. In Azure each cloud has its own zone, but floci-az names a
server's container after the server alone, so a name is unique across every configured cloud, not per cloud.
checkNameAvailability answers from the same ownership. Paths that differ only in case address the same server.
With parity-overrides.strict-names a server, database or firewall rule name outside Azure's naming rule is refused
with 400 InvalidResourceName (see ARM name syntax).
Data-plane providers
| Provider | Behavior |
|---|---|
none |
Default. Azure-compatible ARM state only; no Docker access, image pull, EULA, or TDS endpoint. |
managed |
Starts one real SQL Server container per logical server. Requires Docker and EULA acceptance. |
external |
Reserved for a later release. Requests currently return DataPlaneProviderUnavailable. |
When none is selected, /connect returns 409 DataPlaneNotEnabled. ARM responses still expose
the Azure-shaped {server}.database.windows.net hostname and never include emulator-only port data.
The deprecated mocked setting remains a compatibility alias when data-plane.provider is absent:
true maps to none; false maps to managed.
For upgrade compatibility, accept-eula: "Y" also selects managed when neither
data-plane.provider nor mocked is configured. An explicit provider or legacy mocked value
always takes precedence.
Managed server creation does not wait for image pull or engine startup on the request thread. A new
server is persisted with state=Creating and returns 202 Accepted, Location, and Retry-After.
Poll Location until it returns 200 with status=Succeeded; failed provisioning returns a non-2xx
ARM error envelope containing the provisioning error code and message. Server GET exposes the
corresponding Creating, Ready, or Failed resource state. Equivalent PUT retries reuse the same
operation; conflicting updates during provisioning return 409 ConflictingServerOperation.
EULA Requirement (managed provider only)
SQL Server is covered by the Microsoft SQL Server EULA. You must explicitly accept it before the emulator will start any container:
# Environment variable (docker-compose / CLI)
FLOCI_AZ_SERVICES_SQL_ACCEPT_EULA=Y
# JVM system property (quarkus:dev)
-Dfloci-az.services.sql.accept-eula=Y
Without it, managed PUT /servers/{name} returns:
Endpoints
ARM path (used by Azure SDKs)
/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Sql/servers/{serverName}
/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Sql/servers/{serverName}/databases/{dbName}
Convenience path (quick testing)
/{account}-sql/servers/{serverName}
/{account}-sql/servers/{serverName}/databases/{dbName}
/{account}-sql/servers/{serverName}/connect
/{account}-sql/servers/{serverName}/databases/{dbName}/connect
The /connect endpoints are a floci-az addition: they return all connection string formats in one call.
Quickstart
This data-plane quickstart assumes data-plane.provider: managed, Docker access, and accepted EULA.
For ARM-only use, keep the default none provider and stop after creating management resources.
1: Create a server
curl -s -X PUT \
"http://localhost:4577/subscriptions/my-sub/resourceGroups/my-rg/providers/Microsoft.Sql/servers/myserver?api-version=2021-11-01" \
-H "Content-Type: application/json" \
-d '{
"location": "eastus",
"properties": {
"administratorLogin": "sa",
"administratorLoginPassword": "YourStrong!Passw0rd"
}
}'
In managed mode, first call returns promptly with
202 Accepted; image pull and SQL Server startup continue in the background. Follow theLocationheader before requesting connection strings.
2: Get connection strings
Response:
{
"server": "myserver",
"host": "localhost",
"port": 59743,
"jdbcUrl": "jdbc:sqlserver://localhost:59743;databaseName=master;user=sa;password=YourStrong!Passw0rd;encrypt=true;trustServerCertificate=true;",
"connectionString": "Server=tcp:localhost,59743;Initial Catalog=master;...",
"pyodbc": "DRIVER={ODBC Driver 18 for SQL Server};SERVER=localhost,59743;...",
"entityFramework": "Server=localhost,59743;Database=master;..."
}
3: Create a database
curl -s -X PUT \
"http://localhost:4577/subscriptions/my-sub/resourceGroups/my-rg/providers/Microsoft.Sql/servers/myserver/databases/mydb?api-version=2021-11-01" \
-H "Content-Type: application/json" \
-d '{"location": "eastus", "properties": {}}'
4: Connect via JDBC
String jdbcUrl = "jdbc:sqlserver://localhost:59743;"
+ "databaseName=mydb;user=sa;password=YourStrong!Passw0rd;"
+ "encrypt=true;trustServerCertificate=true;";
try (Connection conn = DriverManager.getConnection(jdbcUrl);
Statement stmt = conn.createStatement()) {
stmt.executeUpdate("CREATE TABLE greet (msg NVARCHAR(100))");
stmt.executeUpdate("INSERT INTO greet VALUES ('Hello from floci-az!')");
ResultSet rs = stmt.executeQuery("SELECT msg FROM greet");
while (rs.next()) System.out.println(rs.getString(1));
}
Important: use
encrypt=true;trustServerCertificate=trueto accept the container's self-signed certificate while retaining encrypted connections withmssql-jdbc12.x.
SDK Connection
// pom.xml
// <dependency>
// <groupId>com.microsoft.sqlserver</groupId>
// <artifactId>mssql-jdbc</artifactId>
// <version>12.6.1.jre11</version>
// </dependency>
String jdbcUrl = "jdbc:sqlserver://localhost:59743;"
+ "databaseName=mydb;"
+ "user=sa;"
+ "password=YourStrong!Passw0rd;"
+ "encrypt=true;"
+ "trustServerCertificate=true;";
try (Connection conn = DriverManager.getConnection(jdbcUrl)) {
// use conn
}
import pyodbc
conn_str = (
"DRIVER={ODBC Driver 18 for SQL Server};"
"SERVER=localhost,59743;"
"DATABASE=mydb;"
"UID=sa;"
"PWD=YourStrong!Passw0rd;"
"TrustServerCertificate=yes;"
"Encrypt=yes;"
)
conn = pyodbc.connect(conn_str)
cursor = conn.cursor()
cursor.execute("SELECT @@VERSION")
print(cursor.fetchone()[0])
from sqlalchemy import create_engine, text
engine = create_engine(
"mssql+pyodbc://sa:YourStrong!Passw0rd@localhost:59743/mydb"
"?driver=ODBC+Driver+18+for+SQL+Server"
"&TrustServerCertificate=yes"
"&Encrypt=yes",
echo=False,
)
with engine.connect() as conn:
result = conn.execute(text("SELECT @@VERSION"))
print(result.scalar())
Getting the Port
The container port is dynamically assigned by the OS. Retrieve it from:
The /connect response:
PORT=$(curl -s "http://localhost:4577/devstoreaccount1-sql/servers/myserver/connect" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['port'])")
ARM resources intentionally omit the emulator-specific port. Use /connect for reachable endpoint
details in data-plane modes.
REST API Reference
Servers
| Method | Path | Description |
|---|---|---|
PUT |
.../servers/{name} |
Create or update a server |
GET |
.../servers/{name} |
Get server properties |
DELETE |
.../servers/{name} |
Delete server and stop its container |
GET |
.../servers |
List all servers in the resource group |
POST |
.../checkNameAvailability |
Check if a server name is available |
Databases
| Method | Path | Description |
|---|---|---|
PUT |
.../servers/{name}/databases/{db} |
Create or update a database |
GET |
.../servers/{name}/databases/{db} |
Get database properties |
DELETE |
.../servers/{name}/databases/{db} |
Drop database (blocked for master) |
GET |
.../servers/{name}/databases |
List all databases |
Firewall Rules
| Method | Path | Description |
|---|---|---|
PUT |
.../servers/{name}/firewallRules/{rule} |
Create or update a firewall rule |
GET |
.../servers/{name}/firewallRules/{rule} |
Get a firewall rule |
DELETE |
.../servers/{name}/firewallRules/{rule} |
Delete a firewall rule |
GET |
.../servers/{name}/firewallRules |
List all firewall rules |
PATCH .../servers/{name}/databases/{db} changes only the fields its body names.
Settings
Each setting has one name. GET before any write answers the default shown; PUT replaces the writable
properties and answers 200; GET on the collection lists the one setting.
| Path | Default | PATCH |
|---|---|---|
.../servers/{name}/connectionPolicies/default |
connectionType: Default |
no |
.../servers/{name}/sqlVulnerabilityAssessments/default |
state: Disabled |
no |
.../databases/{db}/transparentDataEncryption/current |
state: Enabled |
no |
.../databases/{db}/securityAlertPolicies/default |
state: Disabled, no alert recipients |
no |
.../databases/{db}/backupShortTermRetentionPolicies/default |
7 days, differential every 12 hours | yes |
.../databases/{db}/backupLongTermRetentionPolicies/default |
PT0S weekly, monthly and yearly, week 0 |
no |
.../databases/{db}/geoBackupPolicies/Default |
state: Enabled |
no |
A security alert policy's storageAccountAccessKey is accepted and never read back, as in Azure.
Restorable Dropped Databases
| Method | Path | Description |
|---|---|---|
GET |
.../servers/{name}/restorableDroppedDatabases |
Always {"value":[]} |
GET |
.../servers/{name}/restorableDroppedDatabases/{id} |
Always 404 ResourceNotFound |
Convenience (floci-az only)
| Method | Path | Description |
|---|---|---|
GET |
/{account}-sql/servers/{name}/connect |
All connection strings for the server |
GET |
/{account}-sql/servers/{name}/databases/{db}/connect |
All connection strings for a database |
Configuration
floci-az:
services:
sql:
enabled: true
data-plane:
provider: managed # none (default) | managed | external (reserved)
accept-eula: "Y" # Required to start containers
image: "mcr.microsoft.com/mssql/server:2025-latest"
startup-timeout-seconds: 60
Microsoft supports SQL Server Linux container images only on Intel and AMD x86-64 hosts. ARM64 hosts require an explicitly configured alternative image or unsupported CPU emulation.
| Environment Variable | Default | Description |
|---|---|---|
FLOCI_AZ_SERVICES_SQL_ENABLED |
true |
Enable or disable the SQL service |
FLOCI_AZ_SERVICES_SQL_DATA_PLANE_PROVIDER |
none |
Data-plane provider: none, managed, or reserved external |
FLOCI_AZ_SERVICES_SQL_MOCKED |
(unset) | Deprecated alias used only when provider is unset: true = none, false = managed |
FLOCI_AZ_SERVICES_SQL_ACCEPT_EULA |
N |
Set to Y to accept the Microsoft SQL Server EULA in managed mode |
FLOCI_AZ_SERVICES_SQL_IMAGE |
mcr.microsoft.com/mssql/server:2025-latest |
Docker image to use for SQL Server containers |
FLOCI_AZ_SERVICES_SQL_STARTUP_TIMEOUT_SECONDS |
60 |
Seconds to wait for the SQL Server engine to become ready |
Docker Compose
services:
floci-az:
image: floci/floci-az:latest
ports:
- "4577:4577"
- "4578:4578" # HTTPS (Cosmos Java SDK)
volumes:
- /var/run/docker.sock:/var/run/docker.sock # required for SQL + Functions
environment:
FLOCI_AZ_SERVICES_SQL_DATA_PLANE_PROVIDER: managed
FLOCI_AZ_SERVICES_SQL_ACCEPT_EULA: "Y"
# SQL Server containers bind a random port directly to the host.
# Do NOT add those ports here: floci-az manages them via Docker socket.
Sidecar ports: SQL Server containers bind a host port directly via the Docker daemon; these ports are not published on the
floci-azservice. WithFLOCI_AZ_SERVICES_SQL_DEFAULT_PORT=0(the default) the OS assigns one per server. Set it to a specific port and the first server to start binds exactly that port; any further server, or a start when the port is already taken, falls back to an OS-assigned port with a warning in the log. Availability is judged when the server starts, so a port that something else grabs in the same instant fails the create like any other bind conflict. Read the real port from the server's connection details rather than assuming.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ Your App │
│ │
│ ARM REST calls ──────► floci-az :4577 ──► SqlHandler │
│ (create server, (state, routing) │
│ create database, │
│ get conn strings) │
│ │
│ TDS / JDBC ──────────────────────────────────────────────► │
│ (SQL queries, DDL) SQL Server container :59743 │
└──────────────────────────────────────────────────────────────┘
The management plane (ARM API) always goes through floci-az on port 4577. With provider=managed,
the data plane connects directly to the SQL Server container on its dynamic port. With
provider=none, no data-plane container or port exists.