Installation
Requirements
- Linux, macOS, or Windows host with Docker or Podman running
docker compose(v2) available inPATHif you plan to use Paste & Deploy or Git Deployment's compose mode — DockPod checks for it on startup and warns if it's missinggitavailable inPATHfor Git Deployment (clone/fetch on every deploy)- No database server to install — DockPod uses an embedded pure-Go SQLite driver (no CGO)
Server size
DockPod itself is deliberately lightweight: it is a single Go binary with an embedded SQLite database and typically uses less than 30MB of RAM. Most server capacity is therefore available to Docker and the applications you deploy. The actual requirement is driven by those containers, especially databases and source builds, rather than by the DockPod panel.
| Server | Suitable for |
|---|---|
| 1 vCPU, 1GB RAM, 10GB free disk | Minimum for DockPod, Docker, and one or two lightweight containers. The installer adds swap automatically when none is active. |
| 2 vCPU, 2–4GB RAM, 20GB+ disk | Recommended starting point for small production apps, a database, and occasional builds. |
| 4+ vCPU, 8GB+ RAM, 40GB+ disk | Multiple applications, databases, or regular Docker/Nixpacks/Railpack builds. |
Disk usage can grow much faster than DockPod's own footprint because Docker keeps image layers, build cache, volumes, and container logs. The installer rotates Docker's default json-file logs at 10MB with five files retained, but application data, images, and build cache still need their own headroom.
Quick Install (Linux)
curl -fsSL https://dockpod.io/install.sh | bashThis installs the dockpod binary to /usr/local/bin, sets up a systemd service (enabled and started immediately), and — on Ubuntu/Debian (apt) or RHEL/CentOS/Rocky/Alma/Fedora (dnf/yum) — checks for and installs any missing dependencies first:
- git, if missing
- Docker, via the official convenience script, but only if neither Docker nor Podman is already present — a host you've deliberately set up with Podman is left alone
- docker compose v2 plugin, if the
dockerCLI is present but the plugin isn't - Memory-aware host tuning — creates swap only when none is active (with a 1GB free-disk reserve), adjusts
vm.swappiness,vm.vfs_cache_pressure, andnofile/nproclimits based on physical RAM - Docker daemon defaults — enables live restore and rotates
json-filelogs at 10MB with 5 retained files; existing validdaemon.jsonsettings are preserved during the merge - Nixpacks and Railpack, used by Git Deployment's zero-config build types — installed via each tool's own official install script. A failure here (e.g. no internet access) is a warning, not a fatal error; that build type just shows as unavailable in the deploy-mode picker until installed manually
On an unrecognized distro (anything without apt/dnf/yum), the dependency checks are skipped with a warning rather than failing the install — install Docker/Podman, docker compose, git, and (optionally) Nixpacks/Railpack yourself first.
Re-running the script plain (no flags) is safe — each dependency check is a no-op if already satisfied, and it always re-downloads and reinstalls the dockpod binary itself. Note that it does not restart the running service on its own: systemctl enable --now is a no-op start on a unit that's already active, so the new binary sits on disk until the next restart (sudo systemctl restart dockpod, or reboot) — unless you use --update below, which handles that for you.
Updating
From the panel
Settings → General → About shows the version you're on and offers an update button when a newer release exists. Confirm it and DockPod updates itself: the panel is covered while it works, and the page reloads on its own once the new version is serving.
There's nothing different about this path — it runs the same install.sh --update below, from a copy embedded in the binary, so neither can drift from the other. Everything said about --update on this page applies equally to the button, including the firewall caveat.
The button only appears when DockPod can actually do it: running as root, under systemd, as the active dockpod unit. If any of that isn't true — a container, a manually-run binary, a non-systemd host — the panel shows the command below to copy instead, rather than offering something that would fail.
From the command line
curl -fsSL https://dockpod.io/install.sh | bash -s -- --update--update skips the OS-level setup (dependency installs, fail2ban, firewall — assumed already done on an existing install) and just re-downloads the binary and restarts the dockpod service, so the new version takes effect immediately. This is the fast path for picking up a new release; the plain command above is really a first-install flow.
Because --update skips firewall setup entirely, it won't retroactively apply firewall-related fixes added after your install — see --docker-firewall below for that specific case.
Pin a specific version instead of the latest release by adding it after --update:
curl -fsSL https://dockpod.io/install.sh | bash -s -- --update v1.2.3(A version can be pinned the same way on a first install too — sh -s -- v1.2.3, without --update.)
Firewall & fail2ban
On the same recognized distros (apt/dnf/yum), the installer also hardens the host by default:
- fail2ban — installed and enabled with
[sshd],[sshd-ddos], and[recidive]jails (24h ban after 3 failed SSH attempts within 10 minutes, escalating for repeat offenders). - Firewall —
ufwon Debian/Ubuntu,firewalldon RHEL-family, enabled with a default-deny-incoming policy. Before enabling, it opens exactly five ports: SSH, 80, 443, 8080 (panel), and 8090 (MCP).
The SSH port is auto-detected from /etc/ssh/sshd_config rather than assumed to be 22 — if you've moved SSH to a custom port, the firewall rule follows it, so enabling default deny incoming doesn't lock you out.
If you override --addr/--mcp-addr away from their :8080/:8090 defaults, the firewall rules won't automatically follow — open the new ports manually (ufw allow <port>/tcp or firewall-cmd --permanent --add-port=<port>/tcp && firewall-cmd --reload).
On an unrecognized distro, both are skipped with a warning, same as the dependency checks above.
Docker-published ports vs. the firewall
Docker publishes container ports through its own NAT/forwarding path, ahead of where ufw/firewalld's normal rules apply — so a container port published without a domain/reverse-proxy in front of it (e.g. -p 5432:5432 in a compose file) could be reachable from the public internet even with the firewall's default-deny policy enabled above. The installer closes this by hooking into the DOCKER-USER chain (the standard, conntrack-based approach — kept inline rather than fetched from a third-party script) so only the reverse proxy's routed 80/443 stay reachable; anything else a workload publishes is dropped and logged.
This protection was added in v1.8.0. A fresh install on v1.8.0 or later already has it — nothing to do. If your install predates v1.8.0 and you've been updating with --update since, you won't have it: update mode intentionally skips all firewall/OS-level setup, not just this piece, so it never retroactively applies. Apply it on its own, without touching anything else (no package installs, no binary swap):
curl -fsSL https://dockpod.io/install.sh | bash -s -- --docker-firewallSafe to re-run — it reports whether it actually changed anything or the host was already current:
==> Docker/UFW forwarding protection already configured
==> Already patched — nothing to change.A plain (non---update) re-run of the full installer applies it too, alongside redoing everything else it already checks idempotently.
Exposing a Published Port to the Public Internet
Sometimes the block above is exactly what you don't want for one specific port — e.g. a provisioned database's Accessible from public toggle, meant to be reachable from outside. This protection blocks that too, same as an accidental one, so opening it back up needs its own rule.
On v1.9.0 or later, do this from the panel instead of the CLI: Settings → Security → Firewall → add a rule with Routed enabled. It applies the correct ufw route allow/DOCKER-USER direct-rule for you (whichever the host uses) and tracks it so it can be removed the same way later — see Security for details.
Pre-v1.9.0 / CLI fallback
A plain ufw allow <port>/tcp or firewall-cmd --add-port=<port>/tcp will not work here — those only affect traffic destined for the host itself. Docker-published ports arrive through the NAT/forward path instead, so the rule has to target that path specifically:
ufw:
sudo ufw route allow proto tcp to any port <PORT>firewalld:
sudo firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 0 -p tcp --dport <PORT> -m conntrack --ctstate NEW -j RETURN
sudo firewall-cmd --reloadTo revoke later, repeat the same command with ufw route delete allow ... or --remove-rule in place of --add-rule.
This exposes the port with no TLS and no DockPod-level access control — anyone who reaches it authenticates with nothing but the generated database password. Prefer linking the database to an app instead (dockpod_database_link if you're using Terraform, or the equivalent in the web UI) — that reaches it over the container network with no public exposure at all — and only open the port directly when external access is genuinely required.
Running the Binary Directly
./dockpod --addr :8080 --data-dir /var/lib/dockpodFlags & Environment Variables
Every flag has a matching environment variable, useful for running under systemd or in a container.
| Flag | Env var | Default | Description |
|---|---|---|---|
--addr | DOCKPOD_ADDR | :8080 | Main web UI + REST API listen address |
--mcp-addr | DOCKPOD_MCP_ADDR | :8090 | MCP server listen address (separate port — see MCP Setup) |
--data-dir | DOCKPOD_DATA_DIR | ./data | Where the SQLite DB, git clones, and SSH keys are stored |
--socket | DOCKPOD_SOCKET | (auto-detect) | Explicit Docker/Podman socket path |
--jwt-secret | DOCKPOD_JWT_SECRET | (auto-generated) | JWT signing secret — generated once and persisted in the DB if not set |
Socket Auto-detection
If --socket isn't set, DockPod checks, in order:
/var/run/docker.sock/run/podman/podman.sock
Podman exposes a Docker-compatible API on its socket, so the same client code works against either engine unmodified.
Podman Compatibility
Tested end-to-end against a real Podman machine (Podman 6.0.0, rootless, API version 1.44 negotiated automatically):
- ✅ Container list / start / stop / restart / remove
- ✅ Image list / pull (streamed progress) / remove / prune
- ✅ Live memory updates (Optimize's Memory, both Git Deployment apps and Container Detail) — confirmed the change actually lands at the cgroup level inside the container (
/sys/fs/cgroup/memory.max), not just accepted by the API and silently ignored - ✅ Replica creation (Optimize's Scale) via
PublishAllPorts - ✅ Compose-based deploys (Paste & Deploy, Git Deployment compose mode) — see the
DOCKER_HOSTnote below
A real bug surfaced during this testing and has since been fixed: image pulls returned 200 OK with an empty body and silently pulled nothing. The cause was DockPod-side — a stream was closed before it had been fully read — not a Podman incompatibility; raw calls to Podman's API worked correctly throughout. Docker didn't expose the bug because dockerd keeps pulling in the background even after the client connection drops; Podman ties the pull to the connection staying open, which is what made it visible.
A second bug surfaced testing compose-based deploys specifically: DockPod shells out to the docker compose CLI for these (the compose plugin isn't exposed via the daemon API), and that subprocess had no DOCKER_HOST set, so it fell back to its own default socket detection instead of necessarily matching whichever socket DockPod's own client connected to. On a host with both Docker and Podman present, or with DOCKER_HOST already set to something else in the ambient environment, this could silently deploy to the wrong engine. Fixed by explicitly pinning the subprocess's DOCKER_HOST to the exact socket DockPod itself resolved — confirmed via a real Paste & Deploy compose stack landing in Podman specifically (absent from a separately-running real Docker daemon on the same machine) and reachable on its published port.
Not yet verified against Podman specifically (works against Docker; expected to work given the shared Docker-compatible API, but hasn't been exercised end-to-end): the Traefik reverse-proxy/custom-domains feature. If you hit an issue on Podman, please open an issue.
Running Podman on Apple Silicon macOS
podman machine uses the libkrun provider on Apple Silicon, which needs the external krunkit helper binary. If podman machine start fails with krunkit exited unexpectedly or, with --log-level debug, error: unexpected argument '--timesync' found, your krunkit build is stale (this affects the slp/krunkit Homebrew tap in particular). Install the current release from containers/krunkit instead.
Running as a systemd Service
[Unit]
Description=DockPod
After=docker.service
Requires=docker.service
[Service]
ExecStart=/usr/local/bin/dockpod --data-dir /var/lib/dockpod
Restart=on-failure
User=root
[Install]
WantedBy=multi-user.targetDocker socket access
DockPod needs access to the Docker socket, which is effectively root-equivalent access to the host. Only expose DockPod's web UI over HTTPS (via a reverse proxy) and keep login credentials strong — anyone who can log in can, in effect, run arbitrary code on the host via container creation.