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:
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.
| Port | Protocol | Purpose |
|---|---|---|
| 3000 | TCP | Management panel (HTTP without TLS) |
| 80 / 443 | TCP | Caddy when TLS is enabled |
| 1812 | UDP | RADIUS authentication (from NAS IPs only) |
| 1813 | UDP | RADIUS accounting (from NAS IPs only) |
| 514 | UDP/TCP | Syslog — 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.
| Port | Destination | When required |
|---|---|---|
| TCP 587 / 465 | Your mail server (SMTP) | Sending email (verification, reports, notifications) |
| TCP 443 | SMS provider API | Guest verification via SMS |
| TCP 443 | Your webhook target URLs | If you use webhook integrations |
| TCP 443 | PMS / hotel system (per address) | If you use hotel PMS integration |
| TCP 443 | app.captivo.io | License validation (standard profile; not needed for air-gapped) |
| TCP 80 | *.kamusm.gov.tr | If you use 5651 KamuSM timestamping |
| TCP 80 / 443 | Let's Encrypt | TLS profile — obtaining/renewing certificates (see §4) |
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:
- Checks for docker and openssl dependencies.
- Generates
.env.onpremwith all secrets (POSTGRES_PASSWORD, AUTH_SECRET, DATA_ENCRYPTION_KEY, etc.) randomly generated via openssl rand. - Runs
docker compose pullto pull the public images from Docker Hub, thenup -dto start them. - The
migratorservice applies the database schema. - Prints the panel URL when done.
Docker Hub Images
The following images are public and pulled automatically by docker compose pull:
| Image | Role |
|---|---|
| captivoio/captivo-onprem | Management panel + captive portal (Next.js) |
| captivoio/captivo-radius | FreeRADIUS authentication / accounting |
| captivoio/captivo-logd | 5651 log server (optional — see §6) |
.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
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.
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:
Admin Account
Set the super-admin email and password.
Organisation
Company/organisation name, logo, language preference.
Server Address
The permanent address the panel presents to guests (email links + downloadable portal) — optional.
Portal Design
Captive portal appearance (background, colors, heading).
RADIUS
RADIUS shared secret and NAS configuration.
Login Methods
Social login, SMS OTP, guest form options.
SMS
SMS provider (your own account: Netgsm, Twilio, etc.) — optional.
SMTP
Outbound email server (for notifications) — optional.
Licence
Upload a .lic file or start the 15-day free trial.
Done
Setup summary and sign in to the panel.
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
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).
- 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) andkey.pem(unencrypted private key). Place both files inonprem/certs/on the server. - 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.)
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:
| Setting | Value |
|---|---|
| RADIUS server IP | This server's LAN IP address |
| Authentication port | 1812 UDP |
| Accounting port | 1813 UDP |
| RADIUS method | PAP |
| Shared secret | The value generated in wizard Step 5 or shown at Settings → RADIUS |
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:
| Setting | Value |
|---|---|
| Syslog target address | <server-ip>:514 |
| Protocol | UDP or TCP (514) |
| Source devices | pfSense, OPNsense, MikroTik, FortiGate, Cisco Meraki, UniFi, Ruijie |
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
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):
- Go to Settings → Licence in the panel and copy your Installation ID.
- 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.)
- Upload the
.licfile 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.
8. Backups
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
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
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)
.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
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.