Administrator Guide
This guide covers installing, configuring, and operating Plate on a Linux host. The recommended deployment method is Docker Compose. A bare-metal systemd deployment is also supported for hosts without Docker.
Plate requires a running Snackbox backend. It has no built-in content storage and will not start successfully without a reachable Snackbox API.
For a technical overview of how Plate is structured, see docs/ARCHITECTURE.md. For contributing to the codebase, see docs/DEVELOPER.md.
Resource requirements
Figures from baseline benchmarks (500 posts, cache-warm, loopback):
| Minimal | Recommended | |
|---|---|---|
| RAM | 256 MB | 512 MB |
| CPU | 1 vCPU | 1 vCPU |
| Disk | 50 MB | 50 MB |
Memory profile: ~159 MB idle, ~207 MB after cache warm-up, ~376 MB peak under 50 concurrent connections. Throughput: ~28,000 req/s on cached responses.
Docker Compose (recommended)
Pre-built images are published to the GitLab container registry:
registry.gitlab.com/cozybadgerde/applications/plate:latest
registry.gitlab.com/cozybadgerde/applications/plate:<version>
1. Fetch the compose file and env template:
curl -LO https://gitlab.com/cozybadgerde/applications/plate/-/raw/trunk/deployments/docker/docker-compose.yml
curl -LO https://gitlab.com/cozybadgerde/applications/plate/-/raw/trunk/configs/.env.example
cp .env.example .env
chmod 0600 .env
2. Set your Snackbox URL in .env:
SNACKBOX_API_URL=http://<your-snackbox-host>:8080
All other settings have production-safe defaults. See Configuration for the full reference.
3. Start Plate:
docker compose -f docker-compose.yml up -d
4. Verify:
docker compose -f docker-compose.yml logs -f
curl http://localhost:3000/health
Upgrading
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d
To pin a specific version, set TAG=v1.2.0 in .env or on the command line.
systemd
Prerequisites
- Node.js 24 or later (
nodeon$PATH) - A release archive or locally built
dist/tree
Install
1. Create a system user and directories:
useradd --system --no-create-home --shell /usr/sbin/nologin plate
mkdir -p /usr/local/lib/plate /etc/plate /var/lib/plate/themes
chown plate:plate /var/lib/plate/themes
2. Deploy the application:
# Unpack the release archive or copy the build output:
cp -r dist node_modules package.json /usr/local/lib/plate/
3. Configure:
cp plate.env.example /etc/plate/plate.env
$EDITOR /etc/plate/plate.env # set SNACKBOX_API_URL at minimum
chmod 640 /etc/plate/plate.env
chown root:plate /etc/plate/plate.env
The annotated template is in deployments/systemd/plate.env.example.
4. Install and start the service:
cp deployments/systemd/plate.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now plate
5. Verify:
systemctl status plate
curl http://localhost:3000/health
Upgrading
Replace the contents of /usr/local/lib/plate/ with the new release and restart the service:
systemctl restart plate
Reverse proxy
Plate listens on 127.0.0.1:3000 by default and should not be exposed directly to the internet. Place a reverse proxy in front of it for TLS termination and to route /media/* requests to Snackbox.
Caddy
A Caddyfile is provided in deployments/caddy/. It proxies /media/* to Snackbox and all other requests to Plate.
Docker Compose overlay:
docker compose \
-f docker-compose.yml \
-f docker-compose.caddy.yml \
up -d
Set SERVER_NAME to your domain for automatic HTTPS via Let's Encrypt:
SERVER_NAME=plate.example.com docker compose \
-f docker-compose.yml \
-f docker-compose.caddy.yml \
up -d
Bare-metal:
Copy deployments/caddy/Caddyfile to /etc/caddy/Caddyfile, set the SERVER_NAME, PLATE_UPSTREAM, and SNACKBOX_UPSTREAM environment variables (or edit the defaults in the file directly), then reload Caddy:
systemctl reload caddy
Configuration
All settings are read from environment variables. Plate ships with production-safe defaults for everything except SNACKBOX_API_URL.
For Docker, set variables in a .env file next to the compose file. For systemd, edit /etc/plate/plate.env. A fully annotated template is available at deployments/systemd/plate.env.example and configs/.env.example.
Reference
| Variable | Default | Description |
|---|---|---|
SNACKBOX_API_URL | http://localhost:8080 | URL of the Snackbox API. Set this. |
SNACKBOX_TIMEOUT | 5000 | Snackbox request timeout in milliseconds. |
NODE_ENV | production | Keep production in production. |
LISTEN_ADDRESS | 127.0.0.1:3000 | Address and port Plate listens on. Use 0.0.0.0:3000 in Docker. |
SHUTDOWN_TIMEOUT | 10000 | Drain timeout before force-closing connections on shutdown (ms). |
TRUSTED_PROXIES | (unset) | Comma-separated proxy IPs or CIDR ranges to trust for X-Forwarded-For. Set to your reverse proxy IP(s) in production. |
RATE_LIMIT_WINDOW_MS | 60000 | Rate limit window in milliseconds. |
RATE_LIMIT_MAX | 100 | Maximum requests per window per IP. |
THEME_NAME | (unset) | Lock Plate to a specific theme and disable hot-reload. Leave unset to follow Snackbox settings. |
DEFAULT_THEME | picnic | Fallback theme when none is configured or the requested theme is not found. |
THEMES_DIR | themes | Directory for external themes. |
POSTS_PER_PAGE | 10 | Number of posts per page (1–100). |
PAGE_PREFIX | pages | URL prefix for static pages (/pages/<slug>). Set to empty string for clean URLs (/<slug>). When empty, posts, tags, and health are reserved and cannot be used as page slugs. |
CACHE_ENABLED | true | Enable the in-memory API response cache. |
CACHE_TTL_MULTIPLIER | 1 | Multiplies base content TTLs (post list: 60 s, individual post/page: 300 s). Admin-controlled resources (settings, navigation, tags) use a fixed 15 s TTL. Minimum 1. Use CACHE_ENABLED=false to disable caching entirely. |
CACHE_MAX_ENTRIES | 100 | Maximum cached entries (LRU eviction). |
METRICS_ADDRESS | 127.0.0.1:9091 | Address for the Prometheus metrics endpoint. Never expose publicly. |
LOG_LEVEL | info | Log level: trace | debug | info | warn | error | fatal | silent. |
LOG_FORMAT | json | Log format: json (production) or pretty (development). |
LOG_ACCESS | true | Emit an access log entry per request. |
SITE_URL | (unset) | Public base URL of the site (e.g. https://example.com). Required for sitemap.xml generation; also adds a Sitemap: line to robots.txt. Omit the trailing slash. |
Metrics
Plate exposes a Prometheus metrics endpoint at METRICS_ADDRESS (default 127.0.0.1:9091). Add it as a scrape target in your Prometheus configuration:
scrape_configs:
- job_name: plate
static_configs:
- targets: ["localhost:9091"]
The endpoint binds to localhost by default. Do not expose it publicly.
Logging
Plate writes structured logs to stdout. LOG_FORMAT=json is the default in production — set this so your log collector can parse them. Pipe stdout to your preferred aggregation stack (journald, Loki, Fluentd, etc.).
With systemd, logs are available via:
journalctl -u plate -f
External themes
Drop theme directories into THEMES_DIR (default /var/lib/plate/themes for systemd deployments). Plate picks up the active theme from Snackbox settings and hot-reloads on change — no restart required.
To pin a theme regardless of Snackbox settings, set THEME_NAME.
The built-in Picnic theme is always available as a fallback and requires no additional setup.
Built-in routes
Plate serves the following routes automatically. Themes have no control over their output.
| Route | Description |
|---|---|
GET /robots.txt | Standard robots file. Includes a Sitemap: directive when SITE_URL is set. |
GET /feed | RSS 2.0 feed of the 20 most recent posts. |
GET /sitemap.xml | XML sitemap of all posts, pages, and tags. Returns 404 when SITE_URL is not set. |
GET /health | Health check endpoint — returns 200 OK when Snackbox is reachable. |
GET /_theme/assets/* | Static assets served from the active theme's assets/ directory. |
Set SITE_URL to your public base URL (e.g. https://example.com) to enable the sitemap and the Sitemap: line in robots.txt.