Pingtower Docs
Docs Self-hosting Install the daemon

Install the daemon

A single static Go binary on a Linux box, verified before it runs, under systemd, with its data in PostgreSQL — or SQLite, with nothing else to install.

  1. Download and verify

    Get the binary for your architecture from the download table. Every release carries an SSH signature over a signed provenance file that names exactly what was built. Check the SHA-256 against the table, then verify the signature against the release public key, pingtower-release.pub.

    sha256sum pingtower-linux-amd64   # compare with the table
    printf '[email protected] namespaces="pingtower-release" %s\n' \
      "$(cut -d' ' -f1,2 pingtower-release.pub)" > allowed_signers
    ssh-keygen -Y verify -f allowed_signers -I [email protected] \
      -n pingtower-release -s pingtower-linux-amd64.release.sig \
      < pingtower-linux-amd64.release
    cat pingtower-linux-amd64.release   # sha256= must match the sum above
    

    Needs stock OpenSSH 8.2 or later. If any check fails, do not run the binary.

  2. Install the binary and a user

    sudo install -m 0755 pingtower-linux-amd64 /usr/local/bin/pingtower
    sudo useradd --system --home /var/lib/pingtower --shell /usr/sbin/nologin pingtower
    sudo mkdir -p /var/lib/pingtower
    sudo chown pingtower:pingtower /var/lib/pingtower
    
  3. Create a systemd unit

    Settings that are secrets go in an environment file rather than the unit, which any local user can read. /etc/pingtower/pingtower.env, owned by root, mode 0600:

    PINGTOWER_SECRET_KEY=…   # openssl rand -base64 48; back it up now
    PINGTOWER_CLIENT_IP_HEADER=X-Real-IP
    

    /etc/systemd/system/pingtower.service:

    [Unit]
    Description=pingtower
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=simple
    User=pingtower
    Group=pingtower
    EnvironmentFile=/etc/pingtower/pingtower.env
    ExecStart=/usr/local/bin/pingtower \
      -listen 127.0.0.1:8391 \
      -data-dir /var/lib/pingtower
    Restart=on-failure
    RestartSec=2
    TimeoutStopSec=20
    LimitNOFILE=65536
    UMask=0077
    NoNewPrivileges=yes
    ProtectSystem=strict
    ReadWritePaths=/var/lib/pingtower
    ProtectHome=yes
    PrivateTmp=yes
    
    [Install]
    WantedBy=multi-user.target
    
    sudo systemctl daemon-reload
    sudo systemctl enable --now pingtower
    curl -s http://127.0.0.1:8391/healthz
    
  4. Put TLS in front

    The daemon speaks plain HTTP on localhost and expects a reverse proxy to terminate TLS. See Reverse proxy & TLS.

  5. First account, first tenant

    curl -s https://alerts.example.com/v1/signup \
      -H 'Content-Type: application/json' \
      -d '{"email":"[email protected]","password":"…"}'
    # → {"account":{…},"token":"pts_…"}
    
    curl -s https://alerts.example.com/v1/tenants \
      -H "Authorization: Bearer pts_…" \
      -H 'Content-Type: application/json' \
      -d '{"name":"Acme"}'
    

    From here the quickstart applies unchanged, with your domain in place of api.pingtower.com.

Useful flags

pingtower -h lists everything. The ones most boxes set:

FlagDoes
-listenAddress to serve on. Keep it on localhost behind the proxy.
-data-dirWhere the daemon looks for config.yaml, and where SQLite keeps control.db and tenants/<id>/pingtower.db.
-retain-daysHow long retained log rows are kept.
-max-logs-per-sourceA per-source cap on retained rows.
-backup-dirWhere periodic backups are written, readable by the daemon’s user alone. SQLite only; back up PostgreSQL as you would any server.
-secret-keySeals integration credentials, action headers and bodies, webhook signing secrets and the relay credential at rest with AES-256-GCM. Env PINGTOWER_SECRET_KEY. Use 32 or more random characters, such as openssl rand -base64 32 makes. Under 16 the daemon refuses to start with a FATAL line naming the flag; under 32 it starts with a warning. Set it before creating any integration.
-client-ip-headerThe header your reverse proxy sets to the real client address, such as X-Real-IP. Read only from loopback peers. Env PINGTOWER_CLIENT_IP_HEADER. See Reverse proxy.
-log-reset-tokensWrites password-reset tokens to the log for you to hand over, since a box sends no mail. On by default; -log-reset-tokens=false withholds them. Env PINGTOWER_LOG_RESET_TOKENS.
The secret key is one-wayOnce a keyed daemon has sealed a secret, it will not start without the key: it refuses with a FATAL line naming --secret-key rather than serve tenants it cannot open. A different key opens none of the sealed secrets, and every signed webhook, integration and action fails until it is re-created. Keep the key with your backups.

Database

The daemon takes its database from one setting, database.url, in config.yaml in the data directory (or the file --config or PINGTOWER_CONFIG names):

database:
  url: postgres://pingtower:[email protected]/pingtower
  • PostgreSQL (postgres:// or postgresql://): the control plane and every tenant live on that server, each tenant in its own schema. A PostgreSQL URL never falls back to SQLite.
  • SQLite: with no file or no url, the daemon keeps everything in the data directory, with nothing else to install. sqlite:///var/lib/pingtower names that directory explicitly.
  • The URL is read from the file only — there is no flag or environment variable for it — so the password never appears in ps. The daemon refuses a config.yaml that others can read or rewrite; use mode 0600 or 0640.
  • pingtower pair writes a SQLite data directory directly, so it refuses a PostgreSQL box and points you at the running daemon’s pairing flow instead.

Storage

Accounts, tenants, memberships, sessions, API keys and invites are kept apart from each tenant’s sources, templates, retained logs, alerts and project configs. On PostgreSQL each tenant is its own schema; on SQLite, control.db plus one tenants/<tenant_id>/pingtower.db per tenant. Tenant isolation falls out of which one was opened, chosen by the caller’s credential.

Back up PostgreSQL the way you back up any PostgreSQL server. On SQLite, the data directory copies cleanly — SQLite runs in WAL mode with a single writer per file — or let -backup-dir write periodic backups.

Upgrading

Replace the binary and restart the unit. Schema migrations run at startup. Read the release notes on the blog first; a release that needs anything more says so there.

Upgrade the addons agent on each box together with the tower: the tower first, then its agents. An agent older than the tower keeps running every check it knows, but a newer addon reaches it as an unknown addon. The synthetic addon needs addons agent 0.4.0 or later; the tower warns, without refusing the save, when a synthetic check names a source whose agent is older than 0.4.0, or reports no version (every release before 0.4.0), and the sources API reports each source’s agent_version. An agent that runs gRPC checks needs addons agent 0.5.0 or later: on 0.4.0 a gRPC check can hang and stop reporting, and the tower does not warn about it.

Take a backup before you upgrade. Migrations only go forward, and there is no downgrade other than restoring that backup: an older binary starts on a newer database without refusing it, and once Release 1 has added per-project access and custom roles, that widens what project-limited members can reach. Upgrading to Release 1 lists everything that release needs.

Last updated 7 Oct 2026 Report a problem with this page