#Installing the server
This guide takes you from an empty Linux server to a running, hardened SignetMail on its own HTTPS address. Plan about one hour. The reference environment is a small VPS with Rocky Linux 10; commands for other distributions differ only in the package manager.
Warning. SignetMail inserts text into every e-mail your company sends. Treat the server as a security-critical system: keep it patched, restrict who can reach the portal and protect the administrator accounts with MFA. The steps below include the hardening; do not skip them.
#Overview
- Create the server and a cloud firewall.
- Point a DNS name at the server.
- Harden the operating system (
harden.sh). - Configure SignetMail (
.env, certificate for Microsoft Graph). - Start the containers.
- Register the application in Entra ID (next chapter) and verify.
#1. Create the server
Create a VPS with a recent Rocky Linux (or another Linux with Docker support). 2 vCPU and 4 GB RAM are sufficient.
Add your SSH public key when creating the server. Do not use password login for
root.Create a cloud firewall in your provider's console in front of the server and allow only:
- TCP 22 from your own IP address only,
- TCP 80 and 443 from anywhere.
This is a layer before the server; the server has its own firewall as well.
#2. DNS
Create an A record (and an AAAA record if the server has IPv6) for the public name, for example app.example.com, pointing to the server's IP address. Wait until it resolves:
nslookup app.example.com
Tip. Caddy requests the Let's Encrypt certificate the first time the container starts. If DNS is not yet correct at that moment, the request fails and is retried later; fixing DNS and running
sudo docker compose restart caddyis enough.
#3. Harden the operating system
Log in as root once and fetch the deployment files:
dnf -y install git
git clone https://github.com/AljazEferl/signetmail.git /opt/signetmail
cd /opt/signetmail/deploy/cloud
sudo ./harden.sh <admin-user> "<your SSH public key>"
harden.sh is idempotent and does the following:
| Area | What it does |
|---|---|
| Admin user | Creates <admin-user> with sudo, installs your SSH key. |
| SSH | Key-only login, root login disabled, password authentication disabled — only after you confirm in a second terminal that key login works. |
| Firewall | firewalld with 22 (rate-limited), 80 and 443 only. |
| Brute force | fail2ban for SSH. |
| Updates | Automatic security updates (dnf-automatic). |
| Kernel | sysctl hardening; SELinux stays in enforcing mode. |
| Docker | Installs Docker Engine and the Compose plugin. |
The script pauses and asks you to open a second terminal and sign in with the key. Answer da (yes) only when that works; otherwise you could lock yourself out.
After it finishes, work as the admin user (not root) from now on. The files live in /opt/signetmail/cloud (the folder that contains docker-compose.yml).
#4. Configure SignetMail
cd /opt/signetmail/cloud
cp .env.example .env
nano .env
Fill in these values:
| Variable | Meaning |
|---|---|
SIGNETMAIL_VERSION |
Image tag to run. latest follows the main branch; pin a sha-… tag for controlled updates. |
SIGNETMAIL_HOST |
Public name, for example app.example.com. Must match the DNS record. |
ENTRA_TENANT_ID |
Directory (tenant) ID from the Entra app registration. |
ENTRA_CLIENT_ID |
Application (client) ID of the SignetMail app registration. |
PORTAL_OWNER |
UPN of the first Owner of the portal, for example jane.doe@example.com. Further people are added in the portal under Access. |
GRAPH_CERT_PASSWORD |
Password for the Graph certificate; any long random string. |
PORTAL_ALLOWED_IPS |
Optional. Space-separated IPs/CIDRs allowed to open the portal, for example 203.0.113.10 198.51.100.0/24. Empty = everyone. |
BACKUP_GIT_REMOTE |
Optional. SSH URL of a private repository for encrypted backups. |
You will not know ENTRA_TENANT_ID and ENTRA_CLIENT_ID until you complete the Entra registration; it is fine to create the app registration first and come back here.
Generate the certificate SignetMail uses to authenticate to Microsoft Graph:
./setup.sh
This creates data/ and certs/graph.pfx (private key, password protected, readable only by the container user) and certs/graph.cer (public part, valid 730 days). You will upload graph.cer to Entra in the next chapter. Note the expiry date — renewal is described in Operations.
#5. Pull and start
The container image is private. Log in to the GitHub Container Registry once with a personal access token that has only the read:packages scope:
sudo docker login ghcr.io -u <github-user>
sudo docker compose pull
sudo docker compose up -d
Afterwards log out and delete the token if you do not need it for updates:
sudo docker logout ghcr.io
Check that everything runs:
sudo docker compose ps
sudo docker compose logs --tail 50 signetmail-api
curl https://<host>/health
/health returns a small JSON document such as {"status":"ok","lastSyncUtc":"2026-01-01T08:00:00Z"}. A recent lastSyncUtc proves the server has read your directory from Entra ID. It deliberately reveals nothing else.
#What the stack contains
docker-compose.yml defines two services:
- caddy — reverse proxy on ports 80/443. It sets HSTS, hides the
Serverheader, limits request bodies to 2 MB, blocks/devpaths, optionally restricts the portal toPORTAL_ALLOWED_IPS, and keeps Let's Encrypt certificates in thecaddy_datavolume. - signetmail-api — the application. Not published on the host; only Caddy can reach it. It runs as an unprivileged user (uid 1654) with all Linux capabilities dropped,
no-new-privileges, memory and process limits, and rotated logs. Yourdata/folder (database, uploaded logos) andcerts/folder (read-only) are mounted into it.