Installing on a server
This page is meant to be followed end to end, with nothing left to guess. It installs Kewos ERP on a Linux server, as a permanent service, behind your own reverse proxy.
What the server needs
| Operating system | Debian 12 / 13, Ubuntu 22.04 / 24.04, or equivalent, 64-bit. |
| CPU | Four cores to begin with. |
| Memory | 8 GB. Plan for 16 GB beyond twenty concurrent users. |
| Disk | 20 GB for the product, plus room for your attachments and your backups. |
| Database | PostgreSQL 16 or 17. |
| Queue | Redis 6 or later. |
| Network | A domain name and a proxy terminating TLS. |
System packages
sudo apt updatesudo apt install -y postgresql redis-server ca-certificates curlRendering PDF documents relies on a few native libraries:
sudo apt install -y libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libgdk-pixbuf-2.0-0 \ libffi8 shared-mime-info fonts-dejavu-coreWithout them the application starts and printing a document fails at the moment somebody needs it. Install them now.
1. The service account and the layout
The application never runs as root.
sudo useradd --system --home-dir /opt/kewos --shell /usr/sbin/nologin kewos
sudo mkdir -p /opt/kewos /etc/kewos /var/lib/kewos/storage /var/log/kewossudo chown -R kewos:kewos /opt/kewos /var/lib/kewos /var/log/kewossudo chmod 750 /etc/kewos| Path | Contents |
|---|---|
/opt/kewos/current | The release in service (a symlink into /opt/kewos/releases/). |
/opt/kewos/releases/ | Installed releases. Keep two or three so you can go back. |
/etc/kewos/kewos.env | The configuration. Holds secrets: root:kewos, mode 640. |
/etc/kewos/license.token | Your licence token. |
/var/lib/kewos/storage | Attachments and generated documents. Back this up. |
/var/log/kewos | Logs. |
2. The database
The role needs CREATEDB: the application creates its own database and, in advanced
multi-company setups, those of the extra entities.
sudo -u postgres psql <<'SQL'CREATE ROLE kewos LOGIN PASSWORD 'a-long-random-password' CREATEDB;CREATE DATABASE kewos OWNER kewos;SQLCheck the connection before going further:
PGPASSWORD='a-long-random-password' psql -h 127.0.0.1 -U kewos -d kewos -c 'select 1'3. Fetch your release
cd /tmpcurl -u '<your-identifier>' -O https://download.kewos.io/erp/kewos-erp-<version>.tar.gzcurl -u '<your-identifier>' -O https://download.kewos.io/erp/kewos-erp-<version>.tar.gz.sha256Check the digest before deploying. A package that does not match its own digest is not a Kewos package.
sha256sum -c kewos-erp-<version>.tar.gz.sha256Then install it into its own release directory:
sudo mkdir -p /opt/kewos/releases/<version>sudo tar -xzf kewos-erp-<version>.tar.gz -C /opt/kewos/releases/<version> --strip-components=1sudo chown -R kewos:kewos /opt/kewos/releases/<version>sudo ln -sfn /opt/kewos/releases/<version> /opt/kewos/currentThe symlink is not decoration: it is what makes upgrading and rolling back immediate, and it never leaves the application half-copied.
4. Configuration
Create /etc/kewos/kewos.env. It is the only file you have to write.
sudo install -o root -g kewos -m 640 /dev/null /etc/kewos/kewos.envsudo editor /etc/kewos/kewos.envGenerating the secrets
Three values must be random and yours. Generate them once:
echo "KEWOS_SECRETS_MASTER_KEY=$(openssl rand -base64 32)"echo "KEWOS_ATTACHMENT_SIGNING_SECRET=$(openssl rand -hex 32)"echo "KEWOS_PORTAL_SIGNING_SECRET=$(openssl rand -hex 32)"The file, in full
# --- Required ------------------------------------------------------------KEWOS_ENV=productionKEWOS_MASTER_DATABASE_URL=postgresql+psycopg://kewos:PASSWORD@127.0.0.1:5432/kewosKEWOS_REDIS_URL=redis://127.0.0.1:6379/0KEWOS_SECRETS_MASTER_KEY=...KEWOS_LICENSE_PUBLIC_KEY=... # issued by Kewos with your licenceKEWOS_PUBLIC_BASE_URL=https://erp.your-domain.com
# --- Paths ---------------------------------------------------------------KEWOS_MODULES_ROOT=/opt/kewos/current/modulesKEWOS_STORAGE_ROOT=/var/lib/kewos/storageKEWOS_LOG_DIR=/var/log/kewos
# --- Network -------------------------------------------------------------KEWOS_HOST=127.0.0.1KEWOS_PORT=8080
# --- Link signing --------------------------------------------------------KEWOS_ATTACHMENT_SIGNING_SECRET=...KEWOS_PORTAL_SIGNING_SECRET=...
# --- First administrator (read on first boot only) -----------------------KEWOS_ADMIN_LOGIN=adminKEWOS_ADMIN_EMAIL=admin@your-domain.comKEWOS_ADMIN_PASSWORD=...
# --- Recommended ---------------------------------------------------------KEWOS_LOG_LEVEL=INFOKEWOS_MAIL_FROM=no-reply@your-domain.comKEWOS_RATE_LIMIT_PER_MINUTE=100KEWOS_HTTP_TIMEOUT_SECONDS=20KEWOS_DB_TIMEOUT_SECONDS=30KEWOS_JOB_TIMEOUT_SECONDS=900What each key does
| Key | Role |
|---|---|
KEWOS_ENV | production on a server. Any other value enables development conveniences, including hot schema application. |
KEWOS_MASTER_DATABASE_URL | The database address. The postgresql+psycopg:// prefix is required. |
KEWOS_REDIS_URL | The job queue and the cache. Without it no background work runs. |
KEWOS_SECRETS_MASTER_KEY | Encrypts stored secrets. Irreplaceable. |
KEWOS_LICENSE_PUBLIC_KEY | The key your licence token is verified against. Without it the application refuses to start in production. |
KEWOS_PUBLIC_BASE_URL | The public address. Used to build absolute links in emails and PDFs; without it images do not appear in sent documents. |
KEWOS_MODULES_ROOT | Where the modules live. Point at the current symlink, not a fixed release. |
KEWOS_STORAGE_ROOT | Where attachments live. Back this up with the database. |
KEWOS_LOG_DIR | Enables the rotating file log. Without it everything goes to standard output. |
KEWOS_HOST / KEWOS_PORT | What it listens on. Stay on 127.0.0.1: the proxy does the exposing. |
KEWOS_ATTACHMENT_SIGNING_SECRET | Signs attachment links. Changing it invalidates links already sent. |
KEWOS_PORTAL_SIGNING_SECRET | Signs external portal access. Same caveat. |
KEWOS_ADMIN_PASSWORD | The first administrator’s password, applied on first boot. |
KEWOS_MAIL_FROM | Default sender for outbound mail. |
KEWOS_RATE_LIMIT_PER_MINUTE | Request budget per actor per minute. |
KEWOS_LOG_LEVEL | INFO in service, DEBUG only while diagnosing. |
5. First boot
This registers the installation, creates the schema and the administrator. It is idempotent: running it again does no harm.
cd /opt/kewos/currentsudo -u kewos env $(grep -v '^#' /etc/kewos/kewos.env | xargs) \ ./bin/kewos-server provisionThen align the schema. A fresh installation reports drift until this is done, because the tables are created in one go without the migrations being stamped. The command is additive: it creates what is missing and never destroys.
sudo -u kewos env $(grep -v '^#' /etc/kewos/kewos.env | xargs) \ ./bin/kewos reconcile --confirm --modules-root /opt/kewos/current/modulesmain [active]: reconciled (37 migration(s), 0 column(s))done: 1 tenant(s), 0 failed6. Apply your licence
The application serves nothing until this is done:
refusing to start: tenant(s) without a valid license: main=missingsudo install -o root -g kewos -m 640 your-licence.token /etc/kewos/license.token
cd /opt/kewos/currentsudo -u kewos env $(grep -v '^#' /etc/kewos/kewos.env | xargs) \ ./bin/kewos license install \ --tenant main \ --database-url "$KEWOS_MASTER_DATABASE_URL" \ --token-file /etc/kewos/license.tokenCheck it:
status: validtenant: mainplan: on-premusers: 1 / 25companies: 1 / 3expires: 2027-09-03T11:28:38+00:00If your licence is machine-bound
sudo -u kewos ./bin/kewos license fingerprintSend the value to Kewos, which issues a token bound to this server.
7. The services
Two services: the application, and the worker that drains the queue. Both are required. Without the worker, emails, imports and documents wait with no error message.
/etc/systemd/system/kewos.service:
[Unit]Description=Kewos ERPAfter=network-online.target postgresql.service redis-server.serviceWants=postgresql.service redis-server.service
[Service]Type=simpleUser=kewosGroup=kewosWorkingDirectory=/opt/kewos/currentEnvironmentFile=/etc/kewos/kewos.envExecStart=/opt/kewos/current/bin/kewos-serverRestart=on-failureRestartSec=5
NoNewPrivileges=truePrivateTmp=trueProtectSystem=strictProtectHome=trueReadWritePaths=/var/lib/kewos /var/log/kewosLimitNOFILE=65535
[Install]WantedBy=multi-user.target/etc/systemd/system/kewos-worker.service is the same unit with
ExecStart=/opt/kewos/current/bin/kewos worker --threads 8 and no LimitNOFILE.
sudo systemctl daemon-reloadsudo systemctl enable --now kewos kewos-workersudo systemctl status kewos kewos-worker --no-pagerStartup takes about ten seconds. A check run too early fails while everything is fine.
8. The proxy
Caddy
erp.your-domain.com { encode zstd gzip request_body { max_size 60MB } reverse_proxy 127.0.0.1:8080}nginx
server { listen 443 ssl http2; server_name erp.your-domain.com;
ssl_certificate /etc/letsencrypt/live/erp.your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/erp.your-domain.com/privkey.pem;
# Attachments and imports can be large: too low a value here returns 413 # before the application ever sees the request. client_max_body_size 60m;
location / { proxy_pass http://127.0.0.1:8080; 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_read_timeout 120s; }}9. Check it
curl -fsS https://erp.your-domain.com/health/live# {"status":"alive"}
curl -fsS https://erp.your-domain.com/health/schema# {"status":"ok","tenants_checked":1,"drifted":{}}
curl -o /dev/null -w '%{http_code}\n' https://erp.your-domain.com/api/v1/about# 401A 401 on the API is a good sign: it is closed.
| What you see | What it means | What to do |
|---|---|---|
| The login screen | All good. | Go live. |
| The service does not start | Missing licence, missing public key, or unreachable database. | journalctl -u kewos -n 50 --no-pager |
| A blank page, API responding | The package is incomplete. | Download again and check the digest. |
/health/schema says drifted | The schema lags the code. | Run reconcile --confirm, then restart. |
| Emails do not go out | The worker is not running. | sudo systemctl status kewos-worker |
10. Upgrading, and going back
An upgrade means installing a release beside the current one and moving the symlink.
# 1. Back up FIRST.sudo -u postgres pg_dump -Fc kewos > /var/backups/kewos-$(date +%F).dump
# 2. Install the new release next to the old one.sudo mkdir -p /opt/kewos/releases/<new>sudo tar -xzf kewos-erp-<new>.tar.gz -C /opt/kewos/releases/<new> --strip-components=1sudo chown -R kewos:kewos /opt/kewos/releases/<new>
# 3. Switch, align the schema, restart.sudo ln -sfn /opt/kewos/releases/<new> /opt/kewos/currentsudo systemctl stop kewos kewos-workercd /opt/kewos/current && sudo -u kewos env $(grep -v '^#' /etc/kewos/kewos.env | xargs) \ ./bin/kewos reconcile --confirm --modules-root /opt/kewos/current/modulessudo systemctl start kewos kewos-workerRolling back is the same move in reverse:
sudo ln -sfn /opt/kewos/releases/<old> /opt/kewos/currentsudo systemctl restart kewos kewos-workerNext
Go live: the settings to get right before you open the door.