Index Monitor appliance install guide
This guide takes you from a purchased Index Monitor appliance license to a running self-hosted appliance on a Linux server with Docker.
The install release is appliance-v1.1.5. The published GHCR images for that
release are pinned below to v1.1.5; do not use latest.
Prerequisites
- A Linux server where you can run Docker containers.
- Docker Engine with the Compose plugin, using the
docker composecommand. This stack was verified with Docker29.6and Composev5.2; no lower supported minimum is published for this release. - Inbound HTTP on the host port you choose. The compose file maps
APPLIANCE_HTTP_PORTto container port80; the default is host port8080. - A public HTTPS URL for the appliance, for example
https://gsc.example.com. Use your normal reverse proxy or load balancer for TLS if you do not expose the container port directly. - Outbound HTTPS from the appliance to the license server and to Google.
- Enough CPU, RAM, and disk for MariaDB data and Search Console history. See Hardware sizing / small deployments for the small-box preset and practical minimums.
Before you start, have these ready:
- Your appliance install key.
- The vendor license server URL and license public key supplied with your purchase.
- A real operator email address.
- Mail delivery credentials, or a decision to start with logging-only mail and finish delivery setup in the browser wizard.
- If you want OAuth Search Console access, a Google Cloud project where you can create a Web application OAuth client.
Hardware sizing / small deployments
The Index Monitor appliance is an API poller plus MariaDB. Google does the crawling, indexing, and Search Console processing; the appliance stores the results and serves the UI. There is no crawler and no headless browser in the GSC profile.
Container footprint
| Service | Typical memory footprint | Notes |
|---|---|---|
db |
About 250-400 MB on the small preset | MariaDB stores Search Console rows. The small preset caps innodb_buffer_pool_size at 128 MB. |
app |
About 30-60 MB per PHP-FPM child | The default pool allows 20 children for a busy agency. The small preset lowers this to 4 children, roughly 150-300 MB of PHP-FPM budget. |
worker + cron |
About 100-200 MB combined | The Python workers are async I/O (aiohttp / aiomysql) and spend most of their time waiting on Google APIs or the database. |
Hardware recommendations
| Target | Recommendation | Notes |
|---|---|---|
| Bare minimum VPS | 2 vCPU / 2 GB RAM | Use the small preset below and add swap. Suitable for one operator and a few properties. |
| Comfortable small box | 2-4 cores / 4 GB RAM | Recommended starting point for a single agency. Leaves room for backfills, reports, and database cache. |
| Old laptop or mini-PC | 2+ cores / 4 GB+ RAM, SSD | Usually the easiest "hardware you already own" option. x86-64 images work unchanged. |
| Small VPS | 2 vCPU / 4 GB RAM | A good hosted baseline if you do not want to keep hardware on site. |
The current appliance images are published for linux/amd64 only. Use an
x86-64 Linux host for this release.
Small-box preset
For a 2 GB target, you can optimize MariaDB and PHP-FPM memory limits.
Create mariadb-small.cnf in your install directory:
[mariadb]
innodb_buffer_pool_size=128M
max_connections=80
performance_schema=OFF
Then, in your docker-compose.yml file (created below):
1. Add the PHP-FPM variables to your x-app-env: section.
2. Mount the .cnf file in the db service volumes.
x-app-env: &app-env
# ... existing env vars ...
SC_PHP_FPM_MAX_CHILDREN: "4"
SC_PHP_FPM_START_SERVERS: "2"
SC_PHP_FPM_MIN_SPARE_SERVERS: "1"
SC_PHP_FPM_MAX_SPARE_SERVERS: "2"
SC_PHP_FPM_MAX_REQUESTS: "500"
services:
db:
# ...
volumes:
- appliance_db:/var/lib/mysql
- ./mariadb-small.cnf:/etc/mysql/conf.d/90-small.cnf:ro
On 2 GB hosts, add a swap file before first boot so a backfill or database warm-up does not trigger the kernel OOM killer:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
The Sentinel appliance has an optional JS-rendering worker profile that runs headless Chromium. The Index Monitor appliance does not use that worker.
Create the install directory
mkdir -p /opt/gsc-appliance/custom
cd /opt/gsc-appliance
The custom directory is mounted into the app container. You can leave it empty
on a standard install.
Pull the pinned images
This guide assumes public GHCR images:
docker pull ghcr.io/user256/portal-appliance-app:v1.1.5
docker pull ghcr.io/user256/portal-appliance-worker:v1.1.5
The image tag for release appliance-v1.1.5 is v1.1.5.
This is the intended customer path: public GHCR images after the the original 1.0.0 appliance-release retraction described in appliance-build-and-distribution.md. If the packages are still private during staging, authenticated GHCR pulls are the temporary fallback:
docker login ghcr.io
docker pull ghcr.io/user256/portal-appliance-app:v1.1.5
docker pull ghcr.io/user256/portal-appliance-worker:v1.1.5
Create docker-compose.yml
The easiest way to generate your configuration is using our Web Generator, which will output a fully-populated compose file with secure database passwords and your domain already set.
If you prefer to create it manually, use this single-file template. It uses a YAML anchor (x-app-env) to inject environment variables into all application containers without requiring a separate .env file.
x-app-env: &app-env
# Database
SC_DB_DRIVER: "mysql"
SC_DB_NAME: "portal"
SC_DB_USER: "portal"
SC_DB_PASS: "replace-with-a-strong-database-password"
# Configuration
SC_PUBLIC_SITE_BASE_URL: "https://gsc.example.com"
SC_SITE_PROFILE: "gsc_appliance"
APPLIANCE_HOSTNAME: "gsc-appliance"
# Initial Admin (Change this later)
ADMIN_EMAIL: "admin@example.com"
ADMIN_PASSWORD: "replace-with-a-temporary-strong-password"
# Mail Settings
# WARNING: alerts do not reach an inbox in log mode. Configure a delivery
# provider in the first-boot wizard before relying on alert email.
SC_EMAIL_TRANSPORT: "log"
SC_FROM_ADDRESS: "noreply@example.com"
SC_FROM_NAME: "Index Monitor"
SC_ADMIN_EMAIL: "admin@example.com"
# License Details — copy all three values from the vendor license handoff.
# Do not use a sample or placeholder public key: it must match the active
# Ed25519 signing key or this appliance cannot verify its Pro license.
SC_LICENSE_SERVER_URL: "https://analytics.johnfegan.org"
SC_LICENSE_PUBLIC_KEY: "XW1pxiNePCWpu3x+PMB4u2oRtTl/odMU5im9+sCeQac="
SC_LICENSE_INSTALL_KEY: "replace-with-your-install-key"
services:
db:
image: mariadb:10.11
restart: unless-stopped
environment:
MARIADB_DATABASE: "portal"
MARIADB_USER: "portal"
MARIADB_PASSWORD: "replace-with-a-strong-database-password"
MARIADB_ROOT_PASSWORD: "replace-with-a-strong-root-password"
volumes:
- appliance_db:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 12
app:
image: ghcr.io/user256/portal-appliance-app:v1.1.5
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "8080:80"
volumes:
- ./custom:/var/www/portal/custom
- appliance_license:/var/www/portal/var/cache/license
environment:
<<: *app-env
worker:
image: ghcr.io/user256/portal-appliance-worker:v1.1.5
restart: unless-stopped
depends_on:
db:
condition: service_healthy
volumes:
- appliance_license:/opt/portal/var/cache/license
environment:
<<: *app-env
cron:
image: ghcr.io/user256/portal-appliance-worker:v1.1.5
command: ["cron"]
restart: unless-stopped
depends_on:
db:
condition: service_healthy
volumes:
- appliance_license:/opt/portal/var/cache/license
environment:
<<: *app-env
volumes:
appliance_db:
appliance_license:
Env var reference
The variables above are the minimum needed to boot. You can find more configuration options in the Configuration guide (e.g. SMTP setups).
Start the appliance
docker compose -f docker-compose.yml up -d
Watch the first boot:
docker compose -f docker-compose.yml logs -f app
Expected app boot messages include:
[appliance] waiting for the database at db:3306 ...
[appliance] database ready after Ns.
[appliance] checking schema/code version compatibility...
[appliance] applying migrations (idempotent)...
[appliance] ensuring admin user <ADMIN_EMAIL> exists...
[appliance] seeding GSC tiers (idempotent)...
[appliance] activating default theme (idempotent)...
[appliance] handing off to: /usr/local/bin/appliance-run-app.sh
Check the login page:
curl -I http://localhost:8080/account/login.php
Use your chosen APPLIANCE_HTTP_PORT if you changed it from 8080.
First-boot setup wizard
Open the appliance in your browser:
http://your-server:8080/account/login.php
If you put the appliance behind HTTPS, use your public URL instead.
Log in with ADMIN_EMAIL and ADMIN_PASSWORD from .env. The browser setup
walks through these steps:
- Replace the bootstrap identity. Use your real operator email and a strong password. Passwords must be 12 or more characters with uppercase, lowercase, number, and symbol.
- Enable two-factor authentication. Scan the QR code with an authenticator app, confirm a code, and save the recovery codes.
- Configure notification delivery. Choose Resend, SMTP, or Gmail OAuth; set the primary sender; send a test email if you want delivery checked immediately. You can skip this step and return later, but alerts, invites, password resets, and operator notices will not send until delivery is configured.
- Connect Google Search Console. The wizard shows the exact authorized redirect URI. In Google Cloud, create a Web application OAuth client, add that redirect URI, enable the Search Console API, then upload the downloaded OAuth client JSON or paste the client ID and secret. Connect a Google account and choose the properties the appliance should collect.
- Finish setup and open Search Analytics.
GSC Analytics reuses the Google connection from Index Monitor. If you open the GSC Analytics connected-accounts page and it redirects to Index Monitor, that is expected.
Apply or update your license
The appliance reads the license install key from .env. To apply a license
after first boot, edit these values:
SC_LICENSE_INSTALL_KEY=lsv_replace_with_your_install_key
SC_LICENSE_SERVER_URL=https://analytics.johnfegan.org
SC_LICENSE_PUBLIC_KEY=XW1pxiNePCWpu3x+PMB4u2oRtTl/odMU5im9+sCeQac=
Then recreate the services so app, worker, and cron all receive the same environment:
docker compose -f docker-compose.yml up -d
docker compose -f docker-compose.yml restart worker cron
The cron container renews the license token at boot and then keeps it fresh. Check the cron logs:
docker compose -f docker-compose.yml logs --tail=80 cron
For a working license, expect a line like:
[license-renew] renewed OK.
The license is bound to this install's machine fingerprint. The fingerprint uses
the pinned APPLIANCE_HOSTNAME and the shared license volume. Do not change
APPLIANCE_HOSTNAME, delete the appliance_license volume, or move the install
to another host after the first successful renewal unless your vendor rotates
the install key.
Free tier and downgrade behavior
Without a valid Pro license, the appliance stays usable on the Free tier:
- Up to 5 Search Console properties.
- Up to 10,000 tracked URLs.
- Worker processing remains active but is capped.
Worker logs may include a Free-tier cap message when no valid license token is available.
If a license is cancelled, suspended, expired, or cannot renew, the appliance does not immediately shut off. A still-valid cached token continues to work until it expires. After expiry, the appliance falls back gracefully to the Free tier and may show an operator message such as:
Appliance locked. This appliance has not been licensed yet. Contact your vendor to restore access.
or:
The license has expired and could not be renewed.
Restore the license values in .env, keep the same hostname and license volume,
and restart cron and worker to renew again.
Basic troubleshooting
Show container status:
docker compose -f docker-compose.yml ps
The db service should become healthy, then app, worker, and cron should
stay running.
Check app boot and web errors:
docker compose -f docker-compose.yml logs --tail=200 app
Check worker startup and tier enforcement:
docker compose -f docker-compose.yml logs --tail=200 worker
Check license renewals:
docker compose -f docker-compose.yml logs --tail=200 cron
Check the web entry point from the server:
curl -i http://localhost:8080/account/login.php
Use your configured host port if it is not 8080.
Common problems:
| Symptom | What to check |
|---|---|
db is not healthy |
Check SC_DB_PASS, DB_ROOT_PASSWORD, and docker compose logs db. |
| Login page does not respond | Check APPLIANCE_HTTP_PORT, local firewall rules, and docker compose logs app. |
| First boot loops or exits | Read the [appliance] lines in app logs; schema and migration failures are reported there. |
License renew says not_configured |
One or more SC_LICENSE_INSTALL_KEY, SC_LICENSE_SERVER_URL, or SC_LICENSE_PUBLIC_KEY values is missing from .env. |
License renew returns http_403 |
The install key may be suspended, cancelled, already bound to a different fingerprint, or pointed at the wrong license server. Contact support. |
| Worker remains on Free tier after adding a license | Restart cron and worker, confirm [license-renew] renewed OK., and confirm APPLIANCE_HOSTNAME has not changed. |
| Google OAuth callback fails | Confirm SC_PUBLIC_SITE_BASE_URL and the Google authorized redirect URI match exactly, including https and the shared path /account/oauth-callback.php (for Gmail and Search Console). On upgrades from before v1.1.5, update the Google client first or Google returns redirect_uri_mismatch. |
| Emails do not send | Complete the Delivery step in the setup wizard, or check the provider variables for the transport in .env. |
Updates
For a future release, update the pinned image tags in
docker-compose.yml, then run:
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d
Keep APPLIANCE_HOSTNAME and the named volumes unchanged across updates.
Support
For license, image-access, or billing problems, use the support contact supplied with your purchase or license email.
For an already running appliance, open the in-app Support page from the account navigation. Include:
- The release:
appliance-v1.1.5. - Your public appliance URL.
- The output of
docker compose -f docker-compose.yml ps. - Relevant excerpts from
app,worker, orcronlogs.
Do not send .env, passwords, install keys, OAuth secrets, or provider API
keys in a support request.