A self-hosted web hosting control panel built with React Router 7. Manage websites, databases, and services through a modern web UI -- no cPanel or Plesk licence required.
Each user site runs in isolated Docker containers with its own network, Nginx + PHP-FPM web server, and MariaDB database. Optional per-site SFTP access can be enabled with one click.
- Site management -- create, start, stop, restart, and delete websites from the browser
- Isolated containers -- every site gets its own Docker network and web server, with optional database
- Auto-generated credentials -- database and SFTP passwords are created with cryptographic randomness
- Automatic SSL -- Traefik provisions and renews Let's Encrypt certificates for every site
- User authentication -- Keycloak provides login, password reset, MFA, and brute-force protection
- Role-based access -- admin users can manage all sites and users; regular users see only their own
- Server dashboard -- real-time CPU, RAM, disk, and network stats for admins
- Docker admin -- view running containers, prune unused resources, all from the panel
- SFTP per site -- optional SFTP container with auto-assigned port and generated credentials
- Per-site home folders -- site content lives under
/home/<user>/<site>/public_html - Activity log -- track who did what across the panel
Requirements: Ubuntu 22.04+ (or Debian 12+), a domain name with DNS pointed to the server, and ports 80/443 open.
curl -fsSL https://raw.githubusercontent.com/03c/jigsaw/main/install.sh -o /tmp/jigsaw-install.sh && chmod +x /tmp/jigsaw-install.sh && sudo /tmp/jigsaw-install.shThe installer will:
- Install Docker and Docker Compose if not present
- Clone the repository to
/opt/jigsaw - Ask for your domain, email, and Keycloak admin password
- Auto-generate all secrets (database passwords, session key, OIDC client secret), reusing existing
.envsecrets on reruns - Pull prebuilt panel and PHP images from GHCR
- Start the full stack (Traefik, PostgreSQL, Keycloak, Jigsaw panel)
- Run database migrations
- Validate DNS, request one SAN Let's Encrypt certificate for panel/auth/traefik, then print first-login instructions
If you've already cloned the repo, run the script directly:
sudo ./install.shPoint three A records to your server's public IP:
| Record | Type | Value |
|---|---|---|
panel.example.com |
A | <your-server-ip> |
auth.panel.example.com |
A | <your-server-ip> |
traefik.panel.example.com |
A | <your-server-ip> |
Each site you create will also need its own A record pointing to the same IP.
If you prefer to set things up yourself instead of using the installer:
# 1. Clone
git clone https://github.com/03c/jigsaw.git /opt/jigsaw
cd /opt/jigsaw
# 2. Create .env from the example and fill in your values
cp .env.example .env
nano .env
# 3. Generate secrets for .env
# POSTGRES_PASSWORD: openssl rand -base64 32 | tr -d '/+='
# SESSION_SECRET: openssl rand -hex 32
# KEYCLOAK_CLIENT_SECRET: openssl rand -base64 48 | tr -d '/+='
# 4. Patch the Keycloak realm with your client secret and admin email
sed -i "s|JIGSAW_CLIENT_SECRET_PLACEHOLDER|<your-client-secret>|" keycloak/jigsaw-realm.json
sed -i "s|JIGSAW_ADMIN_EMAIL_PLACEHOLDER|<your-email>|" keycloak/jigsaw-realm.json
# 5. Create data directories
mkdir -p data/sites data/databases data/postgres docker/compose
# 6. Pull prebuilt images
docker pull ghcr.io/03c/jigsaw/panel:latest
docker pull ghcr.io/03c/jigsaw/php:8.4
docker tag ghcr.io/03c/jigsaw/php:8.4 jigsaw-php:8.4
# 7. Start the stack
docker compose up -d
# 8. Wait for PostgreSQL, then run migrations
docker compose exec jigsaw npm run db:pushBuild and push images from your workstation or CI:
docker build -t ghcr.io/03c/jigsaw/panel:latest .
docker build -t ghcr.io/03c/jigsaw/php:8.4 docker/templates/web/
echo "$GITHUB_TOKEN" | docker login ghcr.io -u <github-username> --password-stdin
docker push ghcr.io/03c/jigsaw/panel:latest
docker push ghcr.io/03c/jigsaw/php:8.4- Open
https://panel.example.com-- you'll be redirected to Keycloak - Log in with username admin and the Keycloak admin password you entered during install
- You're now in the Jigsaw dashboard as an admin
- To create additional users, go to
https://auth.panel.example.comand use the Keycloak admin console
If Keycloak fails with password authentication failed for user "jigsaw", your PostgreSQL data was initialized with a different password than the one in .env.
For a fresh install, reset PostgreSQL data and rerun:
docker compose down && sudo rm -rf data/postgres && sudo ./install.shIf Traefik logs client version 1.24 is too old. Minimum supported API version is 1.44, update to the latest repo and recreate containers:
git pull && docker compose down && docker compose up -dIf install times out waiting for SSL certificates, verify panel/auth/traefik domains resolve to this server and that ports 80/443 are reachable from the internet.
If /auth/login returns Unexpected Server Error or Authentication is temporarily unavailable, Keycloak is not ready from the panel container yet. Check:
docker compose logs -f keycloak jigsaw
docker compose exec -T jigsaw node -e "fetch('http://keycloak:8080/realms/jigsaw/.well-known/openid-configuration').then((r)=>r.text().then((t)=>console.log(r.status,t.slice(0,120)))).catch((e)=>{console.error(e);process.exit(1)})"If Keycloak shows Invalid parameter: redirect_uri, update the jigsaw-panel client redirect URI to exactly:
https://<your-panel-domain>/auth/callback
Then restart the panel:
docker compose restart jigsawIf the browser shows ERR_TOO_MANY_REDIRECTS after login, clear cookies for server.jigsawhost.com and auth.server.jigsawhost.com, then retry using the exact configured panel domain (not IP or alternate host).
If logs include OAUTH_JSON_ATTRIBUTE_COMPARISON_FAILED with issuer mismatch (expected http://keycloak:8080/... vs issuer https://auth.<domain>/...), pull the latest config and recreate the panel:
git pull && docker compose up -d --force-recreate jigsawIf certificate names look wrong after changing TLS domain settings, recreate Traefik certificates:
docker compose down
docker volume rm $(docker volume ls -q | grep traefik_letsencrypt)
docker compose up -dInternet
|
[Traefik] ports 80/443, auto-SSL
|
├── Jigsaw Panel (React Router 7, Node.js)
├── Keycloak (authentication)
├── PostgreSQL (shared: panel data + Keycloak data)
│
├── site-a_web (Nginx + PHP-FPM) ┐
├── site-a_db (MariaDB) ├─ isolated network per site
├── site-a_sftp (optional) ┘
│
├── site-b_web ┐
├── site-b_db ├─ isolated network per site
└── ... ┘
All configuration is in the .env file. See .env.example for all available options.
| Variable | Description |
|---|---|
PANEL_DOMAIN |
Domain for the panel (Keycloak is at auth.<domain>) |
ACME_EMAIL |
Email for Let's Encrypt certificate notifications |
POSTGRES_PASSWORD |
PostgreSQL password (auto-generated) |
KEYCLOAK_ADMIN_PASSWORD |
Keycloak admin console password |
KEYCLOAK_CLIENT_SECRET |
OIDC client secret shared between Keycloak and the panel |
KEYCLOAK_CONSOLE_URL |
URL used for admin Keycloak navigation links |
TRAEFIK_DASHBOARD_URL |
URL used for admin Traefik navigation links |
SITE_WEB_IMAGE_TEMPLATE |
Template used for site web images (default jigsaw-php:{phpVersion}) |
SITE_DB_IMAGE |
Database image for new site DB containers (default mariadb:lts) |
SITE_SFTP_IMAGE |
Image for per-site SFTP containers (default atmoz/sftp) |
SITES_BASE_PATH_HOST |
Host base path for site folders (default /home) |
SITES_BASE_PATH_PANEL |
Mounted path inside panel container for writing site files |
DOCKER_SOCKET_PATH |
Docker daemon socket override (useful for Windows local dev) |
SESSION_SECRET |
Encryption key for session cookies |
jigsaw/
├── app/
│ ├── components/ # UI components (sidebar, cards, badges)
│ ├── lib/ # Server utilities
│ │ ├── auth.server.ts # Keycloak OIDC (PKCE flow)
│ │ ├── db.server.ts # PostgreSQL via Drizzle ORM
│ │ ├── docker.server.ts # Docker container orchestration
│ │ ├── images.server.ts # Site image resolution and env overrides
│ │ ├── session.server.ts # Cookie session + auth guards
│ │ ├── crypto.server.ts # Password/slug/UUID generation
│ │ └── stats.server.ts # System & Docker stats
│ ├── models/
│ │ └── schema.ts # Drizzle schema (users, sites, services, activity_log)
│ ├── routes/ # React Router 7 routes
│ │ ├── auth.*.tsx # Login, callback, logout
│ │ ├── dashboard.*.tsx # User dashboard, site management
│ │ └── admin.*.tsx # Admin panel, user management, server stats
│ ├── root.tsx
│ └── routes.ts # Route config
├── docker/
│ ├── templates/web/ # Nginx + PHP-FPM Dockerfile & config
│ ├── templates/site/ # Default site bootstrap templates
│ └── init-keycloak-db.sql # Creates Keycloak DB in shared PostgreSQL
├── keycloak/
│ └── jigsaw-realm.json # Keycloak realm with roles, client, default admin
├── docker-compose.yml # Full stack: Traefik + PostgreSQL + Keycloak + Panel
├── docker-compose.local.yml # Local-only PostgreSQL + Keycloak services
├── scripts/
│ └── prepare-dev-realm.mjs # Generates local Keycloak realm import file
├── Dockerfile # Multi-stage build for the panel (Node 22)
├── drizzle.config.ts
├── install.sh # Interactive installer
├── .env.example
├── package.json
└── tsconfig.json
| Layer | Technology |
|---|---|
| Frontend + Backend | React Router 7 (SSR, loaders, actions) |
| Database (panel) | PostgreSQL 17 via Drizzle ORM |
| Database (sites) | MariaDB 11 (one per site) |
| Auth | Keycloak 26 (OIDC/PKCE) |
| Reverse Proxy | Traefik v3 (auto-SSL) |
| Container Mgmt | dockerode (Node.js Docker SDK) |
| Styling | Tailwind CSS 4 |
| Runtime | Node.js 22 LTS |
For a fast local dev loop (recommended), run the app on your host with HMR and only run backing services in Docker.
npm install
# Create local env file
cp .env.local.example .env.local
# Start local services (PostgreSQL + Keycloak)
npm run dev:services:up
# Push the schema
npm run db:push
# Start the dev server with HMR
npm run devPowerShell equivalent for env copy:
Copy-Item .env.local.example .env.localThe app runs at http://localhost:5173 and Keycloak at http://localhost:8080.
Default local credentials:
- Keycloak admin user:
admin - Keycloak admin password:
admin(change in.env.local)
When you are done:
npm run dev:services:downTo tail local service logs:
npm run dev:services:logsIf you want to test Traefik + TLS + router labels exactly like production, run the full stack in Docker (docker compose up -d) and point local domains accordingly.
# View logs
docker compose logs -f
docker compose logs -f jigsaw
# Local dev services
npm run dev:services:up
npm run dev:services:down
npm run dev:services:logs
npm run dev:bootstrap
# Restart the panel after code changes
docker compose restart jigsaw
# Pull and restart the panel
docker compose pull jigsaw && docker compose up -d jigsaw
# Run database migrations
docker compose exec jigsaw npm run db:push
# Open a shell in the panel container
docker compose exec jigsaw sh
# Stop everything
docker compose down
# Stop everything and remove volumes (destructive!)
docker compose down -vMIT