# Zimphony Webmail Clean VM Installation Guide

This guide is for installing a packaged Zimphony Webmail Docker release on a
new virtual machine. It is written for customer or partner deployments.

Use a VM, not an LXC container, for production. Docker inside LXC depends on
host-level kernel, cgroup, namespace, AppArmor, and sysctl permissions. Some LXC
hosts block Docker container startup with errors such as
`net.ipv4.ip_unprivileged_port_start: permission denied`. A VM gives Docker the
expected kernel isolation and is the supported deployment target.

## 1. Supported Editions

Zimphony Webmail release zips are edition locked. Install only the package that
matches the purchased or selected edition.

- Free Webmail: mail, contacts, Admin, 2FA, migration/import/export, no license server check
- Shared Webmail: mail and contacts with sharing, paid deployment license, unlimited active mailboxes from Zimphony side
- Native-Zimbra / Complete Workspace: Mail and Contacts plus Native Calendar, Tasks, and Briefcase, paid 50 / 100 / 250 / 500 / 750 mailbox capacity ladder
- Google Connect / Complete Workspace: Mail and Contacts plus Google Calendar, Tasks, and Drive, paid 50 / 100 / 250 / 500 / 750 mailbox capacity ladder
- 1000+ mailbox deployments are Contact Sales only and do not have a fixed public artifact
- Demo packages are internal only

Paid packages require both:

```env
ZIMPHONY_LICENSE_KEY=<license-key>
LICENSE_CHECK_SIGNING_SECRET=<license-check-secret>
```

The license key identifies the entitlement. The signing secret proves that the
installation is allowed to check that license. Do not use or distribute the
license server's internal `LICENSE_SIGNING_SECRET`.

## 2. System Requirements

Minimum VM:

- 2 vCPU
- 4 GB RAM
- 40 GB disk
- 64-bit Linux
- Docker Engine 24 or newer
- Docker Compose plugin 2.x or newer

Recommended VM:

- 4 vCPU
- 8 GB RAM
- 80 GB SSD disk
- Ubuntu Server 22.04 LTS or 24.04 LTS
- Rocky Linux 9 or compatible RHEL 9 clone

Supported hosting targets:

- VMware, Proxmox VM, Hyper-V VM, KVM, cloud VM, or bare-metal Linux
- Docker Desktop on Windows for testing or small internal deployments

Not supported for production:

- LXC containers
- OpenVZ containers
- very old Linux distributions that cannot run a current Docker Engine
- hosts where Docker cannot manage iptables/nftables, cgroups, and namespaces

Network requirements:

- Public HTTPS hostname for webmail, for example `mail.example.com`
- DNS A/AAAA record pointing to the VM or reverse proxy
- Outbound access from the VM to the mail server SOAP endpoint
- Outbound access from paid editions to `https://license.zimphony.com`
- Inbound HTTP/HTTPS to the reverse proxy

Ports:

- Public: `80/tcp` and `443/tcp` on the reverse proxy
- Local Docker host: frontend on `3000`, backend on `8000`
- Do not expose backend port `8000` publicly unless a support engineer asks for
  a temporary diagnostic path.

Zimbra requirements:

- User SOAP URL, for example `https://mail.example.com/service/soap`
- Admin SOAP URL, for example `https://mail.example.com:7071/service/admin/soap`
- Admin account with permission to list accounts and read provisioning data
- Network path from the Zimphony VM to both SOAP endpoints

## 3. Prepare a New Ubuntu VM

Update the OS:

```bash
sudo apt-get update
sudo apt-get upgrade -y
sudo reboot
```

Install basic tools:

```bash
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg unzip nano
```

Install Docker Engine and Compose plugin:

```bash
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

Confirm:

```bash
docker --version
docker compose version
```

Optional: allow the current user to run Docker without `sudo`:

```bash
sudo usermod -aG docker "$USER"
newgrp docker
```

## 4. Prepare a New Rocky Linux 9 VM

Update the OS:

```bash
sudo dnf update -y
sudo reboot
```

Install basic tools:

```bash
sudo dnf install -y dnf-utils curl unzip nano
```

Install Docker Engine and Compose plugin:

```bash
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
```

Confirm:

```bash
docker --version
docker compose version
```

Optional: allow the current user to run Docker without `sudo`:

```bash
sudo usermod -aG docker "$USER"
newgrp docker
```

If SELinux is enforcing and a reverse proxy cannot reach Docker, check the
reverse proxy logs and enable only the required network policy. Do not disable
SELinux unless the server owner accepts that operational tradeoff.

## 5. Install Zimphony Webmail

Create an installation directory:

```bash
sudo mkdir -p /opt/zimphony
sudo chown "$USER":"$USER" /opt/zimphony
cd /opt/zimphony
```

Upload the release zip to the VM, then extract it:

```bash
unzip /path/to/zimphony-webmail-<edition>-YYYYMMDD-HHMMSS.zip
cd zimphony-webmail-<edition>-YYYYMMDD-HHMMSS
chmod +x install.sh deploy/docker/install.sh
```

Run the installer:

```bash
./install.sh
```

The installer writes:

```text
deploy/docker/<edition>/backend.env
deploy/docker/<edition>/frontend.env
```

These env files contain secrets and are written with `600` permissions.

## 6. Non-interactive Install

Free edition example:

```bash
NONINTERACTIVE=1 \
WEBMAIL_HOST=mail.example.com \
ZIMBRA_SOAP_URL=https://mail.example.com/service/soap \
ZIMBRA_ADMIN_SOAP_URL=https://mail.example.com:7071/service/admin/soap \
ZIMBRA_ADMIN_EMAIL=admin@example.com \
ZIMBRA_ADMIN_PASSWORD='<zimbra-admin-password>' \
BRAND_NAME='Zimphony Webmail' \
BRAND_TAGLINE='Business email that grows with you' \
ADMIN_HELP_TEXT='Help : support@example.com' \
./install.sh
```

Paid edition example:

```bash
NONINTERACTIVE=1 \
WEBMAIL_HOST=mail.example.com \
ZIMBRA_SOAP_URL=https://mail.example.com/service/soap \
ZIMBRA_ADMIN_SOAP_URL=https://mail.example.com:7071/service/admin/soap \
ZIMBRA_ADMIN_EMAIL=admin@example.com \
ZIMBRA_ADMIN_PASSWORD='<zimbra-admin-password>' \
LICENSE_SERVER_URL=https://license.zimphony.com \
ZIMPHONY_LICENSE_KEY='<license-key>' \
LICENSE_CHECK_SIGNING_SECRET='<license-check-secret>' \
BRAND_NAME='Zimphony Webmail' \
BRAND_TAGLINE='Business email that grows with you' \
ADMIN_HELP_TEXT='Help : support@example.com' \
./install.sh
```

Optional branding values:

```bash
BRAND_LOGO_URL=https://example.com/logo.png
BRAND_FAVICON_URL=https://example.com/favicon.png
```

## 7. Configure HTTPS Reverse Proxy

The Docker frontend listens on host port `3000`. Put Nginx, Caddy, Traefik,
Cloudflare Tunnel, or a load balancer in front of it.

Nginx example:

```nginx
server {
    listen 80;
    server_name mail.example.com;

    client_max_body_size 80m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
```

Enable TLS with Certbot on Ubuntu:

```bash
sudo apt-get install -y nginx certbot python3-certbot-nginx
sudo certbot --nginx -d mail.example.com
```

Enable TLS with Certbot on Rocky Linux 9:

```bash
sudo dnf install -y nginx certbot python3-certbot-nginx
sudo systemctl enable --now nginx
sudo certbot --nginx -d mail.example.com
```

## 8. Post-install Validation

Check containers:

```bash
docker compose -f deploy/docker/*/docker-compose.yml ps
```

Check logs:

```bash
docker compose -f deploy/docker/*/docker-compose.yml logs --tail=100
```

Check local HTTP:

```bash
curl -I http://127.0.0.1:3000/
curl http://127.0.0.1:3000/api/config
```

Check public HTTPS:

```bash
curl -I https://mail.example.com/
curl https://mail.example.com/api/config
```

Validate in the browser:

- Open `https://mail.example.com/`
- Log in with a real mailbox
- Open `/admin`
- Confirm Product Information shows the expected edition and tier
- Confirm branding appears on login, webmail, and admin pages
- Send a test email
- Send a test attachment
- Open the same site on a mobile viewport

Paid license validation:

- Confirm license status is healthy in `/admin`
- Confirm `license.zimphony.com` shows the deployment
- Confirm active mailbox count is updated
- Temporarily stop outbound access to the license server and confirm existing
  usage is not blocked after a successful activation

## 9. Backup Policy

Back up before every upgrade and at least once per day for production systems.

Back up:

- Docker volume containing backend runtime data
- `deploy/docker/<edition>/backend.env`
- `deploy/docker/<edition>/frontend.env`
- reverse proxy configuration
- TLS certificate configuration if not managed externally

The runtime Docker volume contains:

- SQLite platform database
- branding uploads
- license activation/cache state
- local runtime metadata

Find the volume name:

```bash
docker volume ls | grep backend-data
```

Backup example:

```bash
mkdir -p backups
docker compose -f deploy/docker/*/docker-compose.yml stop backend
docker run --rm \
  -v "$(basename "$PWD" | tr '[:upper:]' '[:lower:]')_backend-data":/data \
  -v "$PWD/backups":/backup \
  alpine tar czf /backup/zimphony-backend-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /data .
docker compose -f deploy/docker/*/docker-compose.yml start backend
cp deploy/docker/*/backend.env backups/backend.env.$(date +%Y%m%d-%H%M%S)
cp deploy/docker/*/frontend.env backups/frontend.env.$(date +%Y%m%d-%H%M%S)
```

If the computed volume name does not exist, use the exact name from
`docker volume ls`.

Restore example:

```bash
docker compose -f deploy/docker/*/docker-compose.yml down
docker run --rm \
  -v <volume-name>:/data \
  -v "$PWD/backups":/backup \
  alpine sh -c 'cd /data && tar xzf /backup/<backup-file>.tar.gz'
docker compose -f deploy/docker/*/docker-compose.yml up -d
```

Keep backup archives encrypted if they include customer branding, license
state, or any local data that should not be public.

## 10. Update Policy

Use the same edition when updating. Do not update a Free installation with a
paid package or a paid installation with a Free package.

Recommended update steps:

1. Copy the new release zip to `/opt/zimphony-webmail`.
2. Extract it into a new versioned directory under `/opt/zimphony-webmail`.
3. Run the update script from that new directory:

```bash
cd /opt/zimphony-webmail/<new-release-directory>
sudo ./update.sh /opt/zimphony-webmail/<new-release>.zip
```

4. Run the built-in verification:

```bash
sudo ./verify-update.sh
```

The scripts detect and validate the Edition automatically. They preserve the
existing Compose project, `backend-data` volume, and generated env files. The
new versioned directory becomes the active Docker working directory; the old
directory and timestamped backup remain available for rollback and support.

Rollback:

```bash
cd /opt/zimphony/old-release
docker compose -f deploy/docker/<edition>/docker-compose.yml up -d
```

If the update changed database format, restore the backup before rolling back.

## 11. Support Policy

Free edition:

- Community or best-effort support only
- No license server check
- No guaranteed response time
- Upgrade to a paid edition requires installing the paid package and configuring
  a valid license

Paid editions:

- Support and Upgrade Plan controls access to supported updates and official
  support
- The license is perpetual; mail usage is not cut off after successful
  activation
- License warnings may appear for over-deployment, over-quota, inactive support
  plan, invalid license state, or failed signed checks
- Administrators should resolve warnings promptly to avoid user-facing notices

What support needs from the customer:

- Edition and version from `/admin`
- Deployment hostname
- Docker container status
- Recent backend and frontend logs
- License key, but not unrelated passwords
- Description of Zimbra version and OS

Do not send:

- Zimbra admin password
- Mailbox passwords
- `LICENSE_CHECK_SIGNING_SECRET`
- private TLS keys
- license-server `LICENSE_SIGNING_SECRET`

Useful support commands:

```bash
docker compose -f deploy/docker/*/docker-compose.yml ps
docker compose -f deploy/docker/*/docker-compose.yml logs --tail=200 backend
docker compose -f deploy/docker/*/docker-compose.yml logs --tail=200 frontend
curl http://127.0.0.1:3000/api/config
```

## 12. Troubleshooting

Container restart loop:

```bash
docker compose -f deploy/docker/*/docker-compose.yml logs --tail=200 backend
docker compose -f deploy/docker/*/docker-compose.yml logs --tail=200 frontend
```

License check fails:

- Confirm `ZIMPHONY_LICENSE_KEY`
- Confirm `LICENSE_CHECK_SIGNING_SECRET`
- Confirm VM can reach `https://license.zimphony.com`
- Confirm server time is correct; signed checks allow only a short timestamp
  skew

Cannot reach Zimbra admin SOAP:

- Test from the VM:

```bash
curl -k -I https://mail.example.com:7071/service/admin/soap
```

- Prefer an internal IP or internal DNS name when the public hostname hairpins
  slowly or is blocked by firewall policy.

Branding does not change:

- Change branding from `/admin` for the same domain used by the browser
- Refresh the browser
- Confirm backend volume is attached

Uploads fail:

- Increase reverse proxy upload limit
- For Nginx, set `client_max_body_size 80m;`
