Installation
ProxCenter is deployed self-hosted on your own infrastructure using Docker.
Self-Hosted with Docker
Deploy ProxCenter on your own server for full control over your data and configuration.
Prerequisites
Before you begin, make sure you have:
| Requirement | Details |
|---|---|
| Operating System | Linux server (Ubuntu 22.04+, Debian 12+, or any Docker-compatible distribution) |
| Docker | Docker Engine 24+ and Docker Compose v2+ |
| Database | PostgreSQL 15+ (the official Docker install provisions PostgreSQL automatically) |
| RAM | Minimum 2 GB |
| Disk Space | Minimum 10 GB (more recommended for database growth) |
| Ports | 3000 (ProxCenter web interface) |
| Network | Connectivity to your Proxmox nodes on port 8006 (PVE) and/or 8007 (PBS) |
Step 1: Run the installer
Get the current installation command from https://proxcenter.io, then run it on the server that will host ProxCenter:
curl -fsSL https://proxcenter.io/install/community | sudo bash
The installer provisions the Docker Compose stack, persistent volumes, PostgreSQL, and the ProxCenter application services.
Step 2: Access the dashboard
Open your browser and navigate to:
http://your-server-ip:3000
On the first launch, you will be prompted to create your admin account. This account has full access to ProxCenter and cannot be deleted.
The screen that creates it is reachable without authentication, because there is no account to authenticate against yet. That window closes for good as soon as one account exists, and it is rate limited to 10 attempts per minute for the whole instance. If the first boot happens on a network other people can reach, close the window with a shared secret first: see First-run setup secret.
Updating ProxCenter
For v1.4.x and later, update an existing PostgreSQL-based deployment with:
docker compose pull
docker compose up -d
ProxCenter uses PostgreSQL for all supported production deployments. Schema migrations run automatically on startup.
docker compose pull updates the images, never docker-compose.yml, and the shipped compose files pass their environment through an explicit environment: list. A setting introduced by a newer release therefore never reaches the container on a pull-only update, whatever /opt/proxcenter/.env holds. In v1.4.8 that matters for one setting in particular: without GHCR_TOKEN, the Prepare the node action of warm migration is not offered at all; the five others are optional tuning whose defaults apply either way.
Re-run the standard install command with your installation token:
curl -fsSL https://proxcenter.io/install/enterprise | sudo bash -s -- --token YOUR_TOKEN
It refreshes docker-compose.yml, writes GHCR_TOKEN into .env, preserves your secrets, your customised NEXTAUTH_URL, APP_URL and license key, leaves the data untouched, then pulls and restarts. Only the containers whose environment changed are recreated. An installation that pinned a specific version in .env should pass it again with --version, since a re-run otherwise resets it to latest.
Check the result. This prints the variable names only, never their values, and no output means the compose file predates the release:
cd /opt/proxcenter && docker compose config | grep -oE 'GHCR_TOKEN|PVE_TIMEOUT_MS|PVE_SLOW_READ_TIMEOUT_MS|PROXCENTER_VDDK_PACKAGE|PROXCENTER_VDDK_TAG|V2V_ROOT_CHOICE_TIMEOUT_MS' | sort -u
If you would rather not run the installer, copy the environment: block of the frontend service from the shipped compose file into yours and add GHCR_TOKEN to .env yourself, then docker compose up -d.
A Community install needs none of this: its images are public and warm migration is an Enterprise feature, so it receives the two Proxmox budgets only. Each setting is described in Optional Environment Settings.
SQLite support was removed in v1.4.0. Existing deployments from v1.3.x or earlier need a planned cutover to a PostgreSQL-backed v1.4 stack. See Upgrade to v1.4 before updating image tags.
Optional Environment Settings
Nine optional settings tune authentication, the Proxmox request budgets and the warm migration runtime. All of them are listed in the environment: block of the frontend service in the community, Enterprise and HA compose files shipped with the version that introduced them, empty by default, so you never have to touch a compose file: declare the ones you want in /opt/proxcenter/.env, next to the compose file, for example
SESSION_IDLE_TIMEOUT=28800
SESSION_ABSOLUTE_TIMEOUT=86400
PROXCENTER_SETUP_TOKEN=a-long-random-string
then recreate the container so the new values are read:
cd /opt/proxcenter && docker compose up -d
An empty value is treated exactly like an unset one, which is why the defaults survive an untouched .env.
These compose files pass their environment through an explicit list, not through env_file. If you upgraded by changing image tags only and kept a compose file from an earlier version, these variables never reach the container whatever you put in .env. Refresh the compose file from the current installer, or add the missing lines to the frontend service's environment: block yourself.
Session lifetimes
A ProxCenter session is a row in the database, and that row is what actually grants access. Two settings bound how long it may live, both expressed in seconds:
| Variable | Default | Meaning |
|---|---|---|
SESSION_IDLE_TIMEOUT | 43200 (12 hours) | Time without activity after which the session expires |
SESSION_ABSOLUTE_TIMEOUT | 604800 (7 days) | Maximum lifetime of a session, whatever the activity |
The session cookie's own lifetime is derived from SESSION_ABSOLUTE_TIMEOUT, so the cookie can never outlive the database row it points at.
A value that is not a positive number of seconds falls back to the default, never to "no limit". A typo therefore weakens nothing.
Users can review and revoke their own sessions, and administrators can revoke another user's, from the interface. See Sessions.
First-run setup secret
PROXCENTER_SETUP_TOKEN turns the first-run window into a shared-secret one. Unset (the default) keeps the historical behaviour: any caller who reaches the instance before the first account exists can create it.
When the variable is set, the setup request must carry the same value in an x-setup-token header, compared in constant time. Anything else is refused with HTTP 403.
The browser setup form does not send that header. Setting PROXCENTER_SETUP_TOKEN means the first administrator has to be created with a direct API call:
curl -fsS -X POST https://proxcenter.example.com/api/v1/auth/setup \
-H 'Content-Type: application/json' \
-H 'x-setup-token: a-long-random-string' \
-d '{"email":"[email protected]","password":"a-strong-password","name":"Admin"}'
Once that call succeeds, sign in normally and remove the variable if you like: the endpoint refuses every later call anyway.
Whether or not the secret is set, the endpoint is bounded three ways: 10 attempts per minute counted for the whole instance rather than per caller (HTTP 429 with a Retry-After header beyond that), an HTTP 400 once any account exists, and a serializable transaction around the "no account exists" test so that two concurrent bootstraps cannot both create an administrator. The loser of that race gets the same HTTP 400.
Proxmox request budgets
Two settings bound how long ProxCenter waits for a Proxmox answer, both expressed in milliseconds. They were introduced in v1.4.8 and reach every edition.
| Variable | Default | Meaning |
|---|---|---|
PVE_TIMEOUT_MS | 8000 (8 seconds) | Budget for a regular Proxmox API call |
PVE_SLOW_READ_TIMEOUT_MS | 30000 (30 seconds) | Budget for the reads that enumerate every storage of a datacenter |
Raise the slow one on a datacenter declaring many Proxmox Backup Server storages. Such a listing can need 20 seconds or more to answer, and it is what fills the Target Storage field of the migrate dialog, which used to come back empty when the answer arrived too late. A response timeout on one of these reads is not treated like an unreachable host, so it no longer counts towards the connection failover threshold.
Warm migration runtime
Broadcom closed the public VDDK download in August 2026, so the plugin the warm migration path needs ships as a private registry package that ProxCenter installs on the target node itself. These four settings were introduced in v1.4.8 and reach the Enterprise and HA stacks only, since migration is an Enterprise feature.
| Variable | Default | Meaning |
|---|---|---|
GHCR_TOKEN | unset | Registry token used to pull the VDDK package |
PROXCENTER_VDDK_PACKAGE | the shipped package | Override only to pin another package |
PROXCENTER_VDDK_TAG | the shipped tag | Override only to pin another tag |
V2V_ROOT_CHOICE_TIMEOUT_MS | 7200000 (2 hours) | How long a cold migration waits for an operator to pick the root filesystem before it gives the node temporary storage back |
The Enterprise installer already writes GHCR_TOKEN to /opt/proxcenter/.env. Without that value reaching the frontend container, the Prepare the node action of the migrate dialog reports no token and stays disabled, and the node has to be prepared by hand instead. See Migration.
Reverse Proxy and Public URL
If you expose ProxCenter behind a reverse proxy (nginx, Traefik, Caddy, HAProxy), you must tell ProxCenter what its public URL is. The frontend uses NEXTAUTH_URL from /opt/proxcenter/.env to:
- build OIDC / SSO
redirect_urivalues sent to your identity provider, - set the
Secureflag on session and CSRF cookies (auto-enabled when the URL starts withhttps://).
The installer writes NEXTAUTH_URL based on the address used at install time. If that address was the server's internal IP, the variable will hold the IP and external integrations (DUO, Okta, Entra ID, Authentik, etc.) will receive the IP as the callback URL.
Set the public URL
-
Edit
/opt/proxcenter/.envand replace theNEXTAUTH_URLline with the public FQDN that users will reach in their browser:NEXTAUTH_URL=https://proxcenter.example.com -
Restart the stack so the new value is picked up:
cd /opt/proxcenter && docker compose up -d
Nginx example
server {
listen 443 ssl http2;
server_name proxcenter.example.com;
# ssl_certificate / ssl_certificate_key ...
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
The Upgrade / Connection headers are required for the noVNC and xterm.js console WebSocket streams to flow through the proxy.
Always set NEXTAUTH_URL before configuring OIDC. It determines the redirect URI ProxCenter sends to your identity provider, which is always ${NEXTAUTH_URL}/api/auth/callback/oidc. If you change the URL afterwards, you also have to update the Redirect URI in your identity provider so they match exactly (scheme, host, port, no trailing slash mismatch).
Rate limiting and request filtering
ProxCenter is a single-page app. On a cold load or a hard refresh (Ctrl+Shift+R) the browser requests a large burst of JavaScript chunks from /_next/static/, often more than a hundred files at once. Web Application Firewalls and rate limiters (CrowdSec, nginx limit_req, ModSecurity, Cloudflare) frequently mistake this burst for abuse and answer with HTTP 429 or 403. When that happens the app never finishes loading and users get stuck on an endless spinner, while the same deployment works fine on the local network where no proxy sits in front.
Two rules keep ProxCenter working behind a security proxy.
1. Do not rate-limit static assets. Exempt /_next/static/ from any request-rate limiting. These files are content-hashed and immutable, so they are safe to serve without a cap.
# Exempt the static chunk burst from rate limiting (CrowdSec, limit_req, ...)
location /_next/static/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
# no limit_req / appsec rule here
}
Also make sure the proxy serves over HTTP/2 (listen 443 ssl http2; in the example above), which multiplexes the burst over a single connection instead of opening dozens of them.
2. Do not apply generic WAF filtering to the sign-in flow. The OIDC / SSO sign-in redirects the browser to your identity provider and back to ProxCenter's callback with long, random state and code values in the query string. Aggressive WAF rules routinely block these legitimate requests. If sign-in hangs or loops only when accessed through the proxy, relax these in particular:
- Empty
User-Agentblocks. Some clients and intermediate hops send no user agent. Returning 403 on an empty UA breaks legitimate logins. - HTTP method allow-lists that are too narrow. Allow at least
GET,HEAD,POST,PUT,DELETE,OPTIONS,PATCH. - Generic "SQL injection" or "dangerous character" patterns matched against the query string. OIDC
stateandcodevalues contain substrings that trip these rules. Match such patterns against POST bodies only, or exempt the auth callback path.
If login works on the local network but hangs or loops only when users come through the reverse proxy, the cause is almost always the proxy filtering the OIDC callback or rate-limiting the static chunk burst, not ProxCenter itself. ProxCenter does no application-level rate limiting.
Security headers
ProxCenter already sends a baseline set of response headers from the application itself: X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin and Permissions-Policy: camera=(), microphone=(), geolocation=(). You do not need to add those at the proxy. If you do set X-Frame-Options at the proxy (for example a stricter DENY), add proxy_hide_header X-Frame-Options; first to avoid a duplicate header.
The one header ProxCenter does not set by default is a Content Security Policy, because the correct value depends on which external resources your deployment allows. The policy below loads everything the UI needs and nothing more. Add it inside the server { ... } block next to the location / from the example above:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: https://*.tile.openstreetmap.org https://*.basemaps.cartocdn.com https://flagcdn.com; connect-src 'self' https://nominatim.openstreetmap.org https://api.github.com; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';" always;
What each non-'self' source is for:
| Source | Used by |
|---|---|
script-src 'unsafe-inline' | Next.js inline hydration scripts, and the inline scripts in the noVNC and SPICE console pages |
style-src 'unsafe-inline' | MUI / Emotion inject <style> tags at runtime |
https://fonts.googleapis.com, https://fonts.gstatic.com | Google Fonts stylesheet and font files |
https://*.tile.openstreetmap.org, https://*.basemaps.cartocdn.com | Map tiles on the Topology and datacenter maps |
https://flagcdn.com | Country flags on the maps |
img-src data: blob: | Console live-preview thumbnails, and PNG / Excel exports |
https://nominatim.openstreetmap.org | Reverse geocoding when adding or editing a connection with coordinates |
https://api.github.com | Release notes shown in the About dialog |
A few things worth knowing:
- The VNC, SPICE and serial consoles connect over
wss://to the same origin (proxied through nginx), soconnect-src 'self'already covers them. There is no direct browser-to-node connection. If a console fails to connect on an older browser, add your explicit origin (for examplewss://proxcenter.example.com) toconnect-src. frame-ancestors 'none'is the modern equivalent ofX-Frame-Options: DENYand is safe to keep: the consoles open in popup windows, never in an iframe.script-src 'unsafe-inline'is currently required: Next.js emits inline bootstrap scripts to hydrate the UI, and the console pages carry their connection logic inline.'unsafe-eval'is not needed by the production image. A nonce-based policy that would let you drop'unsafe-inline'is on the roadmap.
Deploy the policy as Content-Security-Policy-Report-Only first, browse the consoles, the Topology and datacenter maps, and the About dialog, then watch the browser DevTools console for violations. Once it is clean, rename the header to Content-Security-Policy to enforce it.
Production Checklist
Before exposing ProxCenter to users, verify:
- The server can reach Proxmox VE on port
8006and Proxmox Backup Server on port8007. - PostgreSQL data is stored on a persistent volume or managed database.
- Docker logs are collected by your monitoring system.
- Backups cover the PostgreSQL volume and ProxCenter configuration.
- A reverse proxy handles TLS if the web UI is exposed outside a trusted network.
- A Content Security Policy is set at the reverse proxy (see Security headers).
- The first admin account is stored in your access management process.
- Session lifetimes match your own policy, or the defaults are accepted knowingly (see Session lifetimes).
- Enterprise deployments have the license key available during bootstrap.
Service Layout
A standard Docker deployment contains:
| Service | Purpose |
|---|---|
| Frontend | Web UI and API routes exposed on port 3000 |
| Orchestrator | Background jobs, Proxmox polling, alerts, reports and automation |
| PostgreSQL | Application database for users, settings, tenants, vDCs and operational metadata |
| WeasyPrint | PDF rendering sidecar for report generation |
Uninstalling
To stop and remove ProxCenter (data is preserved in the Docker volume):
docker compose down
To remove everything including data:
docker compose down -v
Post-Installation Steps
After installation:
- Create your admin account on first login
- Connect your Proxmox infrastructure -- Add your PVE and PBS instances
- Activate your license (optional) -- If you have an Enterprise license key, go to Settings > License and enter it
- Create tenants (Enterprise, optional) -- For MSP / IaaS deployments, assign whole clusters to MSP tenants or vDCs to IaaS tenants, with quotas, networks, and backup isolation
- Explore the dashboard -- Head to First Steps for a guided tour
Next Steps
Once ProxCenter is up and running, the next step is to connect your Proxmox infrastructure.