Infrastructure & Deployment Plan — Laundry Management System¶
Document Information¶
| Field | Value |
|---|---|
| Project | Laundry Management System |
| Version | 1.0 |
| Language | English |
| Tech Stack | ASP.NET Core, Angular, Tauri, PostgreSQL, Docker, Traefik, Seq |
| Document Type | Infrastructure & Deployment Guide |
Table of Contents¶
- Deployment Architecture Overview
- The Central docker-compose.yml
- Hardware Specifications
- Deployment Type A — Online Server (Type 1)
- Deployment Type B — Offline Branch (Type 2)
- Deployment Type C — Offline Branch with Sync (Type 3)
- Traefik Configuration — HTTPS for All Deployments
- Browser Access Prevention (Offline Branches)
- The Installation Wizard
- Branch Setup USB Package
- Database Backup Strategy
- Monitoring with Seq
- Rollback Procedure
- Environment Strategy
- Peripheral Setup
1. Deployment Architecture Overview¶
Every instance of the system — online server, offline branch, or offline-sync branch — runs the same docker-compose.yml stack. Behavior is controlled by a single .env file.
flowchart TB
subgraph Docker["Docker Compose Stack"]
Traefik["Traefik<br/>:80 :443 :8080<br/>Reverse Proxy<br/>SSL via Let's Encrypt / mkcert"]
API["ASP.NET Core API<br/>:5000<br/>Serves /api + Angular static files"]
PG["PostgreSQL 16<br/>:5432 (internal)<br/>Encrypted volume"]
Seq["Seq<br/>:5341<br/>Logging & Tracing"]
end
Internet["Internet / Tauri App"] -->|HTTPS| Traefik
Traefik -->|Route: /api/*| API
Traefik -->|Route: /logs| Seq
Traefik -->|Route: static| API
API -->|SQL| PG
API -->|Logs & Traces| Seq
ENV[".env file<br/>BRANCH_MODE<br/>DOMAIN<br/>CERT_MODE<br/>BRANCH_ID"] -.- Docker
subgraph Types["Deployment Types"]
T1["Type A: Online Server<br/>CERT_MODE=acme<br/>DOMAIN=customer.com"]
T2["Type B: Offline Branch<br/>CERT_MODE=static<br/>DOMAIN=laundry.local"]
T3["Type C: Offline-Sync<br/>CERT_MODE=static<br/>DOMAIN=laundry.local<br/>+ Sync Export/Import"]
end
ENV ==> Types
Three Deployment Types¶
| Type | .env BRANCH_MODE | Domain | Certificates | Internet | Client |
|---|---|---|---|---|---|
| A — Online Server | online |
Customer's real domain | Let's Encrypt (ACME) | Required | Tauri app or browser |
| B — Offline Branch | offline |
laundry.local |
mkcert (pre-generated) | None | Tauri app only |
| C — Offline-Sync Branch | offline_sync |
laundry.local |
mkcert (pre-generated) | None (except manual sync) | Tauri app only |
2. The Central docker-compose.yml¶
version: "3.9"
services:
traefik:
image: traefik:v3
container_name: laundry-traefik
restart: unless-stopped
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.le.acme.email=${ACME_EMAIL:-admin@localhost}"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
- ./certs:/certs:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN}`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.entrypoints=websecure"
postgres:
image: postgres:16-alpine
container_name: laundry-postgres
restart: unless-stopped
environment:
POSTGRES_DB: laundry
POSTGRES_USER: laundry
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./backups:/backups
api:
image: ghcr.io/${GITHUB_ORG}/laundry-api:${VERSION:-latest}
container_name: laundry-api
restart: unless-stopped
depends_on:
- postgres
environment:
ConnectionStrings__Default: "Host=postgres;Port=5432;Database=laundry;Username=laundry;Password=${DB_PASSWORD}"
BranchMode: ${BRANCH_MODE}
BranchId: ${BRANCH_ID}
BranchCode: ${BRANCH_CODE}
Domain: ${DOMAIN}
SeqServerUrl: "http://seq:5341"
LicensePath: "/license/license.dat"
ClientTokenSecret: ${CLIENT_TOKEN_SECRET}
ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
volumes:
- ./license:/license:ro
- ./backups:/backups
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=le"
- "traefik.http.services.api.loadbalancer.server.port=5000"
seq:
image: datalust/seq:2024.1
container_name: laundry-seq
restart: unless-stopped
environment:
ACCEPT_EULA: "Y"
volumes:
- seqdata:/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.seq.rule=Host(`logs.${DOMAIN}`)"
- "traefik.http.routers.seq.entrypoints=websecure"
volumes:
pgdata:
seqdata:
letsencrypt:
3. Hardware Specifications¶
Type A — Online Server (Customer VPS)¶
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 vCPU | 4 vCPU |
| RAM | 4 GB | 8 GB |
| Disk | 40 GB SSD | 80 GB SSD |
| OS | Ubuntu 24.04 LTS | Ubuntu 24.04 LTS |
| Bandwidth | 1 TB/month | 2 TB/month |
| Providers | Hetzner CX22 (~$8/mo), DigitalOcean Basic ($24/mo), AWS Lightsail ($20/mo) |
Types B & C — Offline Branch PC¶
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | Intel Core i3 / AMD Ryzen 3 | Intel Core i5 / AMD Ryzen 5 |
| RAM | 8 GB | 16 GB |
| Disk | 256 GB SSD | 512 GB SSD |
| OS | Windows 10/11 Pro, Ubuntu 24.04 LTS, macOS 13+ | |
| GPU | Integrated | Integrated |
| Network | None required (B/C); brief LAN/internet optional (C for sync) | |
| UPS | Recommended for power backup |
4. Deployment Type A — Online Server (Type 1)¶
4.1 Server Preparation¶
# 1. Provision a fresh Ubuntu 24.04 VPS.
# 2. SSH into the server.
ssh root@<server-ip>
# 3. Install Docker
curl -fsSL https://get.docker.com | sh
usermod -aG docker $USER
# 4. Point DNS A record for the domain to the server IP:
# laundry.customer-domain.com → <server-ip>
# logs.customer-domain.com → <server-ip>
# 5. Create the application directory structure.
mkdir -p /opt/laundry/{certs,license,backups}
4.2 .env File — Type A¶
# /opt/laundry/.env
BRANCH_MODE=online
BRANCH_ID=00000000-0000-0000-0000-000000000000
BRANCH_CODE=MAIN
DB_PASSWORD=<auto-generated-32-char>
DOMAIN=laundry.customer-domain.com
CERT_MODE=acme
ACME_EMAIL=admin@customer-domain.com
VERSION=1.0.0
GITHUB_ORG=your-org
CLIENT_TOKEN_SECRET=<auto-generated-64-char>
ASPNETCORE_ENVIRONMENT=Production
4.3 Startup¶
cd /opt/laundry
# Place license.dat in ./license/
# Run:
docker compose up -d
# Check all services:
docker compose ps
# Expected: traefik:Up, postgres:Up, api:Up, seq:Up
4.4 Update (Online)¶
5. Deployment Type B — Offline Branch (Type 2)¶
5.1 Overview¶
Offline branches run an identical Docker Compose stack on a local PC. All traffic stays on localhost. HTTPS is provided by locally-trusted mkcert certificates. The Tauri desktop app connects to https://laundry.local.
5.2 Prerequisites (Installed via Branch Setup USB)¶
| Component | How Installed |
|---|---|
| Docker Engine | Docker Desktop for Windows/macOS, or get.docker.com for Linux |
| mkcert | Pre-downloaded binary on USB |
| Docker images (tar) | docker load < laundry-images.tar |
docker-compose.yml |
Copied from USB |
| Tauri installer | Run .msi / .dmg / .deb from USB |
license.dat |
Copied to C:\Laundry\license\ |
| Setup script | setup.bat / setup.sh |
5.3 .env File — Type B¶
# C:\Laundry\.env
BRANCH_MODE=offline
BRANCH_ID=<branch-uuid>
BRANCH_CODE=CAIRO-01
DB_PASSWORD=<auto-generated-32-char>
DOMAIN=laundry.local
CERT_MODE=static
VERSION=1.0.0
GITHUB_ORG=your-org
CLIENT_TOKEN_SECRET=<auto-generated-64-char>
ASPNETCORE_ENVIRONMENT=Production
5.4 mkcert Setup (Run Once)¶
# On the branch PC, via setup script:
mkcert -install
mkcert laundry.local localhost 127.0.0.1 ::1
# Output: laundry.local+3.pem, laundry.local+3-key.pem
# Copy to C:\Laundry\certs\
5.5 Traefik Static Cert Config¶
A traefik.yml file mounted alongside certificates for offline mode:
# C:\Laundry\traefik\traefik.yml (only for offline)
tls:
certificates:
- certFile: /certs/laundry.local+3.pem
keyFile: /certs/laundry.local+3-key.pem
5.6 Tauri Desktop App¶
After installation, the user double-clicks the "Laundry System" icon. The Tauri app opens a native window pointing to https://laundry.local. No browser ever appears.
5.7 Windows Auto-Start¶
To ensure Docker and the Laundry system start on boot:
# Create a scheduled task that runs on system startup:
# Action: Start a program
# Program: C:\Program Files\Docker\Docker\Docker Desktop.exe
# Then: docker compose -f C:\Laundry\docker-compose.yml up -d
6. Deployment Type C — Offline Branch with Sync (Type 3)¶
Identical to Type B in setup, with these additions:
6.1 .env Difference¶
6.2 Additional Features¶
| Feature | Implementation |
|---|---|
| Export Sync Data | Admin clicks "Export Sync Data" in the Tauri app → system compresses unsynced PostgreSQL rows to an AES-256 encrypted .lndsync file → saved to USB |
| Import Sync Data (Central Side) | Admin logs into the online server → Admin panel → "Import Branch Data" → uploads .lndsync → validation → upsert merge |
| Import Master Updates (Branch Side) | Admin receives .lndmaster file (new price lists, services, etc.) → imports via Tauri app → updates local config |
| Health Report | Included automatically in every .lndsync export: Docker status, disk usage, error counts, sync timestamp |
6.3 Sync File Format¶
sync_cairo-01_2026-05-09.lndsync (AES-256 encrypted ZIP containing:
├── journal_entry_lines.json
├── journal_entries.json
├── invoices.json
├── invoice_items.json
├── payments.json
├── customers.json
├── ... (all tables with sync_status='pending')
└── health_report.json
)
# Encryption key derived from: SHA-256(license_key + branch_id)
7. Traefik Configuration — HTTPS for All Deployments¶
7.1 Online: Let's Encrypt (ACME)¶
Controlled entirely by environment variables. Traefik auto-provisions and renews certificates. The docker-compose.yml uses tls.certresolver=le on the api service label.
7.2 Offline: Static mkcert Certificates¶
For offline deployments, the Traefik container uses a config file mounted at /etc/traefik/traefik.yml that references the static .pem files:
# This file is created by the setup wizard based on CERT_MODE=static
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
# No ACME resolver in offline mode.
tls:
certificates:
- certFile: /certs/laundry.local+3.pem
keyFile: /certs/laundry.local+3-key.pem
The wizard generates this file during setup for Types B & C.
8. Browser Access Prevention (Offline Branches)¶
Users must use the Tauri desktop app, not a browser. Two-layer gate:
Layer 1: ASP.NET Core Middleware¶
public class ClientTokenMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
var mode = _config["BranchMode"];
// Online deployments: no restriction
if (mode == "online")
{
await _next(context);
return;
}
// Offline: require X-Laundry-Client-Token header
if (!context.Request.Headers.TryGetValue("X-Laundry-Client-Token", out var token)
|| !ValidateToken(token))
{
context.Response.StatusCode = 403;
context.Response.ContentType = "text/html";
await context.Response.WriteAsync(@"
<html><body style='font-family:sans-serif;text-align:center;padding-top:80px'>
<h1>Access Denied</h1>
<p>Please use the <strong>Laundry System</strong> desktop application.</p>
<p>Opening in a browser is not supported on this device.</p>
</body></html>");
return;
}
await _next(context);
}
private bool ValidateToken(string token)
{
// Token = HMAC-SHA256(license_key + branch_id, CLIENT_TOKEN_SECRET)
var expected = ComputeHmac(_licenseKey + _branchId, _clientTokenSecret);
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(token),
Encoding.UTF8.GetBytes(expected));
}
}
Layer 2: Tauri Injects the Token¶
In src-tauri/src/main.rs:
// On app startup, compute the client token from license + branch_id
// Store in global state
// Before the WebView loads, install a request interceptor that adds the header:
// X-Laundry-Client-Token: <computed-token>
The token is computed from the license.dat file and laundry.config.json — known only to the Tauri binary. Angular never sees it.
9. The Installation Wizard¶
9.1 Overview¶
The wizard is built into the Tauri application. On first launch, if laundry.config.json is missing or setupComplete is false, the wizard screens appear instead of the login page.
9.2 Screens¶
Screen 1: Welcome & Type Selection - Choose: Online Server / Offline Branch / Offline-Sync Branch - Language toggle: English / العربية
Screen 2: Branch Information - Branch Name, Branch Code, Currency, Fiscal Year Start, Default Language - System auto-generates Branch UUID
Screen 3: License
- File picker for license.dat
- Auto-validates: extracts customer name, expiry date, max branches
- Shows validation result with green check / red error
Screen 4: Domain (Online only) - Customer enters their domain name - System checks DNS resolution - Email for Let's Encrypt notifications
Screen 5: Docker Check & Service Start
- Verifies Docker is installed and running
- (First run) Loads images from laundry-images.tar
- Creates Docker volumes
- Generates .env file with all variables
- Creates traefik.yml for offline (if applicable)
- Generates mkcert certificates (if offline)
- Starts docker compose up -d
- Runs EF Core migrations (dotnet ef database update)
- Pre-upgrade: creates a PostgreSQL dump backup
Screen 6: Admin Account
- Username: default admin
- Option 1: Click "Generate" for a 16-char auto-generated password
- Option 2: Type custom password (min 8 chars, 1 uppercase, 1 number, 1 special char)
- "I have saved this password" checkbox required before Finish
- Show Password + Copy buttons available
Screen 7: Complete
- Summary of what was set up
- URL: https://laundry.local (offline) or https://customer-domain.com (online)
- "Launch Laundry System" button
9.3 What the Wizard Produces¶
| File | Purpose |
|---|---|
.env |
All Docker environment variables |
laundry.config.json |
Tauri app config (apiUrl, mode, branchId, clientToken) |
./certs/*.pem |
mkcert certificates (offline only) |
./license/license.dat |
Customer license file |
./traefik/traefik.yml |
Traefik static config (offline only) |
| Docker volumes created | pgdata, seqdata, certs, license, backups |
| First admin user | Created in the database |
10. Branch Setup USB Package¶
For offline branch deployment by a technician, a single USB drive contains:
Laundry_Setup_USB/
├── README.txt # Instructions in AR/EN
├── setup.bat # Windows: runs everything
├── setup.sh # Linux/macOS: runs everything
├── docker/
│ ├── Docker Desktop Installer.exe # Windows Docker
│ ├── docker-install.sh # Linux Docker install script
│ └── laundry-images_v1.0.0.tar # docker save of all images
├── mkcert/
│ ├── mkcert-v1.4.4-windows-amd64.exe
│ ├── mkcert-v1.4.4-linux-amd64
│ └── mkcert-v1.4.4-darwin-amd64
├── app/
│ ├── LaundrySystem_1.0.0_x64_en-US.msi # Tauri Windows installer
│ ├── LaundrySystem_1.0.0_aarch64.dmg # Tauri macOS installer
│ └── laundry-system_1.0.0_amd64.deb # Tauri Linux installer
├── docker-compose.yml
├── license.dat # Customer-specific
└── backup/ # Empty, for post-setup backup
setup.bat (Windows — Simplified)¶
@echo off
echo ==========================================
echo Laundry System - Branch Setup
echo ==========================================
echo.
echo [1/6] Installing Docker Engine...
start /wait "" "%~dp0docker\Docker Desktop Installer.exe" --quiet
echo Done.
echo [2/6] Loading Docker images...
docker load -i "%~dp0docker\laundry-images_v1.0.0.tar"
echo Done.
echo [3/6] Installing mkcert...
copy "%~dp0mkcert\mkcert-v1.4.4-windows-amd64.exe" "%ProgramFiles%\mkcert\mkcert.exe"
set PATH=%PATH%;%ProgramFiles%\mkcert
mkcert -install
mkcert laundry.local localhost 127.0.0.1 ::1
mkdir "C:\Laundry\certs"
move *.pem "C:\Laundry\certs\"
echo Done.
echo [4/6] Copying application files...
mkdir "C:\Laundry\license"
copy "%~dp0docker-compose.yml" "C:\Laundry\"
copy "%~dp0license.dat" "C:\Laundry\license\"
echo Done.
echo [5/6] Installing Laundry System desktop app...
msiexec /i "%~dp0app\LaundrySystem_1.0.0_x64_en-US.msi" /quiet
echo Done.
echo [6/6] Setup complete!
echo.
echo The Laundry System will start on next login.
echo Or run: docker compose -f C:\Laundry\docker-compose.yml up -d
echo Then launch "Laundry System" from the Start Menu.
pause
11. Database Backup Strategy¶
11.1 Central Server (Type 1)¶
| Method | Frequency | Retention |
|---|---|---|
pg_dump via cron |
Daily at 02:00 | 30 days |
| Docker volume snapshot | Weekly | 4 weeks |
| WAL archiving | Continuous | Enabled for point-in-time recovery |
| Off-site copy | Daily | Synced to S3/cloud storage |
11.2 Offline Branches (Type 2 & 3)¶
| Method | How |
|---|---|
| Scheduled backup | Quartz.NET job runs at 02:00 daily → docker exec laundry-postgres pg_dump ... → saves AES-256 encrypted .sql.gz to ./backups/ Docker volume (mounted to C:\Laundry\backups\ on host) |
| Pre-upgrade backup | Wizard runs a full dump before applying EF Core migrations |
| Pre-sync backup (Type 3) | Before exporting sync data, the system auto-creates a backup of the current state |
| Manual backup | Admin UI button: "Create Backup" → saves to configured backup path |
| Backup rotation | Last 7 daily backups retained. Weekly backups retained for 4 weeks. |
11.3 Restore Procedure¶
# 1. Stop the application
docker compose -f C:\Laundry\docker-compose.yml stop api
# 2. Restore the database
docker exec -i laundry-postgres psql -U laundry -d laundry < backup_2026-05-09.sql
# 3. Restart
docker compose -f C:\Laundry\docker-compose.yml up -d
12. Monitoring with Seq¶
12.1 Architecture¶
Seq runs as a Docker container on every instance — online and offline. It receives structured logs from ASP.NET Core via the Serilog Seq sink. No external service required.
12.2 Accessing Seq¶
| Deployment | Access URL |
|---|---|
| Online (Type 1) | https://logs.customer-domain.com (secured by Traefik + Let's Encrypt) |
| Offline (Types 2/3) | http://localhost:5341 (local network only) |
Alternatively, the Admin UI has a "System Logs" page that queries the Seq API and displays recent logs in-app. This avoids opening a separate tab on offline branches.
12.3 What Is Logged¶
- All API requests (method, path, status code, duration)
- All errors (full stack traces)
- All accounting events (journal entry posted, GL balance mismatch)
- All sync operations (export, import, validation errors)
- All authentication events (login success/failure)
- Docker health checks (service restarts)
- License validation events
12.4 Remote Support¶
When a customer calls with an issue at an offline branch:
1. Remote into the branch PC via TeamViewer/AnyDesk.
2. Open http://localhost:5341.
3. Search for errors by time range, log level, or keyword.
4. Export relevant logs as JSON for offline analysis.
13. Rollback Procedure¶
13.1 Strategy¶
Every Docker image is tagged with its SemVer version. The previous version's image remains on the host until explicitly removed. The .env file controls which version is active.
13.2 Online Server Rollback¶
cd /opt/laundry
# 1. Stop current version
docker compose down
# 2. Edit .env: VERSION=1.0.1 (previous version)
nano .env
# 3. Start previous version
docker compose up -d
# 4. Verify
docker compose ps
docker compose logs api --tail=20
13.3 Offline Branch Rollback¶
cd C:\Laundry
# 1. Stop current version
docker compose down
# 2. Load previous image version (if not cached locally)
docker load -i laundry-images_v1.0.1.tar
# 3. Edit .env: VERSION=1.0.1
notepad .env
# 4. Start previous version
docker compose up -d
13.4 Database Compatibility¶
EF Core migrations are additive-only. Rolling back the API does not roll back the database — the previous API must be compatible with the newer schema. All migrations are designed to be backward-compatible:
- Add columns: always nullable or with defaults
- Remove columns: mark as obsolete, remove in next major version
- Rename: never rename; add new, deprecate old
14. Environment Strategy¶
| Environment | Purpose | Hosting | Deployment |
|---|---|---|---|
| Dev (Local) | Developer machines | Docker Desktop | Manual via docker compose up |
| Staging | Pre-release testing, customer demos | Small VPS ($10/mo) | Auto via GitHub Actions on merge to develop |
| Production | Customer servers | Per customer (VPS or branch PC) | Manual (online: docker compose pull; offline: USB) |
15. Peripheral Setup¶
15.1 Thermal Printer (80mm)¶
| OS | Setup |
|---|---|
| Windows | Install printer driver. Set paper size to 80mm × 297mm (or continuous roll). Name the printer "LaundryReceipt". |
| Linux | CUPS setup with 80mm paper size. |
| macOS | Standard printer setup. |
The Angular app prints receipts via window.print() with @media print CSS sized for 80mm. The Tauri app provides an API to trigger system print.
15.2 Barcode Scanner (USB)¶
- Any USB barcode scanner set to keyboard-emulation mode.
- Scans into the active input field.
- Scans Codes 128 barcodes generated by the system (via jsbarcode library).
- Camera-based scanning: optional QR code scanning via the Tauri camera API for mobile/tablet use.
15.3 Cash Drawer (Optional)¶
If a cash drawer is connected: - Triggers open via ESC/POS command sent through the thermal printer or a dedicated serial command via Tauri's serial port API.
Revision History¶
| Date | Version | Author | Changes |
|---|---|---|---|
| 2026-05-10 | 1.0 | System Architect | Initial infrastructure & deployment plan |