Self-host with Docker
Prerequisites
Section titled “Prerequisites”- Docker Engine 27+ and Docker Compose v2 on Linux;
- a trusted, checked-out static-site workspace;
- the site’s locked generator dependencies; and
- TLS termination or a private network.
The supplied container runs as UID/GID 1000, uses a read-only root filesystem,
drops Linux capabilities, and binds its direct recovery port only to
127.0.0.1.
The final image intentionally omits npm and Git: install the site’s locked
dependencies before mounting the workspace, and perform Git administration on
the host. Generator executables already installed in the workspace remain
available to Studio.
Preview isolation uses a noexec,nosuid tmpfs. Its size defaults to 1 GiB and
can be changed with BLOG_STUDIO_TMPFS_SIZE; allow enough room for both a copy
of the source tree and one generated site without exceeding the container’s
memory limit.
1. Prepare directories and secrets
Section titled “1. Prepare directories and secrets”mkdir -p config data/agent-runtime secrets workspace backupscp deploy/traefik/.env.example .envcp examples/config/blog-studio.yml config/blog-studio.ymlcp -R examples/workspace/. workspace/umask 077openssl rand -base64 48 > secrets/cookie_secretchmod 700 data/agent-runtime secretschmod 600 secrets/cookie_secretgit -C workspace initgit -C workspace config user.name "Blog Studio Quick Start"git -C workspace config user.email "quick-start@localhost"git -C workspace add .git -C workspace commit -m "Initialize example workspace"chown -R 1000:1000 data workspacechown -R 1000:1000 secretsThe example is dependency-free, uses the built-in command generator, and keeps
publishing disabled while writing, autosave, and preview remain functional. To
connect a real site, replace workspace/ with a clean trusted checkout, install
its locked dependencies on the host, and update the adapter configuration. The
container path remains /workspaces/blog.
For the Site Agent, provision Pi’s auth.json, models.json, and
settings.json under data/agent-runtime, owned by UID/GID 1000 and mode
0600; keep the directory mode 0700. Configure glm-5.2 as the Pi default.
For optional vision, create secrets/vision_api_key as the same owner and mode
0600, set BLOG_STUDIO_VISION_API_KEY_PATH to its host path, and set the
endpoint, minimax-m3 model, and in-container key-file path in .env. Never
place either credential value in .env, YAML, or an image layer.
2. Initialize the owner and start
Section titled “2. Initialize the owner and start”docker compose config --quietdocker compose build --pulldocker compose run --rm studio \ node dist/server/cli.js auth init \ --database /data/blog-studio.sqlitedocker compose up -ddocker compose pscurl --fail http://127.0.0.1:4310/api/healthThe CLI reads and confirms the first owner password without echo. Open the configured HTTPS origin and log in with that password. All non-health application APIs still require a signed session; mutations additionally require same-origin CSRF validation.
Use the same trusted-container entry point for status or recovery:
docker compose run --rm studio \ node dist/server/cli.js auth status \ --database /data/blog-studio.sqlitedocker compose run --rm studio \ node dist/server/cli.js auth reset \ --database /data/blog-studio.sqliteReset revokes every existing browser session. The legacy opaque token is an optional v0.1 migration fallback and is not part of the normal setup journey.
3. Join an existing Traefik network
Section titled “3. Join an existing Traefik network”docker compose \ -f docker-compose.yml \ -f deploy/traefik/docker-compose.override.yml \ config --quietdocker compose \ -f docker-compose.yml \ -f deploy/traefik/docker-compose.override.yml \ up -dUse both -f arguments for every later up, restart, pull, and recreated
deployment. Using only the base file removes the Traefik network and labels
from the replacement container, so local health may pass while the HTTPS route
returns 404.
The reference defaults use blog-editor.internal.wj2015.com, the external
network home-server_default, and the websecure entrypoint. Override them in
.env for another installation. Traefik must already own the certificate and
entrypoint.
BLOG_STUDIO_ALLOWED_ORIGINS must exactly contain the browser-facing HTTPS
origin. Do not use a wildcard and do not expose port 4310 to a LAN or the public
internet.
Persistent paths
Section titled “Persistent paths”| Host | Container | Content |
|---|---|---|
data/ |
/data |
SQLite drafts, jobs, releases |
data/agent-runtime/ |
/data/agent-runtime |
Pi config and credentials |
config/blog-studio.yml |
/config/blog-studio.yml |
administrator policy |
workspace/ |
/workspaces/blog |
files, Git, generator |
secrets/* |
/run/secrets/* |
cookie/provider secrets |
The static public site has no request-time dependency on these mounts or on Studio availability.
Prove the container contract
Section titled “Prove the container contract”BLOG_STUDIO_SMOKE_IMAGE=blog-studio:local pnpm container:smokeThe isolated smoke test checks non-root identity, a read-only root filesystem, health, authentication, acknowledged draft persistence, clean SIGTERM, and container recreation. It does not mount a real site or call a provider.
Continue with workspace configuration and a backup drill.