Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AZhur771/pivpn-web/llms.txt

Use this file to discover all available pages before exploring further.

PiVPN Web is configured entirely through environment variables passed to the Docker container at startup. There are no configuration files to edit — set the variables you need in your docker run command or docker-compose.yml and the application picks them up automatically. The sections below cover every supported variable, user role semantics, TLS setup, and session secret behaviour.
User accounts are seeded from ADMIN_USER/ADMIN_PASSWORD, VIEWER_USER/VIEWER_PASSWORD, and TECH_USER/TECH_PASSWORD by TypeORM migrations that run once on first startup. Changing these environment variables after the database has been initialised will not update the stored passwords — the SQLite database must be deleted and the container restarted to re-run the migrations with new credentials.

Environment variables

Server

PORT
number
default:"3001"
The TCP port the Express.js web server listens on inside the container. Map this to a host port with Docker’s -p flag (e.g. -p 8080:3001 to expose the dashboard on port 8080 of the host).
IS_SECURE
string
Set to any non-empty value (e.g. "true") to mark the session cookie as secure: true. This is required when PiVPN Web is served over HTTPS — the browser will refuse to send a secure cookie over plain HTTP. Leave unset for HTTP-only deployments. See the TLS / HTTPS section for details.Unlike most other variables, IS_SECURE is read directly from process.env.IS_SECURE inside Server.ts and is not exported from config.ts.
SECRET
string
The secret used to sign the express-session cookie. If not provided, a random value is generated at startup. See the Session secret section for the implications of leaving this unset.

SSH connection

SSH_HOST
string
required
IP address or hostname of the PiVPN WireGuard host the container will connect to over SSH. Every WireGuard operation is executed remotely via this connection. If PiVPN runs on the same physical machine as Docker, use the host’s LAN IP or host.docker.internal (see the Quickstart for details).
SSH_PORT
number
default:"22"
The SSH port on the target host. Override only if your PiVPN host runs SSH on a non-standard port.
SSH_USER
string
required
The Linux username used to authenticate the SSH session. This user must have permission to run pivpn commands on the target host (typically the user who owns the PiVPN installation).
SSH_PASSWORD
string
required
The password for the SSH user specified in SSH_USER. The connection is established using password authentication via the node-ssh library.

Admin account

ADMIN_USER
string
required
The login username for the primary admin account created at first startup. Admin accounts have full access to all dashboard features including creating, deleting, enabling, and disabling WireGuard clients.
ADMIN_PASSWORD
string
required
The password for the admin account. Stored as a bcryptjs hash (10 salt rounds) in the SQLite database — the plaintext value is never persisted.

Viewer account

VIEWER_USER, VIEWER_PASSWORD, TECH_USER, and TECH_PASSWORD are not listed in the original README environment variable table. They are defined in config.ts and seeded by separate TypeORM migrations added after the initial release.
VIEWER_USER
string
The login username for the read-only viewer account seeded alongside the admin account on first startup. The TypeORM migration that creates this account runs unconditionally — if VIEWER_USER is not set, the record is inserted with a null login. Set this variable to a meaningful username so the viewer account is usable. See User roles for what a viewer can and cannot do.
VIEWER_PASSWORD
string
The password for the viewer account. Should always be set alongside VIEWER_USER. Stored as a bcryptjs hash in the same migration that creates the admin account.

Tech account

TECH_USER
string
The login username for a secondary admin-level account seeded by a separate TypeORM migration at first startup. Like the viewer account, the migration runs unconditionally — if TECH_USER is not set, the record is inserted with a null login. Set this variable to a meaningful username so the tech account is usable. See User roles for details.
TECH_PASSWORD
string
The password for the tech account. Should always be set alongside TECH_USER. Stored as a bcryptjs hash (10 salt rounds) in the SQLite database.

User roles

PiVPN Web has two distinct privilege levels, stored in the admin boolean column of the user table. Admin (admin = true) Full access to all API endpoints. Admin accounts can list clients, create new peers, delete peers, enable/disable peers, retrieve QR codes, and download configuration files. Both ADMIN_USER and TECH_USER are seeded as admin-level accounts. Viewer (admin = false) Read-only access. Viewer accounts can list clients, view connection status, retrieve QR codes, and download configuration files. They cannot create, delete, enable, or disable clients — any attempt returns 403 Not enough rights. VIEWER_USER is seeded as a viewer account. The privilege check is enforced server-side in Server.ts via validateSession(req.session, true), which verifies the isAdmin flag stored in the session before allowing any write operation.
Account variableRoleCan write?
ADMIN_USERAdmin✅ Yes
TECH_USERAdmin✅ Yes
VIEWER_USERViewer❌ No

TLS / HTTPS

PiVPN Web does not handle TLS termination itself. To serve the dashboard over HTTPS:
  1. Place the container behind a reverse proxy such as nginx or Caddy that handles the TLS certificate and forwards requests to the container on port 3001.
  2. Set IS_SECURE to any non-empty value when starting the container. This causes the session cookie to be sent with secure: true, which prevents the browser from transmitting it over unencrypted HTTP connections.
When IS_SECURE is set, the session cookie is configured as:
cookie: {
  sameSite: 'strict',
  httpOnly: true,
  secure: true,   // set by IS_SECURE
}
Do not set IS_SECURE unless PiVPN Web is genuinely being served over HTTPS. With secure: true on the cookie and an HTTP-only deployment, users will be unable to log in because the browser will never send the session cookie.
A minimal Caddy reverse-proxy configuration that terminates TLS and forwards to a locally running container looks like this:
vpn.example.com {
  reverse_proxy localhost:3001
}

Session secret

The SECRET environment variable controls the signing key for express-session cookies. When not set, the server generates a random value at startup:
secret: process.env.SECRET ?? String(Math.random()),
This has one important consequence: every container restart invalidates all existing sessions, because the signing key changes. Users will be logged out whenever the container is restarted or updated. To preserve sessions across restarts, set a stable SECRET:
docker run -d \
  -e SECRET=your-long-random-secret-string \
  # ... other variables
  andrew771/pivpn-web
Generate a suitable value with:
openssl rand -hex 32
Even with a stable SECRET, sessions are persisted to the SQLite database via typeorm-store. If the database file is not mounted as a Docker volume, session data will still be lost when the container is recreated. Mount the database directory as a named volume to achieve full persistence.

Build docs developers (and LLMs) love