Compare commits
15 Commits
main
...
postgres-m
| Author | SHA1 | Date | |
|---|---|---|---|
| f185cf8a3f | |||
| f1b55db6d1 | |||
| 7a8e371195 | |||
| e5207d53b7 | |||
| a69862fe73 | |||
| 9b51c866c3 | |||
| 2a786acfb6 | |||
| 40516e8f23 | |||
| f58c14511b | |||
| 5cd80dbf34 | |||
| 923b6b325b | |||
| 559a6e589c | |||
| 9f86b215f9 | |||
| 446423d1a6 | |||
| 6bbc831ae0 |
@@ -2,11 +2,15 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app
|
||||
|
||||
# Publish ONLY the API project: the build context contains the whole
|
||||
# solution, and publishing the sln would dump every project's
|
||||
# appsettings.json into /app (last one wins) — e.g. DataCopy's
|
||||
# overwrote the API's and broke IppanelSms startup.
|
||||
RUN dotnet restore GreenHome.Api/GreenHome.Api.csproj
|
||||
RUN dotnet publish GreenHome.Api/GreenHome.Api.csproj -c Release -o /app
|
||||
|
||||
# مرحله runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app .
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using FluentValidation;
|
||||
using GreenHome.AI.DeepSeek;
|
||||
using Microsoft.AspNetCore.Mvc.Versioning;
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Infrastructure;
|
||||
using GreenHome.Infrastructure.Data;
|
||||
using GreenHome.Sms.Ippanel;
|
||||
using GreenHome.VoiceCall.Avanak;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
@@ -42,7 +44,17 @@ app.Run();
|
||||
|
||||
void ConfigureCoreServices(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddControllers();
|
||||
// API versioning: existing controllers stay unversioned (default v1.0, same URLs).
|
||||
// New v2 controllers use [ApiVersion("2.0")] + [Route("api/v2/[controller]")] (or [Route("api/v2")] prefix).
|
||||
builder.Services
|
||||
.AddApiVersioning(options =>
|
||||
{
|
||||
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.ReportApiVersions = true;
|
||||
options.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
})
|
||||
.AddMvc();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
@@ -110,7 +122,19 @@ void ApplyMigrations(WebApplication app)
|
||||
try
|
||||
{
|
||||
var context = services.GetRequiredService<GreenHomeDbContext>();
|
||||
context.Database.Migrate();
|
||||
var provider = app.Configuration["Database:Provider"] ?? "SqlServer";
|
||||
|
||||
if (provider == "Postgres")
|
||||
{
|
||||
// Soft-migration path: Postgres starts empty. EnsureCreated() is
|
||||
// idempotent — it builds the full schema from the model only when no
|
||||
// tables exist yet, and is a no-op once schema + data are in place.
|
||||
context.Database.EnsureCreated();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Database.Migrate();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Database": {
|
||||
"Provider": "Postgres"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Default": "Server=.;Integrated Security=False;Trusted_Connection=True;Encrypt=false;MultipleActiveResultSets=true;Database=GreenHomeDb;"
|
||||
"Default": "Server=.;Integrated Security=False;Trusted_Connection=True;Encrypt=false;MultipleActiveResultSets=true;Database=GreenHomeDb;",
|
||||
"Postgres": "Host=localhost;Port=5432;Database=greenhome;Username=greenhome;Password=greenhome"
|
||||
},
|
||||
"AvanakVoiceCall": {
|
||||
"Token": "A948B776B90CFD919B0EC60929714136CCB49DDB"
|
||||
|
||||
22
src/GreenHome.DataCopy/GreenHome.DataCopy.csproj
Normal file
22
src/GreenHome.DataCopy/GreenHome.DataCopy.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GreenHome.Infrastructure\GreenHome.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
222
src/GreenHome.DataCopy/Program.cs
Normal file
222
src/GreenHome.DataCopy/Program.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using System.Data.Common;
|
||||
using System.Reflection;
|
||||
using System.Linq.Expressions;
|
||||
using GreenHome.Infrastructure.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql;
|
||||
|
||||
namespace GreenHome.DataCopy;
|
||||
|
||||
// ============================================================
|
||||
// GreenHome.DataCopy — SQL Server -> PostgreSQL data copy tool
|
||||
//
|
||||
// Usage:
|
||||
// dotnet run --project GreenHome.DataCopy (idempotent, resumable)
|
||||
// dotnet run --project GreenHome.DataCopy -- --reset (wipe target tables first)
|
||||
//
|
||||
// Reads from SQL Server (read-only, via EF), writes to PostgreSQL (via EF),
|
||||
// preserving PKs. EF handles all column type conversions (bytea, jsonb, ...).
|
||||
// Safe to re-run after a crash: only missing rows are copied.
|
||||
// Ends with a row-count self-check per table (exit code 1 on mismatch).
|
||||
// ============================================================
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
const int BatchSize = 5000;
|
||||
|
||||
// Generic copy handlers, dispatched per entity type via reflection.
|
||||
static readonly MethodInfo SingleKeyCopy =
|
||||
typeof(Program).GetMethod(nameof(CopySingleIntKeyTable), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
static readonly MethodInfo CompositeKeyCopy =
|
||||
typeof(Program).GetMethod(nameof(CopyCompositeKeyTable), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
static async Task<int> Main(string[] args)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddEnvironmentVariables() // lets the server pass ConnectionStrings__* env vars
|
||||
.Build();
|
||||
|
||||
var sqlServerCs = config.GetConnectionString("SqlServer")
|
||||
?? throw new InvalidOperationException("Missing ConnectionStrings:SqlServer");
|
||||
var pgCs = config.GetConnectionString("Postgres")
|
||||
?? throw new InvalidOperationException("Missing ConnectionStrings:Postgres");
|
||||
var reset = args.Contains("--reset");
|
||||
|
||||
using var source = new GreenHomeDbContext(
|
||||
new DbContextOptionsBuilder<GreenHomeDbContext>().UseSqlServer(sqlServerCs).Options);
|
||||
using var target = new GreenHomeDbContext(
|
||||
new DbContextOptionsBuilder<GreenHomeDbContext>().UseNpgsql(pgCs).Options);
|
||||
|
||||
target.Database.EnsureCreated();
|
||||
|
||||
var entityTypes = target.Model.GetEntityTypes()
|
||||
.Where(e => e.GetTableName() is not null)
|
||||
.Select(e => (Entity: e, Table: e.GetTableName()!))
|
||||
.ToList();
|
||||
|
||||
// --- Topological sort: parents before children (Kahn) ---
|
||||
var deps = entityTypes.ToDictionary(
|
||||
t => t.Table,
|
||||
t => t.Entity.GetForeignKeys()
|
||||
.Select(fk => fk.PrincipalEntityType.GetTableName()!)
|
||||
.Where(pt => pt != t.Table)
|
||||
.ToHashSet());
|
||||
|
||||
var order = new List<(IEntityType Entity, string Table)>();
|
||||
var remaining = deps.ToDictionary(kv => kv.Key, kv => kv.Value.ToHashSet());
|
||||
while (remaining.Count > 0)
|
||||
{
|
||||
var ready = remaining.Where(kv => kv.Value.Count == 0).Select(kv => kv.Key).OrderBy(t => t).ToList();
|
||||
if (ready.Count == 0)
|
||||
ready = [remaining.Keys.OrderBy(t => t).First()]; // cycle fallback (shouldn't happen)
|
||||
foreach (var table in ready)
|
||||
{
|
||||
order.Add(entityTypes.First(e => e.Table == table));
|
||||
remaining.Remove(table);
|
||||
foreach (var kv in remaining) kv.Value.Remove(table);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Optional reset: children first (reverse dependency order) ---
|
||||
if (reset)
|
||||
{
|
||||
foreach (var (_, table) in order.AsEnumerable().Reverse())
|
||||
await target.Database.ExecuteSqlRawAsync($"DELETE FROM \"{table}\"");
|
||||
Console.WriteLine("[reset] all tables cleared");
|
||||
}
|
||||
|
||||
// --- Copy each table ---
|
||||
foreach (var (entity, table) in order)
|
||||
{
|
||||
var pk = entity.FindPrimaryKey();
|
||||
if (pk is null)
|
||||
{
|
||||
Console.WriteLine($"[skip ] {table} (no primary key)");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pk.Properties.Count == 1 && pk.Properties[0].ClrType == typeof(int))
|
||||
await (Task)SingleKeyCopy.MakeGenericMethod(entity.ClrType)
|
||||
.Invoke(null, [source, target, pgCs, entity, table, BatchSize])!;
|
||||
else
|
||||
await (Task)CompositeKeyCopy.MakeGenericMethod(entity.ClrType)
|
||||
.Invoke(null, [source, target, entity, BatchSize])!;
|
||||
}
|
||||
|
||||
// --- Self-check: row counts must match ---
|
||||
Console.WriteLine();
|
||||
var problems = 0;
|
||||
await using var srcConn = new SqlConnection(sqlServerCs);
|
||||
await using var pgConn = new NpgsqlConnection(pgCs);
|
||||
await srcConn.OpenAsync();
|
||||
await pgConn.OpenAsync();
|
||||
|
||||
foreach (var (_, table) in order)
|
||||
{
|
||||
// SQL Server COUNT(*) is Int32, Postgres is Int64 — Convert handles both
|
||||
long src = Convert.ToInt64(await ScalarAsync(srcConn, $"SELECT COUNT(*) FROM {table}"))!;
|
||||
long dst = Convert.ToInt64(await ScalarAsync(pgConn, $"SELECT COUNT(*) FROM \"{table}\""))!;
|
||||
if (src != dst) problems++;
|
||||
Console.WriteLine($"{(src == dst ? "OK " : "FAIL ")} {table,-28} source={src,-10} target={dst}");
|
||||
}
|
||||
|
||||
Console.WriteLine(problems == 0
|
||||
? "\nSelf-check passed: all tables match."
|
||||
: $"\nSelf-check FAILED: {problems} table(s) mismatched.");
|
||||
return problems == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Generic copy handlers
|
||||
// ============================================================
|
||||
|
||||
static async Task CopySingleIntKeyTable<TEntity>(
|
||||
GreenHomeDbContext source, GreenHomeDbContext target, string pgCs,
|
||||
IEntityType entity, string table, int batchSize) where TEntity : class
|
||||
{
|
||||
var pkName = entity.FindPrimaryKey()!.Properties[0].Name;
|
||||
|
||||
await using var pg = new NpgsqlConnection(pgCs);
|
||||
await pg.OpenAsync();
|
||||
|
||||
// Resume point: only copy rows with PK greater than what target already has
|
||||
var frontier = (int)(await ScalarAsync(pg, $"SELECT COALESCE(MAX(\"{pkName}\"), 0) FROM \"{table}\""))!;
|
||||
long copied = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var param = Expression.Parameter(typeof(TEntity), "e");
|
||||
var pkProp = Expression.Property(param, pkName);
|
||||
var where = Expression.Lambda<Func<TEntity, bool>>(
|
||||
Expression.GreaterThan(pkProp, Expression.Constant(frontier)), param);
|
||||
var orderBy = Expression.Lambda<Func<TEntity, int>>(pkProp, param);
|
||||
|
||||
var batch = await source.Set<TEntity>().AsNoTracking()
|
||||
.Where(where).OrderBy(orderBy).Take(batchSize).ToListAsync();
|
||||
if (batch.Count == 0) break;
|
||||
|
||||
target.Set<TEntity>().AddRange(batch);
|
||||
await target.SaveChangesAsync();
|
||||
copied += batch.Count;
|
||||
frontier = batch.Max(e => (int)GetProp(e, pkName)!);
|
||||
Console.Write($"\r[copy ] {table}: {copied} rows (pk={frontier}) ");
|
||||
}
|
||||
|
||||
var max = (int)(await ScalarAsync(pg, $"SELECT COALESCE(MAX(\"{pkName}\"), 0) FROM \"{table}\""))!;
|
||||
if (max > 0)
|
||||
{
|
||||
// Keep the identity sequence above the copied max so new rows don't collide.
|
||||
// pg_get_serial_sequence(text, text) — verified signature. format('%I', ...)
|
||||
// preserves the exact table case EF used; the column is passed as plain text.
|
||||
await target.Database.ExecuteSqlRawAsync(
|
||||
$"SELECT setval(pg_get_serial_sequence(format('%I', '{table}'), '{pkName}'), {max})");
|
||||
}
|
||||
Console.WriteLine($"\n[done ] {table}: copied={copied}, maxId={max}");
|
||||
}
|
||||
|
||||
static async Task CopyCompositeKeyTable<TEntity>(
|
||||
GreenHomeDbContext source, GreenHomeDbContext target, IEntityType entity, int batchSize) where TEntity : class
|
||||
{
|
||||
var pkNames = entity.FindPrimaryKey()!.Properties.Select(p => p.Name).ToArray();
|
||||
|
||||
// Composite-PK tables are small (e.g. DeviceUsers): load all source rows,
|
||||
// skip ones already present, write in batches.
|
||||
var existing = new HashSet<string>();
|
||||
foreach (var e in await target.Set<TEntity>().AsNoTracking().ToListAsync())
|
||||
existing.Add(KeyOf(e, pkNames));
|
||||
|
||||
long copied = 0;
|
||||
foreach (var row in await source.Set<TEntity>().AsNoTracking().ToListAsync())
|
||||
{
|
||||
if (existing.Contains(KeyOf(row, pkNames))) continue;
|
||||
target.Set<TEntity>().Add(row);
|
||||
existing.Add(KeyOf(row, pkNames));
|
||||
copied++;
|
||||
if (copied % batchSize == 0) await target.SaveChangesAsync();
|
||||
}
|
||||
await target.SaveChangesAsync();
|
||||
Console.WriteLine($"[done ] {typeof(TEntity).Name} (composite PK): copied={copied}");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Plumbing
|
||||
// ============================================================
|
||||
|
||||
static async Task<object?> ScalarAsync(DbConnection conn, string sql)
|
||||
{
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return await cmd.ExecuteScalarAsync();
|
||||
}
|
||||
|
||||
static object? GetProp(object entity, string name)
|
||||
=> entity.GetType().GetProperty(name)?.GetValue(entity);
|
||||
|
||||
static string KeyOf(object entity, string[] pkNames)
|
||||
=> string.Join("|", pkNames.Select(n => GetProp(entity, n)?.ToString() ?? "null"));
|
||||
}
|
||||
6
src/GreenHome.DataCopy/appsettings.json
Normal file
6
src/GreenHome.DataCopy/appsettings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=.;Integrated Security=False;Trusted_Connection=True;Encrypt=false;Database=GreenHomeDb;",
|
||||
"Postgres": "Host=localhost;Port=5432;Database=greenhome;Username=greenhome;Password=greenhome"
|
||||
}
|
||||
}
|
||||
@@ -497,5 +497,25 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
b.HasIndex(x => x.At);
|
||||
b.HasIndex(x => x.AdminUserId);
|
||||
});
|
||||
|
||||
// Postgres-specific mappings (SQL Server path is untouched).
|
||||
// MUST run last: fluent HasMaxLength/HasColumnType calls above set the
|
||||
// values we adjust here, so this block has to see the final model state.
|
||||
// 1) DateTime -> "timestamp" (no tz): mirrors naive datetime2, accepts any Kind.
|
||||
// 2) varchar: Postgres counts BYTES, SQL Server's nvarchar counts CHARACTERS.
|
||||
// x4 keeps char-count semantics for any UTF-8 charset (Persian = 2 bytes/char).
|
||||
if (Database.ProviderName == "Npgsql.EntityFrameworkCore.PostgreSQL")
|
||||
{
|
||||
foreach (var et in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var p in et.GetProperties())
|
||||
{
|
||||
if (p.ClrType == typeof(DateTime) || p.ClrType == typeof(DateTime?))
|
||||
p.SetColumnType("timestamp");
|
||||
else if (p.ClrType == typeof(string) && p.GetMaxLength() is int len)
|
||||
p.SetColumnType($"character varying({len * 4})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using GreenHome.Infrastructure.Services.Telemetry;
|
||||
using GreenHome.Infrastructure.Services.Usage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
@@ -28,9 +29,19 @@ public static class DependencyInjection
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Entity Framework
|
||||
// Provider is switchable via config ("Database:Provider" = "SqlServer" | "Postgres")
|
||||
// to allow a soft migration: default stays SqlServer until cutover.
|
||||
var provider = configuration["Database:Provider"] ?? "SqlServer";
|
||||
services.AddDbContext<GreenHomeDbContext>(options =>
|
||||
{
|
||||
options.UseSqlServer(configuration.GetConnectionString("Default"));
|
||||
if (provider == "Postgres")
|
||||
{
|
||||
options.UseNpgsql(configuration.GetConnectionString("Postgres"));
|
||||
}
|
||||
else
|
||||
{
|
||||
options.UseSqlServer(configuration.GetConnectionString("Default"));
|
||||
}
|
||||
});
|
||||
|
||||
// Auth
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -17,40 +17,114 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.VoiceCall.Avanak"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.AI.DeepSeek", "GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj", "{63D676E7-B882-4E70-8090-E56CFD516B0A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.DataCopy", "GreenHome.DataCopy\GreenHome.DataCopy.csproj", "{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Release|x64.Build.0 = Release|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{31D70B40-BD35-4DA8-8E2B-B51120C85F19}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4E330D53-37E7-42D3-B38C-E0DDF52CEA35}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D41BC2ED-CB72-4A98-82D6-8B6B7EC9423C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|x64.Build.0 = Release|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0F5C7A31-54F9-4C29-8E05-66CECAF520C7}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
44
src/infra/docker-compose.yml
Normal file
44
src/infra/docker-compose.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
# Shared database infrastructure — standalone from the app.
|
||||
# Reused by GreenHome and any future service on the same Docker network.
|
||||
#
|
||||
# Server usage (one-time):
|
||||
# 1. mkdir -p /opt/infra && copy this file there
|
||||
# 2. docker network create greenNetwork (skip if it already exists)
|
||||
# 3. cd /opt/infra && docker compose up -d
|
||||
# 4. docker compose exec postgres pg_isready -U greenhome
|
||||
#
|
||||
# App connection string: Host=postgres;Port=5432;Database=greenhome;Username=greenhome;Password=<same as below>
|
||||
#
|
||||
# Adding a DB for another service later (no restart needed):
|
||||
# docker compose exec -it postgres psql -U greenhome -c "CREATE DATABASE otherapp OWNER otherapp;"
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
container_name: postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: greenhome
|
||||
POSTGRES_USER: greenhome
|
||||
# ⚠️ set a strong password here and use the same value in the app's
|
||||
# ConnectionStrings__Postgres env var
|
||||
POSTGRES_PASSWORD: CHANGE_ME_STRONG_PASSWORD
|
||||
# localhost only: containers on greenNetwork reach it by name "postgres";
|
||||
# host psql can still connect via 127.0.0.1:5432. Do NOT bind 0.0.0.0.
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U greenhome -d greenhome"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- greenNetwork
|
||||
|
||||
networks:
|
||||
greenNetwork:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
Reference in New Issue
Block a user