Install Specmate from GHCR

This is the pull-only route for a private testing install on another box. Docker Engine and Docker Compose v2 are required; Node and Bun are already handled inside the application image. GitHub Pages hosts documentation and the local YAML helper, while the actual Specmate application runs on your server with PostgreSQL and a worker.

1. Choose an image and download the files

Use a published stable version (ghcr.io/formless63/specmate:0.1.0, for example, once that release exists) or an immutable digest. A manually published :edge image is suitable for testing and may change. See the package page, release list, and release rules; configuration alone does not mean an image has been published.

Download compose.install.yaml and .env.install.example into an empty private install directory, or download the two files from the YAML helper. Rename the environment file to .env.install and restrict it with chmod 600 .env.install. Keep this directory and its secrets out of public Git.

Generate different database and authentication secrets with openssl rand -hex 32, or use the helper's browser-local generation button. Fill POSTGRES_PASSWORD and BETTER_AUTH_SECRET. Keep both stable across upgrades. Choose APP_IMAGE, APP_BASE_URL, APP_BIND and APP_PORT deliberately.

2. Select access and sign-in

The default loopback binding and Mailpit profile are a private evaluation setup. From your workstation, tunnel the two local ports:

ssh -L 3000:127.0.0.1:3000 -L 8025:127.0.0.1:8025 your-test-box

With that tunnel, use APP_BASE_URL=http://localhost:3000, open the app at that address, and read its magic-link mail at http://localhost:8025. Anyone who can read the inbox can sign in through its links. Keep the inbox on loopback; do not publish it through a proxy.

For a real domain, use APP_BASE_URL=https://your-domain.example and terminate TLS at your reverse proxy. Loopback works for a proxy on the host. A separately containerized proxy needs its own explicitly configured network route to the app; do not treat its localhost as the Docker host. Set APP_BIND=0.0.0.0 only when that is your intended route and restrict access with firewall/proxy configuration.

For GitHub OAuth, remove mailpit from COMPOSE_PROFILES, set MAGIC_LINK_ENABLED=false, and set GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET. The callback is https://your-domain.example/api/auth/callback/github. Alternatively configure the documented OIDC provider. For email sign-in outside a private evaluation, use real SMTP and the appropriate TLS mode. See Getting Started and Operations.

3. Start services and apply migrations explicitly

Run these from the install directory after filling secrets. They create data in this install's named volumes:

docker compose --env-file .env.install -f compose.install.yaml pull app worker postgres mailpit
docker compose --env-file .env.install -f compose.install.yaml up -d --wait postgres mailpit
docker compose --env-file .env.install -f compose.install.yaml run --rm migrate
docker compose --env-file .env.install -f compose.install.yaml run --rm jobs-migrate
docker compose --env-file .env.install -f compose.install.yaml up -d --wait app worker

These commands assume the default Mailpit evaluation profile. Omit mailpit for OAuth/real SMTP. The helper supplies the commands for its selected profiles. Migrations are explicit operator actions, not application startup side effects. The same selected image serves app, worker and migrations.

Open /api/health, sign in, then open Designs. Create synthetic demo provides an illustrative walkthrough. Keep unmeasured physical checks open. Check logs with docker compose --env-file .env.install -f compose.install.yaml logs --tail=100 app worker; share only redacted diagnostics.

4. Configure artifacts when needed

Library, composition and cable documents do not require S3. Uploads, CAD artifacts and their release packages require private S3-compatible storage. Use your existing private bucket with STORAGE_ENDPOINT, STORAGE_REGION, STORAGE_BUCKET, credentials and path-style setting, or enable the bundled RustFS storage profile with fresh storage credentials. The bundled service uses http://storage.localhost:9000, a Compose network alias internally and a loopback hostname in the browser. Port 9000 is bound only to host loopback. Tunnel it with ssh -L 9000:127.0.0.1:9000 your-test-box when using uploads from another machine. Its data and logs persist in named volumes.

For a fresh local testing bucket through the SSH tunnel, set COMPOSE_PROFILES=mailpit,storage, bucket specmate, region us-east-1, endpoint http://storage.localhost:9000, STORAGE_FORCE_PATH_STYLE=true and generated STORAGE_ACCESS_KEY_ID/STORAGE_SECRET_ACCESS_KEY. Pull/start storage alongside PostgreSQL/Mailpit. Once the storage service is ready, initialize this new testing bucket explicitly:

docker compose --env-file .env.install -f compose.install.yaml run --rm \
  -e STORAGE_DEV_BOOTSTRAP=true -e STORAGE_DEV_APP_ORIGIN=http://localhost:3000 \
  worker node .output/storage-bootstrap.mjs
docker compose --env-file .env.install -f compose.install.yaml run --rm \
  worker node .output/storage-check.mjs

This bootstrap intentionally accepts only a loopback origin. Browser uploads use presigned URLs, so an external S3 endpoint must be reachable by both the server and browser without changing its signed hostname/port. For a real domain, provision that endpoint, bucket and narrow CORS policy through your storage operator; do not use the development bootstrap. Native CAD needs a separately isolated runner and reviewed native tools. See Engineering Workflows before enabling it.

Upgrades, backups and shutdown

Keep the project name/install directory, secrets and volumes stable. Back up PostgreSQL, private objects and private configuration together; releases refer to pinned inputs and artifact hashes. Review migration/release notes before upgrading. Stop app/worker, change APP_IMAGE to a reviewed version/digest, pull, apply app/Jobs migrations explicitly, and start the app/worker. Verify health, sign-in, worker and stored artifacts afterward. An image rollback alone cannot reverse an incompatible database migration; retain a tested restore point.

docker compose --env-file .env.install -f compose.install.yaml down stops/removes containers while preserving named volumes. Do not add --volumes to ordinary shutdown or upgrade commands. Changing the Compose project name starts a different set of volumes rather than upgrading the old data.