Уявіть, що ви створили ASP.NET Core Web API з підключенням до PostgreSQL. У файлі appsettings.json ви вказали рядок підключення:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=myapp;Username=postgres;Password=mysecretpassword"
}
}
Ви зібрали Docker-образ з цією конфігурацією. Все працює локально. Але тепер виникають проблеми:
Проблема 1: Різні середовища
localhost:5432staging-db.internal:5432prod-db-cluster.aws.com:5432Для кожного середовища потрібен окремий образ з різними appsettings.json. Це означає три різні збірки, три різні образи, три різні тести. Якщо ви знайшли баг у production-образі, ви не можете бути впевнені, що той самий код працює у development-образі — це різні артефакти.
Проблема 2: Безпека
Пароль mysecretpassword захардкоджений у образі. Будь-хто, хто має доступ до образу, може витягти appsettings.json та побачити пароль:
# Витягти файл з образу
docker create --name temp myapp:latest
docker cp temp:/app/appsettings.json .
docker rm temp
# Прочитати пароль
cat appsettings.json | grep Password
Якщо ви публікуєте образ у Docker Hub (навіть у приватний registry), секрети стають доступними всім, хто має доступ до registry.
Проблема 3: Динамічна конфігурація
Ви хочете змінити рівень логування з Information на Debug для діагностики проблеми у production. З хардкодженою конфігурацією вам потрібно:
appsettings.jsonЦе займає 10-15 хвилин. А якщо проблема критична і кожна хвилина downtime коштує грошей?
Рішення: Винести конфігурацію за межі образу та передавати її через змінні оточення (environment variables) при запуску контейнера. Один образ — багато конфігурацій.
12-Factor App — це методологія розробки cloud-native застосунків, створена інженерами Heroku у 2011 році. Один з ключових принципів — Factor III: Config.
Принцип: "Store config in the environment"
Що таке config?
Конфігурація — це все, що змінюється між deployments (development, staging, production), але не є кодом:
Що НЕ є config:
Традиційний підхід: appsettings.Development.json, appsettings.Production.json
Проблеми:
.gitignore, хтось може випадково закомітити)Підхід 12-Factor: Змінні оточення
Переваги:
appsettings.json) мають містити лише значення за замовчуванням та структуру, але не секрети.-e при запускуСинтаксис:
docker run -e VARIABLE_NAME=value image_name
Приклад:
docker run -d \
--name myapp \
-e ASPNETCORE_ENVIRONMENT=Production \
-e ConnectionStrings__DefaultConnection="Host=prod-db;Database=myapp;Username=postgres;Password=secret" \
myapp:latest
Пояснення:
-e ASPNETCORE_ENVIRONMENT=Production — встановлює змінну ASPNETCORE_ENVIRONMENT зі значенням ProductionConnectionStrings__DefaultConnection — ASP.NET Core використовує __ (подвійне підкреслення) як роздільник для вкладених секцій у appsettings.jsonЯк це працює в ASP.NET Core:
appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=myapp"
}
}
Змінна оточення ConnectionStrings__DefaultConnection перезаписує значення з appsettings.json.
Переваги:
docker runНедоліки:
docker inspect та docker ps (небезпечно для секретів)--env-fileСинтаксис:
docker run --env-file path/to/file.env image_name
Приклад:
Створити файл .env:
# .env
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://0.0.0.0:5000
# Database
ConnectionStrings__DefaultConnection=Host=prod-db;Database=myapp;Username=postgres;Password=secret
# Logging
Logging__LogLevel__Default=Information
Logging__LogLevel__Microsoft.AspNetCore=Warning
# Feature Flags
Features__EnableNewUI=true
Features__EnableBetaFeatures=false
Запустити контейнер:
docker run -d \
--name myapp \
--env-file .env \
-p 5000:5000 \
myapp:latest
Переваги:
.env.example у Git (без секретів)Недоліки:
.env.dev, .env.prod).env файли з секретами у Git! Додайте .env до .gitignore. Для команди створіть .env.example з placeholder-значеннями:# .env.example
ASPNETCORE_ENVIRONMENT=Development
ConnectionStrings__DefaultConnection=Host=localhost;Database=myapp;Username=postgres;Password=YOUR_PASSWORD_HERE
Docker автоматично передає змінні з хоста, якщо вказати ім'я без значення:
export DATABASE_PASSWORD=secret123
docker run -d \
--name myapp \
-e DATABASE_PASSWORD \
myapp:latest
Docker візьме значення DATABASE_PASSWORD з оточення хоста.
Use case: CI/CD pipelines, де секрети зберігаються у secrets manager (GitHub Secrets, GitLab CI Variables).
Приклад у GitHub Actions:
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t myapp:latest .
- name: Run container with secrets
env:
DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
API_KEY: ${{ secrets.API_KEY }}
run: |
docker run -d \
--name myapp \
-e DATABASE_PASSWORD \
-e API_KEY \
-p 5000:5000 \
myapp:latest
Що відбувається:
Приклад у GitLab CI:
deploy:
stage: deploy
script:
- docker build -t myapp:latest .
- docker run -d
--name myapp
-e DATABASE_PASSWORD=$DB_PASSWORD
-e API_KEY=$API_KEY
-p 5000:5000
myapp:latest
variables:
DB_PASSWORD: $CI_DB_PASSWORD # З GitLab CI Variables
API_KEY: $CI_API_KEY
Переваги:
Недоліки:
Синтаксис:
ENV VARIABLE_NAME=value
Приклад:
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
# Змінні за замовчуванням
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://0.0.0.0:5000
ENV Logging__LogLevel__Default=Information
COPY . .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Що відбувається:
ENV встановлюються при побудові образу-e або --env-file при запускуПриклад перезапису:
# Використати значення за замовчуванням (Production)
docker run myapp:latest
# Перезаписати на Development
docker run -e ASPNETCORE_ENVIRONMENT=Development myapp:latest
Коли використовувати ENV у Dockerfile:
ENVENV для non-sensitive значень за замовчуванням. Секрети передавайте через -e або --env-file при запуску.Це перша частина статті 15.environment-and-configuration.md. Я створив:
-e, --env-file, змінні з хостаПродовжити з наступними розділами?
ARG та ENV — це дві інструкції Dockerfile для роботи зі змінними, але вони працюють у різний час та мають різне призначення.
| Аспект | ARG | ENV |
|---|---|---|
| Час життя | Build-time (під час docker build) | Runtime (під час docker run) |
| Доступність | Лише у Dockerfile | У Dockerfile та у контейнері |
| Передача значення | docker build --build-arg NAME=value | docker run -e NAME=value |
| Перезапис | Не можна змінити після build | Можна змінити при кожному run |
| Use case | Параметризація збірки (версії, URLs) | Конфігурація застосунку |
| Безпека | Значення зберігаються у image history | Значення не зберігаються у образі |
ARG використовується для параметризації процесу збірки. Значення доступні лише під час виконання інструкцій Dockerfile.
Приклад:
# Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# Оголосити ARG
ARG BUILD_CONFIGURATION=Release
ARG DOTNET_VERSION=8.0
WORKDIR /src
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
COPY . .
# Використати ARG у команді
RUN dotnet build -c $BUILD_CONFIGURATION -o /app/build
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}
WORKDIR /app
COPY --from=build /app/build .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Побудова з різними значеннями:
# Використати значення за замовчуванням (Release)
docker build -t myapp:latest .
# Перезаписати на Debug
docker build --build-arg BUILD_CONFIGURATION=Debug -t myapp:debug .
# Змінити версію .NET
docker build --build-arg DOTNET_VERSION=8.0-alpine -t myapp:alpine .
Що відбувається:
ARG BUILD_CONFIGURATION=Release — оголошує змінну зі значенням за замовчуванням--build-arg BUILD_CONFIGURATION=Debug — перезаписує значення при побудові$BUILD_CONFIGURATION — використовується у RUN командіdocker build змінна зникає — вона не доступна у контейнеріКоли використовувати ARG:
ENV встановлює змінні, що доступні під час виконання контейнера.
Приклад:
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
# Встановити ENV
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://0.0.0.0:5000
COPY . .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Запуск:
# Використати значення за замовчуванням
docker run myapp:latest
# ASPNETCORE_ENVIRONMENT=Production
# Перезаписати при запуску
docker run -e ASPNETCORE_ENVIRONMENT=Development myapp:latest
# ASPNETCORE_ENVIRONMENT=Development
Що відбувається:
ENV встановлює змінну у образі-e при кожному docker runІноді потрібно передати значення з build-time у runtime. Для цього використовують ARG → ENV:
# Оголосити ARG
ARG APP_VERSION=1.0.0
ARG BUILD_DATE
# Передати значення у ENV
ENV APP_VERSION=${APP_VERSION}
ENV BUILD_DATE=${BUILD_DATE}
# Тепер APP_VERSION доступна у контейнері
LABEL version="${APP_VERSION}"
LABEL build-date="${BUILD_DATE}"
Побудова:
docker build \
--build-arg APP_VERSION=1.2.3 \
--build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
-t myapp:1.2.3 .
Перевірка у контейнері:
docker run myapp:1.2.3 printenv APP_VERSION
# Вивід: 1.2.3
ARG зберігаються у image history та доступні через docker history. Ніколи не передавайте секрети через ARG!# Погано: секрет у ARG
docker build --build-arg API_KEY=secret123 .
# Перевірити history
docker history myapp:latest
# Вивід покаже: ARG API_KEY=secret123
ENV при runtime.ASP.NET Core має вбудовану підтримку змінних оточення через Configuration API. Змінні автоматично читаються та об'єднуються з appsettings.json.
Порядок пріоритету (від найнижчого до найвищого):
appsettings.jsonappsettings.{Environment}.json (наприклад, appsettings.Production.json)Це означає: Змінна оточення завжди перезаписує значення з appsettings.json.
Проблема: JSON має вкладену структуру, а змінні оточення — плоскі рядки.
appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=myapp"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Features": {
"EnableNewUI": false
}
}
Як перезаписати через змінні оточення:
ASP.NET Core використовує подвійне підкреслення __ як роздільник для вкладених секцій:
docker run -d \
-e ConnectionStrings__DefaultConnection="Host=prod-db;Database=myapp;Username=postgres;Password=secret" \
-e Logging__LogLevel__Default="Debug" \
-e Logging__LogLevel__Microsoft.AspNetCore="Information" \
-e Features__EnableNewUI="true" \
myapp:latest
Правило: Section__Subsection__Key → { "Section": { "Subsection": { "Key": "value" } } }
appsettings.json (значення за замовчуванням для Development):
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=myapp_dev;Username=postgres;Password=dev"
}
}
Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
# Не встановлюємо ConnectionString у ENV — це секрет!
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://0.0.0.0:5000
COPY . .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Запуск у Production:
docker run -d \
--name myapp \
-e ConnectionStrings__DefaultConnection="Host=prod-db.aws.com;Port=5432;Database=myapp_prod;Username=app_user;Password=${DB_PASSWORD}" \
-p 5000:5000 \
myapp:latest
Що відбулося:
appsettings.json → Host=localhostConnectionStrings__DefaultConnection → перезаписує на Host=prod-db.aws.comHost=prod-db.aws.com;Port=5432;Database=myapp_prod;...Program.cs (ASP.NET Core 8):
var builder = WebApplication.CreateBuilder(args);
// Конфігурація автоматично читається з appsettings.json + змінних оточення
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
Console.WriteLine($"Using database: {connectionString}");
// Читання інших секцій
var logLevel = builder.Configuration["Logging:LogLevel:Default"];
var enableNewUI = builder.Configuration.GetValue<bool>("Features:EnableNewUI");
// Strongly-typed конфігурація через Options pattern
builder.Services.Configure<FeaturesOptions>(
builder.Configuration.GetSection("Features")
);
var app = builder.Build();
app.Run();
FeaturesOptions.cs:
public class FeaturesOptions
{
public bool EnableNewUI { get; set; }
public bool EnableBetaFeatures { get; set; }
}
Використання у контролері:
[ApiController]
[Route("api/[controller]")]
public class ConfigController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly IOptions<FeaturesOptions> _features;
public ConfigController(
IConfiguration configuration,
IOptions<FeaturesOptions> features)
{
_configuration = configuration;
_features = features;
}
[HttpGet("info")]
public IActionResult GetInfo()
{
return Ok(new
{
Environment = _configuration["ASPNETCORE_ENVIRONMENT"],
Database = _configuration.GetConnectionString("DefaultConnection")?.Split(';')[0], // Лише Host
EnableNewUI = _features.Value.EnableNewUI
});
}
}
Перевірка:
curl http://localhost:5000/api/config/info
# Вивід:
# {
# "environment": "Production",
# "database": "Host=prod-db.aws.com",
# "enableNewUI": true
# }
Options Pattern — це рекомендований спосіб роботи з конфігурацією у .NET. Замість прямого читання через IConfiguration["Key"], ви створюєте POCO класи, що представляють секції конфігурації.
Переваги:
Приклад з валідацією:
using System.ComponentModel.DataAnnotations;
public class DatabaseOptions
{
public const string SectionName = "Database";
[Required]
[MinLength(1)]
public string Host { get; set; } = "localhost";
[Range(1, 65535)]
public int Port { get; set; } = 5432;
[Required]
public string Database { get; set; } = string.Empty;
[Required]
public string Username { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
public string GetConnectionString()
{
return $"Host={Host};Port={Port};Database={Database};Username={Username};Password={Password}";
}
}
Реєстрація з валідацією:
builder.Services.AddOptions<DatabaseOptions>()
.Bind(builder.Configuration.GetSection(DatabaseOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart(); // Валідація при старті застосунку
appsettings.json:
{
"Database": {
"Host": "localhost",
"Port": 5432,
"Database": "myapp",
"Username": "postgres",
"Password": "dev"
}
}
Перезапис через змінні оточення:
docker run -d \
-e Database__Host=prod-db.aws.com \
-e Database__Port=5432 \
-e Database__Database=myapp_prod \
-e Database__Username=app_user \
-e Database__Password=secret123 \
myapp:latest
Використання у сервісі:
public class DatabaseService
{
private readonly DatabaseOptions _options;
public DatabaseService(IOptions<DatabaseOptions> options)
{
_options = options.Value;
}
public async Task ConnectAsync()
{
var connectionString = _options.GetConnectionString();
// Підключитися до бази даних
}
}
IOptions vs IOptionsSnapshot vs IOptionsMonitor:
| Інтерфейс | Lifetime | Reload | Use Case |
|---|---|---|---|
IOptions<T> | Singleton | ❌ Ні | Конфігурація, що не змінюється |
IOptionsSnapshot<T> | Scoped | ✅ Так (per request) | Web API, конфігурація може змінюватися |
IOptionsMonitor<T> | Singleton | ✅ Так (real-time) | Background services, real-time reload |
Приклад з IOptionsSnapshot:
[ApiController]
[Route("api/[controller]")]
public class ConfigController : ControllerBase
{
private readonly IOptionsSnapshot<FeaturesOptions> _features;
public ConfigController(IOptionsSnapshot<FeaturesOptions> features)
{
_features = features;
}
[HttpGet("features")]
public IActionResult GetFeatures()
{
// Значення оновлюються при кожному request
return Ok(_features.Value);
}
}
IConfiguration["Key"]. Це дає IntelliSense, compile-time перевірку, валідацію та легше тестування.Змінні оточення, передані через -e, видимі у кількох місцях:
docker inspect — показує всі змінні контейнераdocker ps (іноді) — може показувати команду з -eps aux на хості може показати змінніДемонстрація проблеми:
# Запустити з секретом у змінній
docker run -d \
--name myapp \
-e DATABASE_PASSWORD=supersecret123 \
myapp:latest
# Подивитися секрет через inspect
docker inspect myapp | grep DATABASE_PASSWORD
# Вивід: "DATABASE_PASSWORD=supersecret123"
Будь-хто з доступом до Docker daemon може побачити секрет.
Docker Secrets — це механізм для безпечного зберігання секретів у Docker Swarm (orchestration platform). Секрети:
/run/secrets/docker inspectОбмеження: Docker Secrets працює лише у Docker Swarm mode. Для standalone Docker (без Swarm) є альтернативи.
Ми можемо імітувати Docker Secrets через монтування файлів:
Крок 1: Створити файл з секретом
echo "supersecret123" > db_password.txt
chmod 600 db_password.txt # Лише власник може читати
Крок 2: Змонтувати файл у контейнер
docker run -d \
--name myapp \
-v $(pwd)/db_password.txt:/run/secrets/db_password:ro \
myapp:latest
Крок 3: Читати секрет з файлу у C# коді
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Читати секрет з файлу
var dbPasswordPath = "/run/secrets/db_password";
string dbPassword = null;
if (File.Exists(dbPasswordPath))
{
dbPassword = File.ReadAllText(dbPasswordPath).Trim();
Console.WriteLine("Database password loaded from secret file");
}
else
{
// Fallback на змінну оточення (для Development)
dbPassword = builder.Configuration["DATABASE_PASSWORD"];
Console.WriteLine("Database password loaded from environment variable");
}
// Побудувати connection string
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
connectionString = connectionString.Replace("{PASSWORD}", dbPassword);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString)
);
var app = builder.Build();
app.Run();
}
}
appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Host=prod-db;Database=myapp;Username=postgres;Password={PASSWORD}"
}
}
Переваги:
docker inspectНедоліки:
Якщо ви використовуєте Docker Swarm, ви можете скористатися вбудованим механізмом секретів.
Крок 1: Ініціалізувати Swarm
docker swarm init
Крок 2: Створити секрет
# З файлу
echo "supersecret123" | docker secret create db_password -
# Або з stdin
docker secret create db_password db_password.txt
Крок 3: Створити сервіс з секретом
docker service create \
--name myapp \
--secret db_password \
-e ASPNETCORE_ENVIRONMENT=Production \
-p 5000:5000 \
myapp:latest
Що відбувається:
/run/secrets/db_passworddocker inspectЧитання у C# коді:
var dbPasswordPath = "/run/secrets/db_password";
if (File.Exists(dbPasswordPath))
{
var dbPassword = File.ReadAllText(dbPasswordPath).Trim();
// Використати пароль
}
Переваги Docker Swarm Secrets:
Недоліки:
Створимо ASP.NET Core Web API з підключенням до PostgreSQL, що коректно конфігурується для різних середовищ через змінні оточення.
Структура проєкту:
MyApp/
├── MyApp.csproj
├── Program.cs
├── appsettings.json
├── appsettings.Development.json
├── Controllers/
│ └── WeatherController.cs
├── Data/
│ └── AppDbContext.cs
├── Dockerfile
├── .dockerignore
├── .env.example
└── docker-compose.yml
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=myapp_dev;Username=postgres;Password=dev"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Information"
}
},
"Features": {
"EnableSwagger": true,
"EnableDetailedErrors": false,
"MaxItemsPerPage": 50
},
"AllowedHosts": "*"
}
Принцип: Файл містить безпечні значення за замовчуванням для Development. Секрети та production-конфігурація передаються через змінні оточення.
using Microsoft.EntityFrameworkCore;
using MyApp.Data;
var builder = WebApplication.CreateBuilder(args);
// Читання connection string (може бути перезаписаний через змінну оточення)
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Додати DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
// Strongly-typed конфігурація
builder.Services.Configure<FeaturesOptions>(
builder.Configuration.GetSection("Features"));
// Додати контролери
builder.Services.AddControllers();
// Swagger (умовно, залежно від Features:EnableSwagger)
var enableSwagger = builder.Configuration.GetValue<bool>("Features:EnableSwagger");
if (enableSwagger)
{
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
}
var app = builder.Build();
// Middleware
if (app.Environment.IsDevelopment() || enableSwagger)
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
// Логування конфігурації при старті (без секретів!)
var logger = app.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Environment: {Environment}", app.Environment.EnvironmentName);
logger.LogInformation("Database Host: {Host}",
connectionString?.Split(';').FirstOrDefault(s => s.StartsWith("Host="))?.Split('=')[1]);
logger.LogInformation("Swagger Enabled: {Swagger}", enableSwagger);
app.Run();
FeaturesOptions.cs:
namespace MyApp;
public class FeaturesOptions
{
public bool EnableSwagger { get; set; }
public bool EnableDetailedErrors { get; set; }
public int MaxItemsPerPage { get; set; }
}
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Копіювати .csproj та restore (окремий шар для кешування)
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
# Копіювати весь код та зібрати
COPY . .
RUN dotnet build -c Release -o /app/build
# Publish
RUN dotnet publish -c Release -o /app/publish --no-restore
# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app
# Створити non-root користувача
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
# Копіювати артефакти
COPY --from=build /app/publish .
# Змінити власника
RUN chown -R appuser:appuser /app
# Перемкнутися на non-root
USER appuser
# Змінні за замовчуванням (НЕ секрети!)
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://0.0.0.0:5000
EXPOSE 5000
ENTRYPOINT ["dotnet", "MyApp.dll"]
# Environment
ASPNETCORE_ENVIRONMENT=Development
# Database (замініть на реальні значення)
ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database=myapp;Username=postgres;Password=YOUR_PASSWORD_HERE
# Logging
Logging__LogLevel__Default=Information
Logging__LogLevel__Microsoft.AspNetCore=Warning
# Features
Features__EnableSwagger=true
Features__EnableDetailedErrors=false
Features__MaxItemsPerPage=50
version: '3.8'
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: dev
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:5000"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database=myapp;Username=postgres;Password=dev
- Features__EnableSwagger=true
- Features__EnableDetailedErrors=true
depends_on:
db:
condition: service_healthy
volumes:
postgres-data:
Development (локально з docker-compose):
# Запустити всі сервіси
docker compose up -d
# Перевірити логи
docker compose logs api
# Відкрити Swagger
open http://localhost:5000/swagger
Staging (окремий сервер):
# Створити .env файл для staging
cat > .env.staging << 'ENVFILE'
ASPNETCORE_ENVIRONMENT=Staging
ConnectionStrings__DefaultConnection=Host=staging-db.internal;Port=5432;Database=myapp_staging;Username=app_user;Password=staging_password_123
Logging__LogLevel__Default=Debug
Features__EnableSwagger=true
Features__EnableDetailedErrors=true
ENVFILE
# Запустити контейнер
docker run -d \
--name myapp-staging \
--env-file .env.staging \
-p 5000:5000 \
myapp:latest
Production (AWS/Azure):
# Секрети передаються через змінні оточення з secrets manager
docker run -d \
--name myapp-prod \
-e ASPNETCORE_ENVIRONMENT=Production \
-e ConnectionStrings__DefaultConnection="Host=${DB_HOST};Port=5432;Database=myapp_prod;Username=${DB_USER};Password=${DB_PASSWORD}" \
-e Logging__LogLevel__Default=Warning \
-e Features__EnableSwagger=false \
-e Features__EnableDetailedErrors=false \
-p 5000:5000 \
myapp:latest
Де ${DB_HOST}, ${DB_USER}, ${DB_PASSWORD} — змінні з AWS Secrets Manager або Azure Key Vault.
Погано:
ENV DATABASE_PASSWORD=supersecret123
{
"ConnectionStrings": {
"DefaultConnection": "Host=prod-db;Password=supersecret123"
}
}
Добре:
docker run -e DATABASE_PASSWORD=${DB_PASSWORD} myapp:latest
Створіть .env.example з усіма необхідними змінними (без реальних значень):
# .env.example
ASPNETCORE_ENVIRONMENT=Development
ConnectionStrings__DefaultConnection=Host=localhost;Database=myapp;Username=postgres;Password=YOUR_PASSWORD
API_KEY=YOUR_API_KEY_HERE
Додайте .env до .gitignore:
.env
.env.local
.env.*.local
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException(
"ConnectionStrings:DefaultConnection is not configured. " +
"Set ConnectionStrings__DefaultConnection environment variable.");
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Environment: {Env}", app.Environment.EnvironmentName);
logger.LogInformation("Database: {Host}", GetDatabaseHost(connectionString));
// НЕ логуйте паролі, API keys, tokens!
// Замість
var maxItems = builder.Configuration.GetValue<int>("Features:MaxItemsPerPage");
// Використовуйте
builder.Services.Configure<FeaturesOptions>(
builder.Configuration.GetSection("Features"));
// У контролері
public class MyController : ControllerBase
{
private readonly FeaturesOptions _features;
public MyController(IOptions<FeaturesOptions> features)
{
_features = features.Value;
}
}
| Середовище | Swagger | Detailed Errors | Log Level |
|---|---|---|---|
| Development | ✅ Enabled | ✅ Enabled | Debug |
| Staging | ✅ Enabled | ✅ Enabled | Information |
| Production | ❌ Disabled | ❌ Disabled | Warning |
Передавайте через змінні оточення:
# Development
-e Features__EnableSwagger=true -e Features__EnableDetailedErrors=true
# Production
-e Features__EnableSwagger=false -e Features__EnableDetailedErrors=false
Ключові концепції:
__ (подвійне підкреслення) для вкладених секцій: ConnectionStrings__DefaultConnection.-e, --env-file або Docker Secrets.Способи передачі змінних:
| Спосіб | Use Case | Безпека |
|---|---|---|
-e VAR=value | Одна-дві змінні | ⚠️ Видно у docker inspect |
--env-file .env | Багато змінних | ⚠️ Файл на диску |
ENV у Dockerfile | Значення за замовчуванням | ✅ Для non-sensitive |
| Docker Secrets | Production секрети | ✅ Зашифровано |
| Secrets Manager | Enterprise production | ✅ Централізовано, ротація |
Best Practices:
.env.example для документації.env до .gitignoreЩо далі:
У наступній статті ми розглянемо Docker Compose — інструмент для декларативного управління multi-container застосунками. Ви навчитеся описувати всю архітектуру (API + Database + Cache) у одному YAML-файлі та керувати нею однією командою.
Завдання 1.1: Передача змінних через -e
Запустіть офіційний образ Nginx з кастомним портом через змінну оточення.
docker run -d \
--name nginx-custom \
-e NGINX_PORT=8080 \
-p 8080:8080 \
nginx:alpine
Перевірка:
docker exec nginx-custom printenv NGINX_PORT
# Має вивести: 8080
Завдання 1.2: Використання --env-file
Створіть файл .env з трьома змінними та запустіть контейнер Alpine, що виводить ці змінні.
Створити .env:
APP_NAME=MyApp
APP_VERSION=1.0.0
APP_ENV=Development
Запустити:
docker run --rm --env-file .env alpine sh -c 'echo "App: $APP_NAME v$APP_VERSION ($APP_ENV)"'
Очікуваний вивід:
App: MyApp v1.0.0 (Development)
Завдання 1.3: ENV у Dockerfile
Створіть Dockerfile з змінними за замовчуванням та перезапишіть їх при запуску.
Dockerfile:
FROM alpine:latest
ENV MESSAGE="Hello from Dockerfile"
ENV COUNT=5
CMD echo "$MESSAGE" && echo "Count: $COUNT"
Побудувати та запустити:
# З значеннями за замовчуванням
docker build -t env-test .
docker run env-test
# Перезаписати при запуску
docker run -e MESSAGE="Custom message" -e COUNT=10 env-test
Завдання 2.1: Конфігурація .NET API
Створіть простий ASP.NET Core Web API, що читає конфігурацію з змінних оточення.
Вимоги:
/api/config повертає поточну конфігураціюProgram.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/api/config", (IConfiguration config) => new
{
Environment = config["ASPNETCORE_ENVIRONMENT"],
Database = config.GetConnectionString("DefaultConnection")?.Split(';')[0],
LogLevel = config["Logging:LogLevel:Default"]
});
app.Run();
Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY bin/Release/net8.0/publish/ .
ENV ASPNETCORE_URLS=http://0.0.0.0:5000
ENTRYPOINT ["dotnet", "MyApp.dll"]
Запустити з різними конфігураціями:
# Development
docker run -d -p 5000:5000 \
-e ASPNETCORE_ENVIRONMENT=Development \
-e ConnectionStrings__DefaultConnection="Host=localhost;Database=dev" \
myapp:latest
# Production
docker run -d -p 5001:5000 \
-e ASPNETCORE_ENVIRONMENT=Production \
-e ConnectionStrings__DefaultConnection="Host=prod-db;Database=prod" \
myapp:latest
Завдання 2.2: ARG для параметризації збірки
Створіть Dockerfile, що приймає версію .NET як ARG.
Dockerfile:
ARG DOTNET_VERSION=8.0
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}
ARG BUILD_DATE
ARG VERSION=1.0.0
LABEL version="${VERSION}"
LABEL build-date="${BUILD_DATE}"
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Побудувати з різними версіями:
# .NET 8.0
docker build --build-arg DOTNET_VERSION=8.0 --build-arg VERSION=1.0.0 -t myapp:8.0 .
# .NET 8.0 Alpine
docker build --build-arg DOTNET_VERSION=8.0-alpine --build-arg VERSION=1.0.1 -t myapp:8.0-alpine .
Завдання 2.3: Secrets через файли
Реалізуйте читання секретів з файлів замість змінних оточення.
Створити файл з секретом:
echo "supersecret123" > db_password.txt
chmod 600 db_password.txt
C# код для читання:
var dbPasswordPath = "/run/secrets/db_password";
string dbPassword = File.Exists(dbPasswordPath)
? File.ReadAllText(dbPasswordPath).Trim()
: builder.Configuration["DATABASE_PASSWORD"];
Запустити:
docker run -d \
-v $(pwd)/db_password.txt:/run/secrets/db_password:ro \
myapp:latest
Завдання 3.1: Multi-environment setup
Створіть повноцінний ASP.NET Core API з підтримкою трьох середовищ: Development, Staging, Production.
Вимоги:
Створити три .env файли:
.env.development.env.staging.env.productionЗапустити з різними конфігураціями:
docker run -d --env-file .env.development -p 5000:5000 myapp:latest
docker run -d --env-file .env.staging -p 5001:5000 myapp:latest
docker run -d --env-file .env.production -p 5002:5000 myapp:latest
Завдання 3.2: Інтеграція з Secrets Manager
Створіть скрипт, що завантажує секрети з AWS Secrets Manager та передає у Docker.
get-secrets.sh:
#!/bin/bash
DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id prod/db/password --query SecretString --output text)
API_KEY=$(aws secretsmanager get-secret-value --secret-id prod/api/key --query SecretString --output text)
docker run -d \
-e DATABASE_PASSWORD="$DB_PASSWORD" \
-e API_KEY="$API_KEY" \
myapp:latest
Завдання 3.3: Конфігурація через docker-compose
Створіть docker-compose.yml з підтримкою різних середовищ через override files.
docker-compose.yml (базова конфігурація):
version: '3.8'
services:
api:
image: myapp:latest
environment:
- ASPNETCORE_ENVIRONMENT=${ENV:-Development}
docker-compose.override.yml (Development):
version: '3.8'
services:
api:
environment:
- ConnectionStrings__DefaultConnection=Host=db;Database=dev
- Features__EnableSwagger=true
docker-compose.prod.yml (Production):
version: '3.8'
services:
api:
environment:
- ConnectionStrings__DefaultConnection=${DB_CONNECTION_STRING}
- Features__EnableSwagger=false
Запуск:
# Development
docker compose up -d
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d