Ritzy One • Deployment Guide

Docker on Windows Server 2025

Host the Ritzy One Docker stacks on a Windows Server 2025 machine: Docker Engine inside WSL2 Ubuntu, owned by one dedicated account, reachable from the network, started automatically at boot and backed up every night.

Applies to the Ritzy One Docker bundle (docker/) on a physical Windows Server 2025  |  Audience: IT / System administrators
Status — first server install pending
This procedure follows Microsoft's documented WSL settings and the Ritzy Docker bundle, but it has not yet been run end to end on a production Windows Server 2025. Do the first install on a server you can rebuild, and work through it in order.

1 How it fits together

Ritzy One's containers are Linux images. On Windows Server they run inside WSL2 — a real Linux kernel in a lightweight VM that Windows manages — with Docker Engine installed in Ubuntu. Each customer is a separate stack: its own app container, its own Firebird container and its own data volume.

Windows Server 2025 Windows Firewall + Hyper-V firewall · startup task · D:\RitzyBackups WSL2 VM — owned by ritzysvc (mirrored networking) Ubuntu 24.04 · systemd · Docker Engine · cron (nightly backups) Stack: customer A app — port 8080 postgres (not published) volume: databases, SDK Stack: customer B app — port 8090 postgres (not published) volume: databases, SDK More stacks… one per customer, each on its own port, each with memory and CPU limits
One Windows account owns the WSL VM; every customer stack runs inside it.
ChoiceWhy
Docker Engine in WSL2Docker Desktop is not supported on Windows Server and needs a paid licence for business use. Windows containers can't run Linux images.
Mirrored networkingThe containers appear on the server's own IP address, so other PCs reach them directly — no port forwarding.
One dedicated accountWSL belongs to one Windows user (next section).

2 Prerequisites & the ritzysvc account

NeedMinimumRecommended
WindowsServer 2025 Standard or Datacenter, fully updated
CPU4 cores with virtualization8 cores
RAM8 GB16 GB or more — each customer stack needs ~1.5–2.5 GB
Free disk60 GB on the system drive (WSL's disk lives in the user profile)200 GB, SSD

Why one dedicated account

WSL is per Windows user. The Ubuntu distro, Docker and every stack inside it, the WSL settings file and the startup task all belong to the account that created them. Another administrator who signs in sees no Ubuntu at all, and there is no system-wide WSL configuration. So the server gets one account, ritzysvc, that owns everything.

Create it

Signed in as any administrator, in PowerShell (Admin). Run the commands one at a time, in order.

First, choose the password. It must:

If Windows rejects it, net accounts shows this server's minimum length.

  1. Ask for the password. Paste only this line and press Enter:
    $pw = Read-Host -AsSecureString "Password for ritzysvc"
    PowerShell shows Password for ritzysvc: and waits. Now type the password and press Enter; it appears as asterisks.
    Two easy mistakes
    • Don't put your password inside the quotes. The quoted text is only the label PowerShell shows.
    • Don't paste this line together with the next ones. The prompt would take the next pasted line as the password. The account then fails with InvalidPasswordException, because that line contains ritzysvc.
  2. Create the account:
    New-LocalUser -Name "ritzysvc" -Password $pw -FullName "RitzyWeb Docker host" -Description "Owns WSL, Docker and the RitzyWeb stacks" -PasswordNeverExpires -AccountNeverExpires
    If it fails with InvalidPasswordException, the password broke a rule above — go back to step 1 and choose another.
  3. Make it an administrator:
    Add-LocalGroupMember -Group "Administrators" -Member "ritzysvc"
  4. Check — expect ritzysvc  True and an empty PasswordExpires:
    Get-LocalUser ritzysvc | Select-Object Name, Enabled, PasswordExpires
  5. Make a shared drop folder for files you copy to the server (SDK zip, migration bundles):
    New-Item -ItemType Directory -Force C:\RitzyInstall
The password must not expire
The startup task (§9) stores this password. If it expires or is changed without updating the task, the stacks silently stop starting at boot. Keep it in your password vault. On a domain-joined server a dedicated domain account works the same way — ask your domain admin to exempt it from password-expiry policy.

Now sign out and sign in as ritzysvc. Every remaining step is done in that session.

3 Install WSL & Ubuntu

In PowerShell (Admin), as ritzysvc:

wsl --install -d Ubuntu-24.04
  1. Reboot when asked and sign in as ritzysvc again.
  2. Ubuntu opens and asks for a Linux username and password. Use the username ritzy and remember the password — it's needed for sudo.
  3. Check the versions:
    wsl -l -v          # Ubuntu-24.04 must show VERSION 2
    wsl --version      # WSL 2.0 or newer (needed for mirrored networking)
    If the distro shows version 1: wsl --set-version Ubuntu-24.04 2. If WSL is older than 2.0: wsl --update.

4 Configure WSL

Two files, both needed. Changes to either take effect only after wsl --shutdown.

FileWhereControls
.wslconfigWindows C:\Users\ritzysvc\.wslconfigThe Linux VM: memory, CPUs, networking
/etc/wsl.confUbuntu inside the distrosystemd, default user, PATH

4a. .wslconfig

Create it from PowerShell — Notepad on its own tends to save .wslconfig.txt, which WSL ignores:

notepad "$env:USERPROFILE\.wslconfig"

Answer Yes to create the file and paste (values for a 16 GB, 8-core server):

[wsl2]
# Memory and CPUs Linux may use. Leave Windows ~4 GB and 1-2 cores.
memory=12GB
processors=6
# Disk-backed overflow when RAM runs out.
swap=4GB
# Containers appear on the server's own IP address. Needs WSL 2.0+.
networkingMode=mirrored
# Resolve DNS through Windows (avoids failures behind corporate DNS or VPNs).
dnsTunneling=true
# Windows firewall rules, including the Hyper-V rules in section 8, apply to WSL.
firewall=true

[experimental]
# Let WSL's virtual disk shrink when files are deleted.
sparseVhd=true
Server RAMmemory=processors=Fits roughly
8 GB5GBcores − 11–2 small stacks
16 GB12GBcores − 23–4 standard stacks
32 GB26GBcores − 28–10 standard stacks
The first build needs at least 6 GB
Below that the front-end build is killed for lack of memory, often with no clear error.

4b. /etc/wsl.conf

In Ubuntu (wsl -d Ubuntu-24.04): sudo nano /etc/wsl.conf, and make it contain:

[boot]
# systemd starts Docker, and cron for the backups, automatically.
systemd=true

[user]
# The Linux account WSL logs in as, including for the startup task.
default=ritzy

[interop]
# Keep the Windows PATH out of Linux.
appendWindowsPath=false

Save with Ctrl+O Enter, exit with Ctrl+X, then exit Ubuntu.

4c. Apply and check

# PowerShell: restart WSL, wait ~10 seconds, reopen Ubuntu
wsl --shutdown
wsl -d Ubuntu-24.04
In Ubuntu, runShould show
free -htotal about the memory= you set
nprocthe processors= number
ps -p 1 -o comm=systemd
ip -4 addr show | grep inetthe server's own LAN IP — proof mirrored networking is on
whoamiritzy

5 Install Docker Engine

In Ubuntu:

curl -fsSL https://get.docker.com | sudo sh
sudo systemctl enable --now docker containerd
sudo usermod -aG docker $USER

Leave Ubuntu and open it again so the group change applies, then check:

docker run --rm hello-world     # "Hello from Docker!"

6 Source code & ZATCA SDK

Keep the source inside the Linux file system (~/…), not under /mnt/c — builds through the Windows drive are several times slower.

cd ~ && git clone https://github.com/ritzysof/.-ritzy-web-.git ritzyweb
cd ~/ritzyweb && git checkout main

No GitHub access on the server? Put a source zip in C:\RitzyInstall and run unzip /mnt/c/RitzyInstall/ritzyweb-source.zip -d ~/ritzyweb.

The ZATCA SDK zip

The image bundles LibreOffice (Word-template PDFs), a Java 11 runtime and the ZATCA fatoora SDK (Saudi e-invoicing). ZATCA only offers its SDK through a captcha-protected download, so it can't be fetched during the build, and the build stops without it.

  1. Download the Java SDK zip from https://sandbox.zatca.gov.sa/downloadSDK on any PC.
  2. Copy it to C:\RitzyInstall on the server.
  3. In Ubuntu:
    cp /mnt/c/RitzyInstall/zatca-einvoicing-sdk-*.zip ~/ritzyweb/docker/vendor/

7 Build, start & first company

cd ~/ritzyweb && bash install-linux.sh          # --port 9090 to move off 8080

The installer skips what's already done, generates the secrets in docker/.env (the PostgreSQL passwords and the sign-in secret), builds the image and starts the stack. The first build takes 10–20 minutes.

cd ~/ritzyweb/docker && docker compose ps     # both services "healthy"

On the server, open http://localhost:8080. The login screen offers Set up Ritzy: enter the support password and complete the wizard to create the first company and its administrator account. See the Docker Installation Guide §10 for the wizard itself.

8 Network access & firewall

With mirrored networking the app answers on the server's IP. Two firewalls must allow the port: Windows Firewall, and the Hyper-V firewall that guards WSL. In PowerShell (Admin), per port:

New-NetFirewallRule -DisplayName "RitzyWeb Docker 8080" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow

New-NetFirewallHyperVRule -Name "RitzyWebDocker8080" -DisplayName "RitzyWeb Docker 8080" -Direction Inbound `
  -VMCreatorId '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' -Protocol TCP -LocalPorts 8080

From another PC, browse to http://<server-ip>:8080.

Never open port 5432
PostgreSQL is only reachable between the containers, and should stay that way.
Customers on the internet
Put a reverse proxy in front for HTTPS and names instead of ports — the bundle's docker/trial-proxy (Caddy) maps customer.your-domain to each stack's port and obtains certificates automatically. It needs ports 80 and 443, so they must not be taken by IIS or an nginx from a native install.

9 Start at boot, without a login

WSL normally starts only when a user opens it. A scheduled task running as ritzysvc starts Ubuntu at boot and keeps it running; Docker then starts with systemd, and every container starts on its own (restart: unless-stopped).

First, on its own, the line that asks for the password. A sign-in box opens; enter the ritzysvc password there (use DOMAIN\ritzysvc for a domain account):

$cred = Get-Credential -UserName "$env:COMPUTERNAME\ritzysvc" -Message "Password of ritzysvc"

Then create the task:

$action   = New-ScheduledTaskAction -Execute "C:\Windows\System32\wsl.exe" `
              -Argument "-d Ubuntu-24.04 --exec /bin/sleep infinity"
$trigger  = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 999 `
              -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName "RitzyWeb WSL (Docker)" -Action $action -Trigger $trigger `
  -Settings $settings -User $cred.UserName -Password $cred.GetNetworkCredential().Password -RunLevel Highest

sleep infinity does nothing but keep the Ubuntu session open, so WSL never shuts down.

Test it
Restart the server, don't sign in, wait two minutes, and open http://<server-ip>:8080 from another PC.

If the ritzysvc password ever changes, update the task straight away. The first line opens a sign-in box — enter the new password there, then run the second line:

$cred = Get-Credential -UserName "$env:COMPUTERNAME\ritzysvc" -Message "New password of ritzysvc"
Set-ScheduledTask -TaskName "RitzyWeb WSL (Docker)" -User $cred.UserName -Password $cred.GetNetworkCredential().Password

10 Backups

Use pg_dump, never a copy of WSL's disk
Copying WSL's virtual disk (ext4.vhdx under C:\Users\ritzysvc\AppData\Local\) while it runs does not give a consistent database backup.

backup-stacks.sh backs up every stack in one run: a pg_dump of the stack's database (every company in it, consistent while people work), the app's files (company backups, ZATCA certificates, licence files) and the stack's settings. It keeps 14 days locally and exits with an error if anything failed.

# Ubuntu: once
sudo apt-get install -y rsync && mkdir -p /mnt/d/RitzyBackups
~/ritzyweb/docker/backup-stacks.sh --rsync /mnt/d/RitzyBackups     # test run

# schedule it: crontab -e, then add
30 2 * * * $HOME/ritzyweb/docker/backup-stacks.sh --rsync /mnt/d/RitzyBackups >> $HOME/ritzy-backups/backup.log 2>&1

Your Windows backup software can then pick up D:\RitzyBackups. Keep a copy off the server as well, for example with --rclone to a storage box.

The backups hold secrets
Each backup includes the stack's database password and any ZATCA private key. Restrict access to the backup folder and its off-site copy.

Restore steps are in the Docker Installation Guide §13.

11 More customers on this server

Each customer gets its own stack. A customer's extra companies go inside their stack (DBA Console → Add Company), not in a new one.

cd ~/ritzyweb/docker
./new-stack.sh acme 8090 --size standard --start

Then add the two firewall rules from §8 for its port. --size sets the stack's memory and CPU ceilings, so one busy customer can't starve the others:

SizeForMemory ceiling (PostgreSQL + app)CPUs
small1 company, trials~1.4 GB1 + 1
standard2–3 companies~2.5 GB2 + 2
large3–5 companies~5.5 GB4 + 2
CPU limits can't exceed the cores WSL has
A stack asking for more CPUs than processors= in .wslconfig fails to start. Ceilings are limits, not reservations: watch docker stats under real load before adding more stacks.

12 Moving a Windows customer in

An existing web-only customer on a Windows install (companies still in Firebird .FDB files) moves into a stack in three steps: the companies are converted to PostgreSQL on the Windows side, then restored into the stack. Customers who also use the Ritzy 7 desktop app stay on their Windows installation — it needs the Firebird databases.

  1. On the customer's Windows database server, as Administrator. It needs Node.js 20+, a RitzyWeb source checkout (the importer is a development script) and a PostgreSQL 18 to convert into — a local one is simplest. For the final move, stop the Ritzy One service first so no work happens after the export:
    powershell -ExecutionPolicy Bypass -File docker\migrate\export-windows.ps1 -PgUrl postgres://postgres:<pw>@localhost:5432/postgres
                                                         # -InstallDir if not C:\RitzyOne
    It converts every company in CompLib.xml with the importer (npm run pg:convert) into a scratch database, dumps it with pg_dump and writes one zip: the dump, the conversion report, a checksum, the settings that must follow the customer (AI key, web-push keys, licensing), the licence files and the ZATCA certificates. The scratch database is dropped afterwards.
  2. Copy the zip to C:\RitzyInstall on this server. It contains every company's data and private keys — move it only over trusted channels.
  3. In Ubuntu:
    cd ~/ritzyweb/docker
    ./new-stack.sh acme 8090 --start                      # if the stack is new
    ./migrate/import-to-stack.sh /mnt/c/RitzyInstall/ritzy-migration-<date>.zip ~/ritzy-acme
    It verifies the checksum, restores the dump into the stack's database, installs the certificates, licence and settings, and restarts the app. It won't replace a stack that already has companies unless you add --force (the old database is kept as ritzy_pre_import_<date>); companies are not merged.

Afterwards: users sign in again (the stack has its own sign-in secret); read the conversion report the import leaves in the stack folder (pg-validation-<date>.md); check that every company opens, that Saudi companies show their ZATCA certificate, and that the licence is valid; run a backup; stop the old Windows service; delete the zip.

13 Day-to-day & updates

Managing the server

Updating to a new version

cd ~/ritzyweb && git pull
cd docker && docker compose -f docker-compose.yml -f docker-compose.build.yml build

# then each stack, when you are ready to move it
cd ~/ritzy-acme && docker compose up -d

Stacks share the image, so the build happens once; each stack moves only when you restart it.

14 Troubleshooting

SymptomCheck
wsl --install fails, or “virtualization not enabled”Enable VT-x/AMD-V in the BIOS. Then in PowerShell (Admin): Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -All and reboot.
wsl -l shows no UbuntuYou're signed in as a different user. Sign in as ritzysvc.
Memory or CPUs unchanged (free -h, nproc).wslconfig isn't in C:\Users\ritzysvc\, or it's named .wslconfig.txt. Fix it, then wsl --shutdown.
Ubuntu shows only a 172.x.x.x address; other PCs can't connectMirrored networking isn't active: wsl --update, check networkingMode=mirrored, wsl --shutdown. On a domain, a Group Policy can block custom networking modes — ask your domain admin.
Works on the server, not from other PCsBoth firewall rules in §8, for that port.
Port already in useA Windows program holds it (IIS or a native install's nginx?). Choose another port.
Site down after a rebootTask Scheduler → RitzyWeb WSL (Docker) → Last Run Result. Was the ritzysvc password changed (§9)?
Build stops: “put the ZATCA fatoora SDK zip in docker/vendor/”§6 — copy the Java SDK zip into ~/ritzyweb/docker/vendor/.
Build dies with no clear errorOut of memory. Raise memory= to at least 6 GB, then wsl --shutdown.
A stack won't start: “range of CPUs”Its CPU limit is above processors=. Lower PG_CPUS / APP_CPUS in the stack's .env, or use --size small.
A container is unhealthydocker compose logs --tail 100 app (or postgres) in that stack's folder.
docker: permission deniedLeave and reopen Ubuntu after the usermod in §5.
More detail
The general container guide is the Docker Installation Guide. In the project, docker/INSTALL-WINDOWS-SERVER.md holds this procedure as plain text, and docker/README.md the operational reference.