The committed Compose stack is the production deployment path. Aspire remains the only supported local full-stack orchestrator.
Internet HTTP/HTTPS
-> caddy:80/443 (automatic HTTPS)
-> bff:3000 (SvelteKit HTTP)
-> backend:8080 (UserIdentity)
-> backend:8081 (UserProfile)
backend container
-> UserIdentity + UserProfile + Notifications over host-local IPC
-> mongodb:27017
Only Caddy publishes host ports. The BFF, backend HTTP, IPC, and MongoDB stay on private Compose networks. Caddy certificates, MongoDB data, and profile pictures use named volumes.
Install a container engine with Compose support, OpenSSL, and curl:
- Docker path: Docker Engine with the Compose plugin.
- Podman path: Podman with the Compose provider/plugin, plus systemd (required for the optional host unit below).
Clone the repository, then create an A record (and AAAA when IPv6 is configured) pointing the public hostname to the VPS. Use direct DNS, including Cloudflare's DNS-only mode. The hostname must resolve publicly; Caddy uses inbound TCP 80 or 443 for certificate validation.
At the VPS provider firewall and any container-aware host firewall:
- Allow SSH only from the administration network required by your access policy.
- Allow inbound TCP
80and443from the internet. - Do not allow
3000,8080,8081, or27017.
Docker-published ports can bypass ordinary UFW INPUT rules. Prefer the provider firewall, or enforce equivalent host rules through Docker's DOCKER-USER chain or the platform's Docker-aware nftables tooling. For rootful Podman, apply the same public-port policy at the provider firewall and any host rules that cover published ports.
From the repository root, pass the hostname without https://, a path, or a port:
scripts/deploy-init.sh helpdesk.example.comThe script creates .env with mode 600, a random hexadecimal MongoDB password, and a new matching 3072-bit RSA JWT pair. It refuses to overwrite an existing .env. The generated credentials are production secrets; back up .env securely and never commit it.
By default email delivery is disabled. To send real email, edit .env, set SMTP_ENABLED=true, and fill in the SMTP values. Port 465 normally uses implicit TLS; port 587 normally uses STARTTLS. This application uses SMTP_USE_SSL=true for both secure modes.
For manual setup instead, copy .env.example to .env, run chmod 600 .env, and replace every placeholder. Keep MongoDB username/password values to URI-unreserved characters (A-Z, a-z, 0-9, ., _, ~, and -) because Compose places the same raw values in both MongoDB initialization and the derived connection URI. Avoid $, which also has special meaning in Compose environment files. deploy-init.sh avoids these ambiguities by generating hexadecimal credentials.
scripts/deploy.shThe script:
- selects Docker Compose, falling back to Podman Compose when Docker is unavailable;
- validates Compose interpolation;
- builds and starts the stack;
- prints service status;
- verifies
https://<DOMAIN>/, retrying while Caddy obtains its certificate.
If the smoke test fails, the script prints recent Caddy and BFF logs. Common causes are incorrect DNS, blocked ports 80/443, or a malformed DOMAIN value.
Docker Compose is the production default. The Podman fallback is suitable when the host is configured to bind ports below 1024. On SELinux-enforcing hosts, Compose applies a private relabel to the Caddyfile mount; ensure the repository filesystem supports relabeling.
scripts/deploy.sh is a one-shot build/start. Compose restart: unless-stopped restarts crashed containers while Podman can supervise them, but host reboot is separate: Podman is daemonless, so something must run compose up -d again after boot.
After a successful deploy on a Podman host:
scripts/install-host-service.shThe script:
- requires Podman Compose, systemd, and a mode-
600.env; - renders
deploy/helpdesk.service.inwith the absolute repository path andpodmanbinary; - installs
helpdesk.serviceas a system unit when run as root, or as a user unit when rootless; - enables linger for the installing user when rootless (so the unit starts at boot without an interactive login);
- runs
systemctl enable --nowfor that unit.
The unit is Type=oneshot with RemainAfterExit=yes:
ExecStart→podman compose --env-file .env up -d(no rebuild on every boot)ExecStop→podman compose --env-file .env stop(containers stop; named volumes stay)
Release updates remain:
git pull --ff-only
scripts/deploy.shRe-run scripts/install-host-service.sh after moving the clone path or changing the unit template. Remove with scripts/install-host-service.sh --remove.
| Mode | Unit location | Notes |
|---|---|---|
Rootful (sudo scripts/install-host-service.sh) |
/etc/systemd/system/helpdesk.service |
Preferred for binding host ports 80/443 |
| Rootless | ~/.config/systemd/user/helpdesk.service |
Needs linger; also needs net.ipv4.ip_unprivileged_port_start ≤ 80 (or equivalent) so Caddy can bind 80/443 |
Status and logs:
# rootful
systemctl status helpdesk
journalctl -u helpdesk -e
# rootless
systemctl --user status helpdesk
journalctl --user -u helpdesk -eThis unit is Podman-only. Docker hosts should enable docker.service at boot; with Docker, containers left running under restart: unless-stopped typically return when the daemon starts, without this unit.
Deploy a new revision:
git pull --ff-only
scripts/deploy.shUseful commands:
docker compose logs -f caddy bff backend mongodb
docker compose restart caddy bff backend
docker compose downUse podman compose in place of docker compose when operating with Podman.
docker compose down preserves named volumes. Do not use docker compose down --volumes unless intentionally deleting MongoDB data, profile pictures, and Caddy's certificate state. Back up mongodb_data and profile_pictures before destructive maintenance or migration.
MongoDB initialization credentials apply only when its data volume is first created. Do not change the MongoDB username/password in .env after initialization without performing the corresponding database credential rotation.
The host unit's ExecStop uses compose stop, not down, so a systemctl stop helpdesk leaves container definitions available for the next up -d. Prefer scripts/deploy.sh for rebuilds; use compose down only for intentional full teardown.
The simple path uses direct DNS. If a CDN proxy is later enabled, use strict origin TLS, restrict origin access to the provider's current source ranges, and verify that Caddy certificate renewal works through the proxy.