Back to On-Premise
✓ Available now

On-Premise Installation Guide

Step-by-step guide to installing Captivo on your own server. With a Docker-ready Linux server, the entire process takes under 30 minutes.

1. Prerequisites

Make sure the following are installed on your server:

OSLinux x86_64 — Ubuntu 22.04+, Debian 12+, or RHEL 9+ recommended
RAMAt least 2 GB (4 GB recommended for production)
DiskAt least 10 GB (20 GB+ recommended for backups)
Docker Engine24+ — check with docker --version
Docker Compose v2docker compose version (space, not hyphen; v1 not supported)
opensslcheck with openssl version

Quick Docker install (Ubuntu/Debian):

curl -fsSL https://get.docker.com | sh

Inbound Ports

Connections coming into the server. Your NAS/gateway and (if used) the syslog source must be able to reach these.

PortProtocolPurpose
3000TCPManagement panel (HTTP without TLS)
80 / 443TCPCaddy when TLS is enabled
1812UDPRADIUS authentication (from NAS IPs only)
1813UDPRADIUS accounting (from NAS IPs only)
514UDP/TCPSyslog — only if the 5651 log module is used (from gateway/NAS IPs only)

The database port (5432) is not exposed — it is only reachable on the internal container network.

Outbound Rules

If you use these integrations, allow outbound connections from the server. On a restricted (deny-all outbound) network, only open the rows you actually use.

PortDestinationWhen required
TCP 587 / 465Your mail server (SMTP)Sending email (verification, reports, notifications)
TCP 443SMS provider APIGuest verification via SMS
TCP 443Your webhook target URLsIf you use webhook integrations
TCP 443PMS / hotel system (per address)If you use hotel PMS integration
TCP 443app.captivo.ioLicense validation (standard profile; not needed for air-gapped)
TCP 80*.kamusm.gov.trIf you use 5651 KamuSM timestamping
TCP 80 / 443Let's EncryptTLS profile — obtaining/renewing certificates (see §4)
A static LAN IP is sufficient for testing or internal use. A publicly accessible domain and TLS are recommended for production (see §4). Hotel PMS integration today works via a webhook into Captivo; it is enough for the PMS server to reach the panel address (3000 or 80/443).

2. Installation (Single Command)

The recommended path installs from Docker Hub: the images are public. You clone a small deploy repo and run the install script with a single command. For networks with no internet access, use the air-gapped (offline) alternative in §9.

2.1 Clone the Deploy Repo

A deploy package (captivo.io/onprem) contains only the docker compose file and install script — no source code. This is where install.sh comes from:

curl -fsSL https://captivo.io/onprem/captivo-onprem.tar.gz | tar xz
cd captivo-onprem

2.2 Run the Install Script

chmod +x onprem/install.sh
./onprem/install.sh

The script asks for the server address during installation and auto-suggests your LAN IP. If you'll use a domain with automatic HTTPS, see the TLS section.

The script:

  1. Checks for docker and openssl dependencies.
  2. Generates .env.onprem with all secrets (POSTGRES_PASSWORD, AUTH_SECRET, DATA_ENCRYPTION_KEY, etc.) randomly generated via openssl rand.
  3. Runs docker compose pull to pull the public images from Docker Hub, then up -d to start them.
  4. The migrator service applies the database schema.
  5. Prints the panel URL when done.

Docker Hub Images

The following images are public and pulled automatically by docker compose pull:

ImageRole
captivoio/captivo-onpremManagement panel + captive portal (Next.js)
captivoio/captivo-radiusFreeRADIUS authentication / accounting
captivoio/captivo-logd5651 log server (optional — see §6)
For air-gapped environments, the images can be pre-packaged into a .tar file and transferred with docker load — see §9 Air-Gapped Installation.

Example output:

✓ Secrets generated (.env.onprem)

✓ Starting services...

✔ Container captivo-postgres Started

✔ Container captivo-migrator Exited

✔ Container captivo-radius Started

✔ Container captivo-web Started

✓ Installation complete!

Panel: http://localhost:3000

IMPORTANT — DATA_ENCRYPTION_KEY: The DATA_ENCRYPTION_KEY value in .env.onprem is the encryption key for guest MAC addresses and other encrypted fields. Back up this file securely off-server (password manager, encrypted USB, etc.). If the key is lost, encrypted data becomes permanently unreadable.

2.3 Updating

To move to a new version — your data and .env.onprem are preserved:

docker compose -f docker-compose.onprem.yml --env-file .env.onprem pull
docker compose -f docker-compose.onprem.yml --env-file .env.onprem up -d
docker image prune -f   # clean old (untagged) image layers

Every update leaves the previous images untagged, accumulating ~1-2 GB per release; left uncleaned, the disk eventually fills up and a future update can fail halfway.docker image prune -f only removes untagged layers — it never touches running containers or freshly pulled images.

The migrator applies new schema changes automatically and additively; the database and secrets are unchanged. A destructive schema change (rare) safely halts the update to prevent data loss — follow the manual steps in that release's notes if it occurs.

An update never touches .env.onprem or the database volume, so credentials stay consistent. New images are pulled from Docker Hub and containers are recreated.

3. First Launch — Setup Wizard

The first time you open the panel (http://<server-ip>:3000), you will be automatically redirected to the /setup wizard. It has 10 steps:

1

Admin Account

Set the super-admin email and password.

2

Organisation

Company/organisation name, logo, language preference.

3

Server Address

The permanent address the panel presents to guests (email links + downloadable portal) — optional.

4

Portal Design

Captive portal appearance (background, colors, heading).

5

RADIUS

RADIUS shared secret and NAS configuration.

6

Login Methods

Social login, SMS OTP, guest form options.

7

SMS

SMS provider (your own account: Netgsm, Twilio, etc.) — optional.

8

SMTP

Outbound email server (for notifications) — optional.

9

Licence

Upload a .lic file or start the 15-day free trial.

10

Done

Setup summary and sign in to the panel.

SMS and SMTP steps are optional — skip them and configure later from the Settings menu.

4. TLS / HTTPS (Recommended for Production)

The default http://localhost:3000 is for local testing only. HTTPS is required in production.

4.1 Configure Your Domain

Edit .env.onprem:

NEXTAUTH_URL=https://wifi.company.com
SITE_DOMAIN=wifi.company.com

4.2 Start Caddy with the TLS Profile

docker compose -f docker-compose.onprem.yml \
  --profile tls \
  --env-file .env.onprem \
  up -d
Caddy uses the SITE_DOMAIN variable to reverse-proxy captivo-web:3000 via the Caddyfile and automatically obtains a TLS certificate from Let's Encrypt. Requirement: The domain's DNS must resolve to this server's public IP and ports 80/443 must be externally reachable.

4.3 TLS with a Corporate CA (Closed Network / Internal FQDN)

On a closed network with an internal domain (e.g. captivo.acme.local), Let's Encrypt cannot be used. Instead you present Caddy with a certificate issued by your organization's own CA (e.g. AD CS).

  1. Issue a server-auth certificate whose SAN is your internal FQDN (corporate CA / AD CS). You need two files: cert.pem(server certificate + intermediate chain) and key.pem(unencrypted private key). Place both files in onprem/certs/ on the server.
  2. Edit .env.onprem:
SITE_DOMAIN=captivo.acme.local
CADDY_TLS_BLOCK=tls /certs/cert.pem /certs/key.pem

Then start the services with the --profile tlscommand from §4.2. (If CADDY_TLS_BLOCK is left empty, Caddy attempts automatic ACME; tls internalis for testing only — devices won't trust it.)

Guest-device warning — critical: Guest BYOD devices do not trust your internal CA. Browsers cannot skip certificate validation for in-page fetch/XHR calls, so if the guest portal address is an internal-CA HTTPS FQDN, the portal's SMS/voucher requests fail silently. The fix is a dual face: staff panel on the corporate-CA https://captivo.acme.local; the guest portal stays on a plain HTTP LAN address (http://LAN-IP:3000) under Settings → System → Server Address. Both faces run side by side in the same box.

5. RADIUS — Connecting Your NAS

The FreeRADIUS container starts automatically and listens on UDP 1812/1813. Configure the following on your NAS or gateway device:

SettingValue
RADIUS server IPThis server's LAN IP address
Authentication port1812 UDP
Accounting port1813 UDP
RADIUS methodPAP
Shared secretThe value generated in wizard Step 5 or shown at Settings → RADIUS
Add Captivo's server LAN IP to the “walled garden” or “unauthenticated pass-through” list on your NAS so guests can reach the portal before authentication.

Supported NAS/gateway models: pfSense, OPNsense, MikroTik, FortiGate, Cisco Meraki, UniFi, Ruijie, and other devices supporting PAP/CHAP.

6. 5651 Log Server (Optional)

The captivo-logd service collects traffic logs (syslog) from your firewall, correlates them with guest identity via RADIUS, signs them daily, and retains them for 2 years as required by Turkish Law No. 5651 — after which they are auto-deleted for KVKK (data-protection) compliance. This is an optional module — enable it for deployments with a 5651 log obligation.

6.1 Enable the Service

The log service listens on port 514 (UDP/TCP). Point your firewall's syslog output to this server:

SettingValue
Syslog target address<server-ip>:514
ProtocolUDP or TCP (514)
Source devicespfSense, OPNsense, MikroTik, FortiGate, Cisco Meraki, UniFi, Ruijie
Make sure port 514 is reachable from firewall → server. Otherwise logs never reach the server.

6.2 Review in the Panel

Collected logs are managed in the panel under Settings → Log Server:

  • Sources — lists the firewalls sending logs
  • Search — query logs with the correlated guest identity
  • Signed archive — daily signed log bundles
  • Verify — verify archive integrity
  • Settings — retention period and other parameters
Logs are self-signed daily; a qualified (TÜBİTAK KamuSM) timestamp integration is available and configured from the panel — both the KamuSM test and production environments are supported.
2 years of raw logs can consume significant disk space. The retention period is configurable under Settings → Log Server — plan your disk capacity accordingly.

7. Licence

A 15-day free trial starts automatically after installation with all features enabled.

To obtain a licence (free right now — for registration only):

  1. Go to Settings → Licence in the panel and copy your Installation ID.
  2. Email support@captivo.io with your full name, organization, Installation ID and email. (The “Request by email” button in the panel pre-fills this email for you.)
  3. Upload the .lic file you receive on the same page; licence details (expiry date, quota) are displayed on screen.

When the trial or licence expires, the system winds down gradually: a warning first, then read-only mode (new guest registration stops, existing data is visible), and finally panel access is restricted until the licence is renewed.

No internet connection required for licence validation in air-gapped environments.

8. Backups

CRITICAL: Back up .env.onprem securely off-server. Without the DATA_ENCRYPTION_KEY inside it, encrypted data in the database backup (MAC addresses, etc.) becomes permanently unreadable.

Database Backup

onprem/backup.sh runs pg_dump from PostgreSQL, compresses the output and stores it (7-day rotation).

Run manually:

POSTGRES_USER=captivo BACKUP_DIR=/opt/captivo-backups \
  ./onprem/backup.sh

Schedule with cron (daily at 03:15):

15 3 * * * POSTGRES_USER=captivo BACKUP_DIR=/opt/captivo-backups \
  /opt/captivo-onprem/onprem/backup.sh >> /var/log/captivo-backup.log 2>&1
Local backups are lost if the server fails. Copy backups regularly to a secondary location — S3, R2, a NAS, or an encrypted external drive.

Application Scheduled Jobs (Optional)

Some features run on a periodic trigger: scheduled reports, MAC whitelist expiry, KVKK/5651 retention cleanup, session webhooks, and the SMS anomaly alert. To use them, add a CRON_SECRET to .env.onprem(e.g. openssl rand -hex 32), restart the container, and add the cron lines below:

# Session webhooks (session.started/ended) — if accounting is on; every minute
*/1 * * * *  curl -sS -X POST -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/session-events       >> /var/log/captivo-cron.log 2>&1
# MAC whitelist expiry — hourly
10 * * * *   curl -sS -X POST -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/mac-whitelist-expiry >> /var/log/captivo-cron.log 2>&1
# Scheduled reports — hourly
0 * * * *    curl -sS -X POST -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/scheduled-reports    >> /var/log/captivo-cron.log 2>&1
# KVKK/5651 retention cleanup — daily
30 3 * * *   curl -sS -X POST -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/kvkk-retention       >> /var/log/captivo-cron.log 2>&1
# SMS usage anomaly — every 15 minutes
*/15 * * * * curl -sS -X POST -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/sms-anomaly          >> /var/log/captivo-cron.log 2>&1
These jobs are optional — add only the lines you use. (trial-expiry is for the SaaS subscription only and is not needed on-premise.)

9. Air-Gapped (Offline) Installation

For networks with no internet access, instead of pulling from Docker Hub the images can be pre-packaged into a .tar file and transferred to the target server. This is the offline alternative to the Docker Hub path in §2.

Bundle on an internet-connected machine

# Bundle the latest release
./scripts/onprem/build-offline-bundle.sh

# For a specific version:
./scripts/onprem/build-offline-bundle.sh 1.0.0

Output: captivo-onprem-1.0.0.tar (includes web, RADIUS, logd, postgres and caddy images)

This bundling script is part of the vendor distribution tooling; it is not included in the customer install package. For installations with no internet access, obtain the image .tar from your vendor — on the target machine only the docker load+ install.sh steps below are needed.

Install on the target machine

# Load images into Docker
docker load -i captivo-onprem-1.0.0.tar

# Run the install script (no internet needed)
./onprem/install.sh
Let's Encrypt certificates cannot be obtained in air-gapped environments. Use your corporate CA certificate or HTTP for internal networks.

10. Troubleshooting

Panel won't open

  • docker compose … ps — are all containers in the Up state?
  • Did the migrator service complete successfully (Exited (0))?
  • Is port 3000 open in your firewall?
  • Logs: docker compose … logs web

RADIUS connection error (guests can't log in)

  • Is the RADIUS server IP on the NAS correct? (This server's LAN IP)
  • Is UDP 1812/1813 reachable from the NAS?
  • Is the shared secret exactly the same on the NAS and in Captivo?
  • Is captivo-radius running? (docker logs captivo-radius)

SMS not arriving

  • Were SMS provider credentials entered in wizard Step 7?
  • Is this server's IP in the NAS walled garden list?

Email notifications not sent

  • Were SMTP settings configured in wizard Step 8?
  • Verify SMTP port and password in the container logs.

General log inspection

# All service logs (last 50 lines)
docker compose -f docker-compose.onprem.yml \
  --env-file .env.onprem \
  logs --tail=50

# FreeRADIUS logs
docker logs captivo-radius

Need help? Contact our support team.