Skip to content

Hosting Docker

Arael Espinosa edited this page Jul 13, 2026 · 3 revisions

Docker Deployment

This page covers a full production-grade Docker Compose deployment of Piro.

For a quick local setup in under 5 minutes, see Getting Started → Docker.


Prerequisites

  • Docker 24+
  • Docker Compose v2
  • A domain or subdomain pointed at your server
  • A TLS-terminating reverse proxy in front (Caddy, nginx, Cloudflare Tunnel, etc.)

Architecture

Piro ships as four containers managed by a single Compose file:

Browser
  └── Your TLS proxy (Caddy / nginx / Cloudflare)
        └── Piro Nginx proxy :80
              ├── /          → web (Next.js status page)
              ├── /admin/*   → static admin SPA (built into proxy image)
              ├── /api/*     → api (ASP.NET Core)
              ├── /hub/*     → api (SignalR WebSocket)
              └── /uploads/* → api (file serving)

Internal Docker network (no external ports except :80):
  api   :8080
  web   :3000
  db    :5432

Only the proxy container exposes port 80. All other services communicate over the internal Docker network.


Docker Images

Image Platform
ghcr.io/heva-co/piro-proxy linux/amd64
ghcr.io/heva-co/piro-web linux/amd64
ghcr.io/heva-co/piro-api linux/amd64, linux/arm64

Images are published to GitHub Container Registry on each tagged release.


Compose File

The repository ships with docker-compose.yml for production (uses GHCR images) and docker-compose.local.yml for local source builds.

For production, you only need docker-compose.yml and a .env file:

curl -O https://raw.githubusercontent.com/Heva-Co/piro/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/Heva-Co/piro/main/.env.example
cp .env.example .env

Edit .env:

# Image version — pin to a specific release or use latest
PIRO_VERSION=latest

# Required
POSTGRES_PASSWORD=a_strong_random_password
JWT_SECRET=a_long_random_string_at_least_32_chars

# Optional
ACCESS_TOKEN_EXPIRY_MINUTES=60
REFRESH_TOKEN_EXPIRY_DAYS=30

Start the stack:

docker compose pull
docker compose up -d

First-Time Setup

  1. Visit http://your-server/admin/ — you will be redirected to the setup wizard automatically if the database is empty
  2. Create your owner account and fill in site configuration (name, URL, email settings, etc.)
  3. Sign in at http://your-server/admin/auth/sign-in

All configuration (site name, SMTP, OIDC/SSO, etc.) lives in the database and is managed through the admin panel. No env vars are needed beyond what's in .env.


TLS Termination

The proxy container listens on port 80 (plain HTTP). TLS should be terminated by whatever sits in front of it.

Caddy (recommended)

status.example.com {
    reverse_proxy localhost:80
}

Caddy handles certificate provisioning automatically via Let's Encrypt.

nginx (with Certbot)

server {
    listen 443 ssl;
    server_name status.example.com;

    ssl_certificate     /etc/letsencrypt/live/status.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/status.example.com/privkey.pem;

    location / {
        proxy_pass         http://localhost:80;
        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;

        # Required for SignalR WebSocket (/hub/)
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "upgrade";
    }
}

server {
    listen 80;
    server_name status.example.com;
    return 301 https://$host$request_uri;
}

Cloudflare Tunnel

Point the tunnel to http://localhost:80. Cloudflare handles TLS. No cert management required.


Volumes

Volume Mount Description
postgres_data /var/lib/postgresql/data All database data
api_uploads /app/wwwroot/uploads Uploaded logos and favicons

Both volumes are created automatically. Include them in your backup strategy.


Updating

docker compose pull
docker compose up -d

Database migrations run automatically on API startup. No manual migration steps are needed.

To pin to a specific version, set PIRO_VERSION in .env:

PIRO_VERSION=v0.2.0

Production Checklist

  • JWT_SECRET is a long random string (≥ 32 chars)
  • POSTGRES_PASSWORD is not the default value
  • TLS is terminated by a reverse proxy — do not expose port 80 directly to the internet
  • postgres_data volume is backed up regularly
  • api_uploads volume is backed up regularly
  • Image versions are pinned in .env for reproducible deployments

Clone this wiki locally