Podium
Docker app from Garretous' Repository
Overview
Readme
View on GitHubPodium
A thin, self-updating static-site runner in a Docker container. Point it at one or more GitHub repos and it will clone, build, and serve each one — then poll every branch and rebuild itself whenever you push. No image rebuilds, no manual deploys.
- Runner, not content: the image bakes in nginx + node + git + a watch script. The site source is pulled from GitHub at runtime.
- Many sites, one container: each site is served on its own port, with its own repo, branch, build command, and output directory.
- Any repo: serves Node/Vite apps, plain static sites, or anything that produces a folder of files.
- Self-updating: push to a tracked branch and that site updates within
POLL_INTERVALseconds. A failed build is logged and the previous version stays live — and never affects the other sites.
How it works
On startup docker/entrypoint.sh reads SITES (a JSON
array) and, for each site:
- Clones
repoatbranch. - Runs
buildand publishesoutputto that site's web root. - Generates an nginx
serverblock listening on the site'sport(with a/healthzendpoint and SPA fallback). - Starts a background poll loop: every
POLL_INTERVALseconds itgit fetches; if the remote HEAD moved, it pulls, rebuilds, and atomically swaps just that site's live files.
nginx then runs in the foreground as the container's main process, serving every site's port.
Configuration
Set SITES to a JSON array of site objects:
[
{
"repo": "https://github.com/you/vite-app.git",
"port": 8080,
"branch": "main",
"build": "npm ci --no-audit --no-fund && npm run build",
"output": "dist"
},
{
"repo": "https://github.com/you/static-site.git",
"port": 8081,
"build": "",
"output": "."
}
]
| Field | Default | Purpose |
|---|---|---|
repo |
— (required) | GitHub repo to deploy. Private? See note below. |
port |
— (required) | Container port to listen on. Must be unique across sites. |
branch |
main |
Branch to track. |
build |
npm ci --no-audit --no-fund && npm run build |
Shell command to build the site. Set to "" to skip building. |
output |
dist |
Directory (relative to repo root) published to the web root. |
Plus one container-wide var:
| Env var | Default | Purpose |
|---|---|---|
POLL_INTERVAL |
300 |
Seconds between update checks (applies to all sites) |
Legacy single-site mode: if
SITESis unset, Podium synthesizes one site from the oldREPO_URL/REPO_BRANCH/PORT/BUILD_CMD/OUTPUT_DIRvars, so existing single-repo deployments keep working unchanged.
Private repo: use a token in the URL, e.g.
https://<user>:<token>@github.com/you/repo.git(a fine-grained PAT with read-only Contents scope). Prefer injecting it via an unraid/compose variable rather than committing it.
Usage
Build the image
docker build -t podium:latest .
Run two sites on two ports
docker run -d --name podium --restart unless-stopped \
-p 8080:8080 -p 8081:8081 \
-e SITES='[
{"repo":"https://github.com/you/vite-app.git","port":8080},
{"repo":"https://github.com/you/static-site.git","port":8081,"build":"","output":"."}
]' \
podium:latest
# vite app: http://localhost:8080
# static site: http://localhost:8081
Run a single site (legacy env vars still work)
docker run -d --name mysite -p 8080:80 --restart unless-stopped \
-e REPO_URL=https://github.com/you/your-vite-app.git \
podium:latest
docker compose
See docker-compose.yml for a template — edit SITES
and add one published port per site.
docker compose up -d --build
Deploying on unraid
- Build the image (or push it to a registry / GHCR and pull it on unraid).
- Add Container → set:
Repository:
podium:latest(or your registry path)Variable:
SITES→ the JSON array on a single line (unraid stores and passes it verbatim). For the portfolio site:[{"repo":"https://github.com/ggardenhire/portfolio.git","port":8080}]For a single site you may instead set the legacy
REPO_URLvariable.Ports: add one mapping per site — container
8080→ host8080, container8081→ host8081, matching theportvalues inSITES.Restart policy:
unless-stopped
- No bind mounts are required — every site is pulled from GitHub on each boot and kept current while running.
Reverse proxy
Point your reverse proxy (Nginx Proxy Manager, SWAG, Traefik, Caddy, …) at each site's host port:
- Upstream:
http://<unraid-ip>:8080,http://<unraid-ip>:8081, … - Health check path:
/healthz - Terminate TLS at the proxy; the container speaks plain HTTP.
Structure
docker/
entrypoint.sh # read SITES → per-site clone → build → serve → poll-and-redeploy
nginx.conf.template # http wrapper; includes the generated per-site blocks
site.conf.template # one server block (static serving + SPA fallback + /healthz)
Dockerfile # thin runner image
docker-compose.yml # example multi-site configuration
License
MIT © 2026 Garret Gardenhire
Categories
Download Statistics
Related apps
Explore more like this
Explore allDetails
ggarden/podium:latestRuntime arguments
- Web UI
http://[IP]:[PORT:8080]/- Network
bridge- Shell
bash- Privileged
- false
Template configuration
Host port for the first site. Must match a port value inside SITES. Add one Port entry per site.
- Target
- 8080
- Default
- 8080
- Value
- 8080
JSON array of sites to serve. Per object: repo (required), port (required, unique), branch (default main), build (default 'npm ci --no-audit --no-fund && npm run build'; use '' for a raw static repo), output (default dist; use '.' for a raw static repo).
- Default
- [{"repo":"https://github.com/you/app.git","port":8080}]
- Value
- [{"repo":"https://github.com/you/app.git","port":8080}]
Seconds between update checks (applies to all sites).
- Default
- 300
- Value
- 300