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.
Document reading (optional)
If your users upload scanned invoices or delivery notes and you want the text out of them without the documents going anywhere:
sudo apt install -y tesseract-ocr tesseract-ocr-fra poppler-utilstesseract-ocr reads images, the language data is picked per language (-fra, -deu, adapt
it), and poppler-utils provides pdftoppm, which turns a scanned PDF into images before
reading.
It is optional on purpose. Without these packages the application runs exactly the same:
the matching engines do not register, the Settings → Integrations → Document reading screen
does not offer them, and nothing fails on the first click. A PDF that already carries a text
layer (an invoice from accounting software, an e-invoice) is read without any of these
packages, exactly, and that is the common case.
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
Kewos ships as one .zip carrying everything: the application binary (the API and the
interface in a single process), the built interface, the modules tree, a docker-compose for
Postgres and Redis, a configuration template, and an installer. No Python, no Node, no build
toolchain is required on the target host.
Kewos hands you a download identifier and a password with your licence. They are for this and nothing else, and they are not your application credentials.
cd /tmpcurl -u '<your-identifier>' -O https://download.kewos.io/erp/kewos-<version>.zipcurl -u '<your-identifier>' -O https://download.kewos.io/erp/kewos-<version>.zip.sha256curl asks for the password: do not put it in the command, it would stay in your shell history.
Check the digest before deploying. A package that does not match its own is not a Kewos package.
sha256sum -c kewos-<version>.zip.sha256# kewos-<version>.zip: OK
unzip -q kewos-<version>.zipcd kewos-<version>You get:
kewos the binary: the application AND the command-line toolinstall.sh the one-shot installerui/ the built interface, served by the same processmodules/ the modules tree, browsable and extensible on sitedocker-compose.onprem.yml Postgres 16 + Redis 7, if you do not have them already.env.example the configuration templateVERSION the bundle's versionINSTALL.md the same procedure, offlineThe binary can say what it is, which saves picking the wrong one on a host carrying two:
./kewos --version# kewos 0.9.27The short way: the installer
sudo ./install.shIt copies the bundle to /opt/kewos, creates a dedicated service user, puts kewos on the
PATH, installs shell completion, drops the configuration for you to edit and writes the two
systemd services (the application and the worker). It starts nothing: editing the
configuration comes first.
On a host without systemd (a container, another service manager) it says so, skips the unit files and gives you the two processes to run yourself. Everything else is installed and works.
After it, four gestures remain: edit /opt/kewos/.env, bring up Postgres and Redis, run
kewos provision, start it. The sections below detail them.
The long way: by hand
If you would rather own the layout, or deploy versions side by side with an instant rollback:
sudo mkdir -p /opt/kewos/releasessudo unzip -q /tmp/kewos-<version>.zip -d /opt/kewos/releases/sudo chown -R kewos:kewos /opt/kewos/releases/kewos-<version>sudo ln -sfn /opt/kewos/releases/kewos-<version> /opt/kewos/currentThe symlink is not decoration: it is what makes an upgrade and a rollback immediate, without ever leaving 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.example.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@example.comKEWOS_ADMIN_PASSWORD=...
# --- Recommended ---------------------------------------------------------KEWOS_LOG_LEVEL=INFOKEWOS_MAIL_FROM=no-reply@example.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) \ ./kewos 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) \ ./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) \ ./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 ./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/kewosRestart=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/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.example.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.example.com;
ssl_certificate /etc/letsencrypt/live/erp.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/erp.example.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.example.com/health/live# {"status":"alive"}
curl -fsS https://erp.example.com/health/schema# {"status":"ok","tenants_checked":1,"drifted":{}}
curl -o /dev/null -w '%{http_code}\n' https://erp.example.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 unzip -q /tmp/kewos-<new>.zip -d /opt/kewos/releases/sudo chown -R kewos:kewos /opt/kewos/releases/kewos-<new>
# 3. Switch, align the schema, restart.sudo ln -sfn /opt/kewos/releases/kewos-<new> /opt/kewos/currentsudo systemctl stop kewos kewos-workercd /opt/kewos/current && sudo -u kewos env $(grep -v '^#' /etc/kewos/kewos.env | xargs) \ ./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.