In this guide, you will run SQL Server on Docker and leave it ready for exercises, testing, and queries.

At the end you will have:

  • SQL Server listening on localhost:1433.
  • A pw0 database.
  • A sensor table.
  • Sample records loaded from a file.

If you want to jump straight to the download, go to Download.

Folder structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sql-server/
├─ docker-compose.yml
├─ Dockerfile
├─ mssql.env
├─ sample-data/
│  └─ room-climate.csv
├─ scripts/
│  ├─ database.sql
│  └─ table.sql
└─ setup/
   ├─ entrypoint.sh
   └─ configure-db.sh

What each file is for:

  • Dockerfile: builds the image that Docker Compose uses to run the service.
  • docker-compose.yml: defines service, ports, env vars, and healthcheck.
  • mssql.env: required environment variables for SQL Server startup.
  • setup/entrypoint.sh: main container startup script.
  • setup/configure-db.sh: waits for SQL Server and then runs SQL scripts.
  • scripts/database.sql: creates the pw0 database.
  • scripts/table.sql: creates the sensor table and loads data.
  • sample-data/room-climate.csv: sample dataset.

Full startup flow

  1. Docker Compose uses docker-compose.yml to start the service.
  2. During startup, Compose builds the image from Dockerfile.
  3. When the container starts, entrypoint.sh runs.
  4. entrypoint.sh launches configure-db.sh in background, then starts SQL Server.
  5. configure-db.sh waits until the engine is ready.
  6. Once ready, it runs database.sql and then table.sql.
  7. Final result: database and table created, data loaded.

File 1: docker-compose.yml (service and startup)

docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
services:
  sql-server:
    container_name: pw0-sql-server
    build:
      context: .
    ports:
      - "1433:1433"
    env_file:
      - ./mssql.env
    restart: always
    healthcheck:
      test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$$MSSQL_SA_PASSWORD" -Q "SELECT 1" || exit 1
      interval: 10s
      timeout: 10s
      retries: 6
      start_period: 10s

Minimum important parts:

  1. container_name: container branding (pw0-sql-server).
  2. build.context: .: uses the local Dockerfile.
  3. ports: exposes SQL Server on localhost:1433.
  4. env_file: loads env vars from mssql.env.
  5. healthcheck: verifies SQL Server is responding.

File 2: Dockerfile (image build)

1
2
3
4
5
6
7
FROM mcr.microsoft.com/mssql/server:2017-CU31-GDR2-ubuntu-18.04
RUN mkdir -p /usr/config
WORKDIR /usr/config
COPY . /usr/config
RUN chmod +x /usr/config/setup/entrypoint.sh
RUN chmod +x /usr/config/setup/configure-db.sh
ENTRYPOINT ["./setup/entrypoint.sh"]

Quick line-by-line read (execution order):

  1. FROM ...: starts from an image that already contains SQL Server.
  2. RUN mkdir -p /usr/config: creates a folder for scripts and data.
  3. WORKDIR /usr/config: sets the working directory.
  4. COPY . /usr/config: copies the full project content into the container.
  5. RUN chmod +x ...: gives execution permission to .sh scripts.
  6. ENTRYPOINT [...]: defines the startup command.

File 3: mssql.env (environment variables)

1
2
3
ACCEPT_EULA="Y"
MSSQL_SA_PASSWORD="pw0SQLpas$"
MSSQL_PID="Developer"

What each variable does:

  1. ACCEPT_EULA="Y": accepts SQL Server license terms.
  2. MSSQL_SA_PASSWORD="...": sets the sa admin password.
  3. MSSQL_PID="Developer": selects Developer edition.

File 4: entrypoint.sh (startup orchestration)

1
2
3
#!/bin/bash
/usr/config/setup/configure-db.sh &
/opt/mssql/bin/sqlservr

Why this order:

  1. configure-db.sh &: starts the init process in background.
  2. sqlservr: starts SQL Server as the main container process.
  3. & lets both run in parallel.

File 5: configure-db.sh (wait + SQL execution)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

DBSTATUS=1
i=0

while [[ "$DBSTATUS" -ne 0 ]] && [[ "$i" -lt 60 ]] || [[ -z "$DBSTATUS" ]]; do
    i=$((i + 1))
    DBSTATUS=$(/opt/mssql-tools/bin/sqlcmd -h -1 -t 1 -U sa -P "$MSSQL_SA_PASSWORD" -Q "SET NOCOUNT ON; SELECT SUM(STATE) FROM sys.databases")
    ERRCODE=$?
    sleep 1
done

if [[ "$DBSTATUS" -ne 0 ]] || [[ "$ERRCODE" -ne 0 ]]; then
    echo "SQL Server took more than 60 seconds to start up"
    exit 1
fi

echo "SQL Server took $i seconds to start up"

/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $MSSQL_SA_PASSWORD -i /usr/config/scripts/database.sql &&
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $MSSQL_SA_PASSWORD -i /usr/config/scripts/table.sql

This is the key startup step:

  1. Waits up to 60 seconds for SQL Server readiness.
  2. Runs database.sql first (database must exist before table creation).
  3. Uses && so table.sql only runs if database.sql succeeds.
  4. Prevents partial initialization on failures.

File 6: database.sql (database creation)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
USE master
GO

IF NOT EXISTS (
    SELECT name
    FROM sys.databases
    WHERE name = 'pw0'
)
CREATE DATABASE pw0
GO

PRINT 'DB pw0 created or already exists'
GO

This script:

  1. Runs from master.
  2. Checks whether pw0 already exists.
  3. Creates pw0 only when missing.

File 7: table.sql (table + initial load)

 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
USE pw0
GO

IF NOT EXISTS (
    SELECT name
    FROM sysobjects
    WHERE name = 'sensor'
      AND xtype = 'U'
)
CREATE TABLE sensor (
    created_at DATETIME,
    temperature FLOAT,
    humidity FLOAT,
    pressure FLOAT,
    lux FLOAT
)
GO

IF NOT EXISTS (
    SELECT TOP 1 *
    FROM sensor
)
BULK INSERT sensor
FROM '/usr/config/sample-data/room-climate.csv'
WITH (
    FIELDTERMINATOR = ',',
    ROWTERMINATOR = '\n',
    FIRSTROW = 2,
    TABLOCK
)
GO

PRINT 'TB created and populated with dummy records'
GO

This script:

  1. Selects pw0.
  2. Creates sensor only if missing.
  3. Loads CSV rows when the table is empty.

Start the stack (step-by-step or zip)

From the folder that contains docker-compose.yml:

1
docker compose up -d

This command applies in both cases:

  1. If you followed the step-by-step setup and created files manually.
  2. If you downloaded the .zip and started from that project folder.

Load your own data

Recommended formats in this setup:

  1. CSV: easiest option with BULK INSERT.
  2. Delimited TXT: also valid with the right delimiter.
  3. Excel (.xlsx): export to CSV first.

What to change

  1. Put your file in sample-data/ (for example my-data.csv).
  2. Update CREATE TABLE columns and types in scripts/table.sql.
  3. Update the BULK INSERT file path:
1
FROM '/usr/config/sample-data/my-data.csv'
  1. If your delimiter is not comma, change FIELDTERMINATOR.
  2. If your file has no header row, use FIRSTROW = 1.

Example for semicolon-separated files:

1
2
3
4
5
6
WITH (
    FIELDTERMINATOR = ';',
    ROWTERMINATOR = '\n',
    FIRSTROW = 2,
    TABLOCK
)

Quick verification

Step 1: open sqlcmd

1
docker compose exec sql-server /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "pw0SQLpas$"

Step 2: check database

1
2
3
4
SELECT name
FROM sys.databases
WHERE name = 'pw0';
GO

Expected output (example):

1
2
3
name
----
pw0

Step 3: check loaded row count

1
2
3
4
USE pw0;
GO
SELECT COUNT(*) AS total_rows FROM sensor;
GO

Expected output (example):

1
2
3
total_rows
----------
1500

Step 4: check sample rows

1
2
3
4
USE pw0;
GO
SELECT TOP 5 * FROM sensor ORDER BY created_at DESC;
GO

Stop and remove the stack

When you finish:

1
docker compose down

This stops and removes the stack containers.


Download

If you do not want to copy files manually, download the full stack (optional):