DEV Community

Cover image for Docker Compose profiles for ASP.NET Core: one file, four databases
GANESH KUMAR
GANESH KUMAR

Posted on

Docker Compose profiles for ASP.NET Core: one file, four databases

My ASP.NET Core project, eCommerceMVCCore, supports four databases. I wanted anyone, buyer or contributor, to try any of them with one command and no local database install.

The answer was Docker Compose profiles. One docker-compose.yml, one profile per database:

docker compose --profile sqlite up --build
docker compose --profile postgres up --build
docker compose --profile mysql up --build
docker compose --profile sqlserver up --build
Enter fullscreen mode Exit fullscreen mode

Here's how it's put together. The YAML below is simplified.

Profiles in one sentence

A service with profiles: [postgres] only starts when you pass --profile postgres. Services without a profile always start. That lets one file describe four different setups without them all starting at once.

Four Compose profiles, each starting its database with a healthcheck and an app service that waits for it

The database services

services:
  postgres:
    image: postgres:17
    profiles: [postgres]
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD:-Dev_Passw0rd!}
      POSTGRES_DB: shop
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d shop"]
      interval: 5s
      retries: 20

  mysql:
    image: mysql:8.4
    profiles: [mysql]
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-Dev_Passw0rd!}
      MYSQL_DATABASE: shop
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      retries: 20

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    profiles: [sqlserver]
    environment:
      ACCEPT_EULA: "Y"
      MSSQL_SA_PASSWORD: ${DB_PASSWORD:-Dev_Passw0rd!}
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -Q 'SELECT 1'"]
      interval: 10s
      retries: 20
Enter fullscreen mode Exit fullscreen mode

Two details are worth calling out. SQL Server's newer images ship sqlcmd under mssql-tools18, and it needs -C to trust the self-signed certificate. And $$ escapes the dollar sign, so the variable is read inside the container, not by Compose.

One app service per profile

The app needs to wait for a different database in each profile, so I define one app service per profile. A YAML anchor keeps the shared parts in one place:

x-app: &app
  build: .
  ports: ["8080:8080"]

services:
  app-postgres:
    <<: *app
    profiles: [postgres]
    environment:
      DatabaseProvider: PostgreSQL
      ConnectionStrings__PostgreSQL: Host=postgres;Database=shop;Username=postgres;Password=${DB_PASSWORD:-Dev_Passw0rd!}
    depends_on:
      postgres:
        condition: service_healthy

  app-sqlite:
    <<: *app
    profiles: [sqlite]
    environment:
      DatabaseProvider: Sqlite
      ConnectionStrings__Sqlite: Data Source=/data/shop.db
    volumes: ["sqlite-data:/data"]

volumes:
  sqlite-data:
Enter fullscreen mode Exit fullscreen mode

ConnectionStrings__PostgreSQL uses the double underscore that ASP.NET Core maps to ConnectionStrings:PostgreSQL. No config file changes are needed per environment.

Try the profiles: download the ASP.NET Core store with a Docker Compose profile for every database and run docker compose --profile sqlite up --build.

Why the healthcheck matters

Without condition: service_healthy, depends_on only waits for the database container to start, not for the database to accept connections. SQL Server in particular takes several seconds to get ready. The app would start, try to run migrations, fail and exit. With healthchecks, the app starts only when the database answers.

I still keep EF Core's retry logic on for the providers that support it, such as EnableRetryOnFailure() for SQL Server. That covers short network blips in production too, not just the startup race.

SQLite needs a volume

SQLite is just a file. If it lives inside the container, it disappears on every rebuild. A named volume keeps the demo data between runs. It's the profile I recommend for a first try: no database container, fastest start.

Migrations and seeding on startup

On first start, the app applies the right migrations for the chosen provider and seeds demo data. So docker compose --profile sqlite up --build ends with a working store at http://localhost:8080, products and all. How the seeding works is a separate article.

Development defaults, not production settings

The default passwords above are for local use only. For anything public, pass real secrets through an .env file that isn't committed, or through your host's secret store, and don't expose database ports.

FAQ

What are Docker Compose profiles?

Optional groups of services. A service with a profiles list only starts when one of its profiles is active, through --profile or the COMPOSE_PROFILES variable. Services without profiles always start.

How do I make an ASP.NET Core container wait for its database?

Give the database service a healthcheck, and use depends_on with condition: service_healthy on the app. Keep EF Core's retry-on-failure enabled as well, for short outages after startup.

How do I pass a connection string to ASP.NET Core in Docker?

Use an environment variable with a double underscore, such as ConnectionStrings__PostgreSQL. ASP.NET Core maps it to ConnectionStrings:PostgreSQL, which is how eCommerceMVCCore configures each database profile.

Get the source code

This Compose setup ships with eCommerceMVCCore, with a profile for every supported database. Get the ASP.NET Core 10 e-commerce source code with Docker support.

Have you used Compose profiles, or do you keep a separate compose file per setup? Let me know in the comments.

Top comments (0)