Installation Example: Ubuntu 24.04¶
This guide walks through a complete on-premise installation on a single Ubuntu 24.04 server, using PostgreSQL on the host, MinIO for S3-compatible storage, and nginx as the TLS reverse proxy. It is based on a real, working deployment.
Adapt it to your environment — the core Installation and Configuration guides remain the reference.
Prefer automation?
A coding AI agent can run this entire installation for you in a single prompt. See AI-Assisted Installation.
Architecture¶
Browser → nginx (:443, TLS) → Docker containers (api :8080, web :8081)
→ PostgreSQL (:5432, on host)
→ MinIO (:9000, on host)
All services run on a single server. Containers reach host services via host.docker.internal.
1. Docker Engine¶
Install Docker from the official repository:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
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
Add your user to the docker group (log out and back in for it to take effect):
2. PostgreSQL 16¶
Create the database, user, and required extensions:
sudo -u postgres psql <<'SQL'
CREATE DATABASE kanap;
CREATE USER kanap WITH PASSWORD 'your-secure-password' NOSUPERUSER NOBYPASSRLS;
GRANT ALL PRIVILEGES ON DATABASE kanap TO kanap;
SQL
sudo -u postgres psql -d kanap <<'SQL'
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
GRANT ALL ON SCHEMA public TO kanap;
SQL
Allow connections from Docker containers¶
Edit /etc/postgresql/16/main/postgresql.conf:
Edit /etc/postgresql/16/main/pg_hba.conf — add before the default host rules:
This allows connections from the Docker bridge network. For tighter security, use the exact Docker subnet (docker network inspect bridge to find it).
Restart PostgreSQL and verify:
sudo systemctl restart postgresql
PGPASSWORD='your-secure-password' psql -h 127.0.0.1 -U kanap -d kanap -c "SELECT 1;"
3. MinIO (S3-Compatible Storage)¶
Download MinIO server and client:
wget -q https://dl.min.io/server/minio/release/linux-amd64/minio -O /tmp/minio
sudo mv /tmp/minio /usr/local/bin/minio
sudo chmod +x /usr/local/bin/minio
wget -q https://dl.min.io/client/mc/release/linux-amd64/mc -O /tmp/mc
sudo mv /tmp/mc /usr/local/bin/mc
sudo chmod +x /usr/local/bin/mc
Create a system user and data directory:
sudo useradd -r -s /sbin/nologin minio-user
sudo mkdir -p /opt/minio/data
sudo chown -R minio-user:minio-user /opt/minio
Configure MinIO — create /etc/default/minio:
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=your-minio-admin-password
MINIO_VOLUMES="/opt/minio/data"
MINIO_OPTS="--address :9000 --console-address :9001"
Create the systemd service — /etc/systemd/system/minio.service:
[Unit]
Description=MinIO Object Storage
After=network-online.target
Wants=network-online.target
[Service]
User=minio-user
Group=minio-user
EnvironmentFile=/etc/default/minio
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
Restart=always
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Enable and start MinIO, then create the bucket and a service account:
sudo systemctl daemon-reload
sudo systemctl enable minio
sudo systemctl start minio
mc alias set localminio http://127.0.0.1:9000 minioadmin your-minio-admin-password
mc mb localminio/kanap-files
mc admin user svcacct add localminio minioadmin \
--access-key your-s3-access-key \
--secret-key your-s3-secret-key
4. Clone KANAP and Configure¶
cd /opt # or your preferred directory
git clone https://github.com/kanap-it/kanap.git
cd kanap
cp infra/.env.onprem.example .env
Edit .env with your values. Here is a working example for this setup:
# DEPLOYMENT MODE
DEPLOYMENT_MODE=single-tenant
# TENANT
DEFAULT_TENANT_SLUG=default
DEFAULT_TENANT_NAME=My Organization
# ADMIN CREDENTIALS
ADMIN_EMAIL=admin@company.com
ADMIN_PASSWORD=ChangeThisAfterFirstLogin!
# SECURITY
JWT_SECRET=<output of: openssl rand -hex 32>
# APPLICATION URL
APP_BASE_URL=https://kanap.company.com
# CORS
CORS_ORIGINS=https://kanap.company.com
# DATABASE — host.docker.internal reaches the host from inside Docker
DATABASE_URL=postgres://kanap:your-secure-password@host.docker.internal:5432/kanap?sslmode=disable
# STORAGE — MinIO on the host
S3_ENDPOINT=http://host.docker.internal:9000
S3_BUCKET=kanap-files
S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-s3-access-key
AWS_SECRET_ACCESS_KEY=your-s3-secret-key
S3_FORCE_PATH_STYLE=true
# EMAIL (optional — choose one transport to enable invitations, password reset, notifications)
# RESEND_API_KEY=re_xxxxx
# RESEND_FROM_EMAIL=KANAP <noreply@yourdomain.com>
# SMTP (single-tenant / on-prem only)
# SMTP_HOST=smtp.company.com
# SMTP_PORT=587
# SMTP_SECURE=false
# SMTP_USER=noreply@company.com
# SMTP_PASSWORD=your-smtp-password
# SMTP_FROM=KANAP <noreply@company.com>
Important: Generate a real JWT secret (openssl rand -hex 32) — do not reuse example values.
If you access KANAP by IP address instead of a domain, set APP_BASE_URL and CORS_ORIGINS to https://YOUR_IP.
If you use SMTP instead of Resend, make sure the SMTP server accepts mail from the SMTP_FROM address and that your domain's SPF, DKIM, and DMARC settings are already in place if messages leave your internal network.
5. Build and Start¶
cd /opt/kanap
docker build -t kanap-api:latest ./backend
docker build -t kanap-web:latest ./frontend
docker compose -f infra/compose.onprem.yml up -d
Check the logs — wait for the "Application started" message:
On first boot, KANAP runs migrations and creates the tenant, admin user, and subscription automatically.
6. Nginx Reverse Proxy with TLS¶
TLS certificate¶
If you have a domain with a proper certificate, use that. Otherwise, generate a self-signed certificate:
sudo mkdir -p /etc/ssl/kanap
sudo openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout /etc/ssl/kanap/server.key \
-out /etc/ssl/kanap/server.crt \
-subj "/CN=kanap.company.com" \
-addext "subjectAltName=DNS:kanap.company.com"
For IP-based access (no domain), replace the -subj and -addext values:
Site configuration¶
Create /etc/nginx/sites-available/kanap:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name kanap.company.com;
ssl_certificate /etc/ssl/kanap/server.crt;
ssl_certificate_key /etc/ssl/kanap/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
client_max_body_size 20m;
# Canonicalize /api → /api/
location = /api { return 301 /api/; }
# API: strip /api prefix before proxying
location ^~ /api/ {
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_set_header X-Forwarded-Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_redirect off;
}
# Everything else → SPA
location / {
proxy_pass http://127.0.0.1:8081;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}
server {
listen 80;
listen [::]:80;
server_name kanap.company.com;
return 301 https://$host$request_uri;
}
Enable the site and restart nginx:
sudo ln -sf /etc/nginx/sites-available/kanap /etc/nginx/sites-enabled/kanap
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx
7. Verify¶
# API health check
curl -sk https://kanap.company.com/api/health
# Expected: {"status":"ok"}
# Frontend
curl -sk -o /dev/null -w "%{http_code}" https://kanap.company.com/
# Expected: 200
8. First Login¶
- Open your KANAP URL in a browser (accept the certificate warning if using self-signed TLS)
- Log in with the
ADMIN_EMAIL/ADMIN_PASSWORDfrom.env - Change the admin password immediately via user profile
- Configure your organization name in Admin settings
- Invite additional users (if email is configured)
Services Summary¶
| Service | Managed By | Config Location |
|---|---|---|
| Docker | systemd | — |
| PostgreSQL | systemd | /etc/postgresql/16/main/postgresql.conf |
| MinIO | systemd | /etc/default/minio |
| KANAP API | Docker Compose | .env + infra/compose.onprem.yml |
| KANAP Web | Docker Compose | .env + infra/compose.onprem.yml |
| nginx | systemd | /etc/nginx/sites-available/kanap |
Useful Commands¶
# View logs
docker compose -f infra/compose.onprem.yml logs -f
# Restart KANAP
docker compose -f infra/compose.onprem.yml restart
# Stop KANAP
docker compose -f infra/compose.onprem.yml down
# Rebuild after update
git pull origin main
docker build -t kanap-api:latest ./backend
docker build -t kanap-web:latest ./frontend
docker compose -f infra/compose.onprem.yml up -d
# Check all services
sudo systemctl status postgresql nginx minio
docker compose -f infra/compose.onprem.yml ps