A hardened, high-performance web hosting pod manager built on Podman. Provision and manage isolated, production-ready site pods from a single web-based management UI — no shell required after initial setup.
Overview · Requirements · Host Install (pdnctl) · Running as a Container · Running the Binary · First Login · Directory Structure · Documentation · License · Support
Each pod is provisioned with nginx as the reverse proxy and optionally Varnish as an in-memory HTTP cache layer in front of nginx. WordPress and PHP sites also include PHP-FPM, MariaDB, and Redis. Node.js and .NET sites include MariaDB and Redis. Static HTML sites get nginx and optionally Varnish only. Reverse Proxy sites route traffic to an upstream URL with no containers of their own.
Supported site types:
| Type | Runtimes Available |
|---|---|
| WordPress | PHP 8.2, 8.3, 8.4, 8.5 |
| PHP | PHP 8.2, 8.3, 8.4, 8.5 |
| Static HTML | nginx only |
| Node.js | Node 22, 24, 25, 26 |
| .NET | .NET 8.0, 9.0, 10.0 |
| Reverse Proxy | Routes to an upstream URL — no pod provisioned |
All sites share a single global SFTP container for file management. The same container also backs a per-site web file manager — browse, upload, download, in-browser text editing, and permission changes scoped to each site's html directory, performed as the site's own user. A global Fail2Ban container monitors SFTP access and automatically bans IPs that repeatedly fail authentication.
The recommended and fully supported deployment method is as a container. The binary option is available for those who prefer to compile and run it directly.
Full documentation — feature guides, configuration reference, the security model, and the complete API reference — lives in the PodNest Knowledge Base. This README covers only what you need to get an instance up and running. See Documentation below for the topic index.
For container deployment (recommended):
- Podman installed and running on the host
- The Podman socket exposed and accessible (see notes below)
- Docker Compose, Podman Compose, or equivalent for compose-based deployments
For binary deployment:
- Go 1.26 or later
gccandmusl-dev(CGO is required for SQLite)- Podman installed and accessible via socket on the host
PodNest communicates with the host Podman daemon through its Unix socket. The socket path varies depending on how Podman is running:
- Root Podman:
/run/podman/podman.sock— this is what the container image expects by default - Rootless Podman:
/run/user/<uid>/podman/podman.sock— this is the binary default when running as a non-root user
Make sure the socket exists and is accessible before starting PodNest. For rootless Podman, socket lingering must be enabled:
loginctl enable-linger $USER
systemctl --user start podman.socketBinding ports 80/443 rootless — when running PodNest rootless and letting it bind the standard web ports directly, lower the unprivileged port floor on the host:
sysctl -w net.ipv4.ip_unprivileged_port_start=80 # persist it: echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-podnest.conf
pdnctl is the host-side manager for PodNest — the recommended way to get an instance running. It installs PodNest as a rootless container under a dedicated user, handles updates and channel switching, and wraps the service lifecycle. All the rootless prerequisites (dedicated user, subuid/subgid ranges, unprivileged port floor, lingering, the Podman socket) are handled for you.
Install it with one command as root:
curl -fsSL https://raw.githubusercontent.com/kpirnie/podnest/main/bootstrap.sh | sudo bashThen set up PodNest. Replace USER with whatever username you would like to utilize, and select a version:
pdnctl install --user USER --version latest|dev|betaThe timezone is pulled from the host, site data lives at /home/<user>/sites, and the install details are stored in /etc/podnest/install.json — every command after install needs zero flags.
Once running, the UI is available at: http://your-host:9000
| Command | Description |
|---|---|
pdnctl install --user <name> --version <latest|dev|beta> |
Fresh rootless setup under a dedicated user |
pdnctl update |
Pull the newest image on the current channel and restart |
pdnctl update --version <latest|dev|beta> |
Switch channels — future updates track the new channel |
pdnctl start / pdnctl stop / pdnctl restart |
Control the PodNest service |
pdnctl status |
Show the install summary and service status |
pdnctl uninstall |
Remove PodNest — keeps the user and site data |
pdnctl uninstall --purge |
Remove PodNest, the dedicated user, and all site data |
All pdnctl commands must be run as root.
Prefer to deploy and manage the container yourself instead of using pdnctl? A pre-built image is published to the GitHub Container Registry — no compilation required.
| Tag | Description |
|---|---|
latest |
Latest stable release — use this for production |
dev |
Tracks the develop branch — use at your own risk |
beta |
Tracks the beta branch — preview features, not production-ready |
ghcr.io/kpirnie/podnest:latest
ghcr.io/kpirnie/podnest:dev
ghcr.io/kpirnie/podnest:beta
A docker-compose-example.yaml is included in the repository as a starting point. Copy and adjust it to your environment:
services:
podnest:
image: ghcr.io/kpirnie/podnest:latest
container_name: podnest
hostname: podnest
restart: unless-stopped
ports:
- 80:80
- 443:443
- 9000:8080
volumes:
- /run/podman/podman.sock:/run/podman/podman.sock:rw
- /your/persistent/path:/opt/podnest:z
tmpfs:
- /tmp
environment:
- TZ=${TZ:-America/New_York}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/login"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10sStart it with:
# with Podman Compose:
podman-compose up -dVolume mount and the :z label — the :z flag on the /opt/podnest mount is a SELinux relabeling option required on systems running SELinux in enforcing mode. It is safe to omit on systems that do not use SELinux.
For production deployments on Linux, running PodNest as a systemd service is recommended over podman-compose. It guarantees the Podman socket exists before the container starts, which prevents a known issue where podman-compose can incorrectly create the socket path as a directory instead of a socket file.
Create /etc/systemd/system/podnest.service:
[Unit]
Description=PodNest Management UI
After=network.target podman.socket
Requires=podman.socket
[Service]
Restart=always
RestartSec=5
TimeoutStartSec=60
ExecStartPre=-/usr/bin/podman rm -f podnest
ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do [ -S /run/podman/podman.sock ] && exit 0; sleep 1; done; exit 1'
ExecStart=/usr/bin/podman run \
--name podnest \
--hostname podnest \
--network host \
-v /run/podman/podman.sock:/run/podman/podman.sock:rw \
-v /home/sites:/opt/podnest:z \
--tmpfs /tmp \
-e TZ=America/New_York \
-e LOG_LEVEL=INFO \
ghcr.io/kpirnie/podnest:latest serve --app-path /opt/podnest --port 9000 --socket /run/podman/podman.sock
ExecStop=/usr/bin/podman stop podnest
[Install]
WantedBy=multi-user.targetReplace the persistent path with the host path where your site data should persist. Enable and start the service:
systemctl daemon-reload
systemctl enable --now podnest.service
systemctl status podnest.serviceA podnest.service example file is included in the repository.
Podman socket warning — a known issue with podman-compose is that if the Podman socket does not exist at the moment the container starts, the mount point is created as a directory instead of a socket file, causing all Podman API calls to fail. The systemd unit avoids this by declaring Requires=podman.socket. If you prefer podman-compose, ensure the socket exists first:
systemctl start podman.socket
ls -la /run/podman/podman.sock # must show srw, not drwx
podman-compose up -dPod auto-recovery after host reboot — when a host running PodNest reboots, PodNest restarts via systemd and, on startup, queries the database for all sites that were running at last shutdown and automatically restarts their pods. Sites whose pods no longer exist are marked as stopped. No manual intervention is required after a reboot.
Container deployment is the recommended approach. The binary path is here for advanced users who prefer it.
git clone https://github.com/kpirnie/podnest.git
cd podnest
CGO_ENABLED=1 GOOS=linux go build \
-ldflags="-s -w -extldflags '-static'" \
-o podnest ./main.goWhen running the binary for the first time, use the init command to set up the database and create your admin account interactively:
./podnest init --app-path /opt/podnestYou will be prompted for username, name, email, phone, and a password. This only needs to be run once. If an admin account already exists, it exits with an error and directs you to the UI.
./podnest serve \
--app-path /opt/podnest \
--port 8080 \
--socket /run/podman/podman.sockBoth init and serve share the following persistent flags:
| Flag | Default | Description |
|---|---|---|
--app-path |
/opt/podnest |
Base path for the database, site configs, and all application data |
--port |
8080 |
Port the management UI listens on |
--socket |
/run/user/<uid>/podman/podman.sock |
Path to the Podman socket |
PodNest also provides a reset command for recovering account access from the host shell:
./podnest reset --app-path /opt/podnestreset interactively resets a user's password and/or clears their TOTP — useful when an admin is locked out. It accepts the same --app-path flag.
There is also a security command for recovering panel access when a security rule has locked you out:
./podnest security --bypass 203.0.113.7 --app-path /opt/podnest--bypass accepts an IP or CIDR and adds it to the security bypass list (skipping all IP/UA/country/ASN/WAF checks). Restart the podnest service for it to take effect.
When the container starts for the first time with no existing database, a default admin account is automatically created:
| Field | Value |
|---|---|
| Username | admin |
| Password | podnest1234@ |
Change this password immediately after your first login. The default credentials are well-known and should never be left in place on a production instance.
If you used podnest init, log in with the credentials you provided during setup. The serve command will only auto-seed the default admin credentials above if no admin account exists at startup.
Once PodNest is running with a persistent volume, the following structure is created at your mounted path (or at --app-path for binary deployments):
/opt/podnest/
├── podnest.db # SQLite database — users, sites, domains, configs, SFTP creds
├── certs/ # TLS certificates (Let's Encrypt and self-signed fallback)
├── logs/ # Proxy access log and WAF log
│ ├── proxy-access.log
│ └── waf.log
├── fail2ban/ # Fail2Ban config and jail data
├── waf/
│ └── crs/ # Downloaded OWASP CRS rule files — auto-updated on startup
├── sftp/ # Global SFTP container config
│ ├── keys/ # Persistent SSH host keys
│ ├── etc-ssh/
│ │ └── sshd_config.d/
│ ├── logs/ # SFTP access logs — watched by Fail2Ban
│ └── users.conf # SFTP user accounts — managed automatically
└── sites/
└── <site-name>/
├── html/ # Web root — your site files go here
├── nginx/
│ ├── nginx.conf
│ ├── conf.d/
│ │ └── site.conf
│ ├── logs/
│ └── cache/
├── php-fpm/ # WordPress and PHP sites only
│ ├── www.conf
│ └── php.ini
├── db/ # All sites except Static HTML and Reverse Proxy
│ └── my.cnf
├── redis/ # All sites except Static HTML and Reverse Proxy
│ └── redis.conf
├── varnish/ # All site types except Reverse Proxy (disabled by default)
│ └── default.vcl
├── backups/ # Restic backup repositories
│ └── local/ # Local restic repo (SFTP accessible, read-only)
└── .env # Auto-generated credentials — do not delete
The .env file inside each site directory holds the auto-generated database and Redis credentials injected into the pod at runtime. Do not delete or manually edit it unless you know exactly what you are doing. The sftp/users.conf file is managed automatically by PodNest — do not edit it manually.
Day-to-day usage, configuration, and the full API reference are documented in the PodNest Knowledge Base. Topic index:
- Managing Sites — creating sites, site actions, container health, cloning, domains
- SFTP Access — global SFTP container, directory access, Fail2Ban integration
- Built-in Reverse Proxy — how it works, SSL / Let's Encrypt, admin domain, Reverse Proxy site type
- Redirects — per-site redirect rules
- Site Configurations — nginx, PHP, MariaDB, Redis, Varnish, resetting a config
- Security Rules — IP rules, User-Agent rules, country blocking, ASN blocking, ASN lookup, global vs per-site, import/export
- WAF — CRS rule management, global settings, per-site overrides, exclusions, WAF log
- Live Logs — per-site and global log streams
- WP-CLI Terminal — browser terminal for WordPress sites
- phpMyAdmin — database management
- Stats & Traffic — dashboard and per-site stats
- Resource Monitoring — host resource watcher and throttling
- User Management — users, roles, two-factor authentication
- Settings — general, trusted proxies, backup schedule, S3, notifications, resources, export/import
- Notifications — channels and per-user opt-in
- Cron Jobs — per-site scheduled jobs
- Backup & Restore — what gets backed up, destinations, manual/scheduled backups, restore, import
- Security — login rate limiting, 2FA, sessions, CSRF, headers, WebSocket security, API authorization
- Updating — update notifications and how to update
- API Reference — complete REST and WebSocket API
PodNest is licensed under the MIT License. See the LICENSE file in the repository for the full license text.
This project is provided as-is through GitHub. Paid support is available for those who need hands-on help with setup, configuration, customization, or troubleshooting. Reach out through https://kevinpirnie.com/about-kevin-pirnie/lets-talk/ to inquire about support options.