Skip to main content

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:

RequirementDetails
Operating SystemLinux server (Ubuntu 22.04+, Debian 12+, or any Docker-compatible distribution)
DockerDocker Engine 24+ and Docker Compose v2+
DatabasePostgreSQL 15+ (the official Docker install provisions PostgreSQL automatically)
RAMMinimum 2 GB
Disk SpaceMinimum 10 GB (more recommended for database growth)
Ports3000 (ProxCenter web interface)
NetworkConnectivity 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
info

ProxCenter uses PostgreSQL for all supported production deployments. Schema migrations run automatically on startup.

Enterprise: a release that adds a setting needs the installer, not just a pull

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.

Upgrading from SQLite

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.

warning

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:

VariableDefaultMeaning
SESSION_IDLE_TIMEOUT43200 (12 hours)Time without activity after which the session expires
SESSION_ABSOLUTE_TIMEOUT604800 (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.

info

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.

warning

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.

VariableDefaultMeaning
PVE_TIMEOUT_MS8000 (8 seconds)Budget for a regular Proxmox API call
PVE_SLOW_READ_TIMEOUT_MS30000 (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.

VariableDefaultMeaning
GHCR_TOKENunsetRegistry token used to pull the VDDK package
PROXCENTER_VDDK_PACKAGEthe shipped packageOverride only to pin another package
PROXCENTER_VDDK_TAGthe shipped tagOverride only to pin another tag
V2V_ROOT_CHOICE_TIMEOUT_MS7200000 (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_uri values sent to your identity provider,
  • set the Secure flag on session and CSRF cookies (auto-enabled when the URL starts with https://).

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

  1. Edit /opt/proxcenter/.env and replace the NEXTAUTH_URL line with the public FQDN that users will reach in their browser:

    NEXTAUTH_URL=https://proxcenter.example.com
  2. 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.

tip

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-Agent blocks. 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 state and code values contain substrings that trip these rules. Match such patterns against POST bodies only, or exempt the auth callback path.
warning

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:

SourceUsed 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.comGoogle Fonts stylesheet and font files
https://*.tile.openstreetmap.org, https://*.basemaps.cartocdn.comMap tiles on the Topology and datacenter maps
https://flagcdn.comCountry flags on the maps
img-src data: blob:Console live-preview thumbnails, and PNG / Excel exports
https://nominatim.openstreetmap.orgReverse geocoding when adding or editing a connection with coordinates
https://api.github.comRelease 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), so connect-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 example wss://proxcenter.example.com) to connect-src.
  • frame-ancestors 'none' is the modern equivalent of X-Frame-Options: DENY and 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.
Roll it out safely

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 8006 and Proxmox Backup Server on port 8007.
  • 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:

ServicePurpose
FrontendWeb UI and API routes exposed on port 3000
OrchestratorBackground jobs, Proxmox polling, alerts, reports and automation
PostgreSQLApplication database for users, settings, tenants, vDCs and operational metadata
WeasyPrintPDF 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:

  1. Create your admin account on first login
  2. Connect your Proxmox infrastructure -- Add your PVE and PBS instances
  3. Activate your license (optional) -- If you have an Enterprise license key, go to Settings > License and enter it
  4. 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
  5. 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.