# Deployment Guide ## Requirements - Docker 24.0+ - Docker Compose 2.20+ - A reverse proxy that terminates TLS for the public hostname - A writable checkout directory for `./data` and `./content` ## Fresh Deployment ```bash git clone https://github.com/your-org/cairnquire.git cd cairnquire cp .env.example .env ``` Edit `.env` and set the HTTPS origin that users will open in their browser: ```dotenv CAIRNQUIRE_PUBLIC_ORIGIN=https://cairnquire.example.com ``` Start the application and verify the local container health endpoint: ```bash docker compose up -d --build docker compose ps curl http://127.0.0.1:8080/health ``` Compose starts Mailpit for local SMTP capture. Open `http://127.0.0.1:8025` to inspect password-reset and notification messages. SMTP is also exposed on `127.0.0.1:1025` for host-run development servers. Then open `https://cairnquire.example.com/setup`. On an empty database the first-run wizard: 1. Creates the initial administrator account. 2. Sets its password. 3. Records whether visitors may create their own accounts. 4. Signs the administrator in. The setup endpoint is only available until the first administrator has been created. The signup setting can be changed later from the administrator's account page. ## Environment Variables The checked-in Compose file sets the container paths. For a normal single-host deployment, only `CAIRNQUIRE_PUBLIC_ORIGIN` must be supplied in `.env`. | Variable | Required | Compose default | Description | |----------|----------|-----------------|-------------| | `CAIRNQUIRE_PUBLIC_ORIGIN` | Yes | None | Exact external HTTPS origin. Passkeys and device-flow URLs depend on this value. | | `CAIRNQUIRE_EMAIL_PROVIDER` | No | `smtp` | Outbound provider: `smtp`, `postmark`, `cloudflare`, or `noop`. | | `CAIRNQUIRE_EMAIL_SMTP_HOST` | No | `mailpit` | SMTP server used for notifications. | | `CAIRNQUIRE_EMAIL_SMTP_PORT` | No | `1025` | SMTP server port. | | `CAIRNQUIRE_EMAIL_POSTMARK_TOKEN` | For Postmark | Empty | Postmark server token. | | `CAIRNQUIRE_EMAIL_CLOUDFLARE_ACCOUNT_ID` | For Cloudflare | Empty | Account with Cloudflare Email Sending enabled. | | `CAIRNQUIRE_EMAIL_CLOUDFLARE_API_TOKEN` | For Cloudflare | Empty | API token with Email Sending permission. Store it as a deployment secret. | | `CAIRNQUIRE_EMAIL_FROM` | No | `notifications@cairnquire.local` | Notification sender address. | | `CAIRNQUIRE_LOG_LEVEL` | No | `INFO` | Application log level. | The server also accepts these lower-level overrides when it is run outside the checked-in Compose deployment: | Variable | Default | Description | |----------|---------|-------------| | `CAIRNQUIRE_SERVER_ADDR` | `:8080` | Listen address. | | `CAIRNQUIRE_DATABASE_PATH` | `../../data/db.sqlite` | Local SQLite/libsql database path. | | `CAIRNQUIRE_DATABASE_PRIMARY_URL` | Empty | Optional remote libsql primary URL for embedded-replica mode. | | `CAIRNQUIRE_DATABASE_AUTH_TOKEN` | Empty | Optional auth token for the remote libsql primary. | | `CAIRNQUIRE_CONTENT_SOURCE_DIR` | `../../content` | Writable Markdown source directory. | | `CAIRNQUIRE_CONTENT_STORE_DIR` | `../../data/files` | Content-addressed attachment store. | | `CAIRNQUIRE_CONFIG` | Empty | Optional JSON configuration file. Environment variables override it. | | `CAIRNQUIRE_DEV_MODE` | `false` | Local-only auth shortcut. Never enable this on a deployed server. | ### Cloudflare Email Service Cairnquire is an external Go application, so it uses Cloudflare Email Service's REST API rather than a Workers binding. Before enabling the provider: 1. Enable Email Sending for the sender domain in Cloudflare, or run `npx wrangler email sending enable example.com`. 2. Create a Cloudflare API token with Email Sending permission for the account. 3. Set `CAIRNQUIRE_EMAIL_PROVIDER=cloudflare`, the account ID, API token, and a `CAIRNQUIRE_EMAIL_FROM` address on the onboarded domain. Cloudflare Email Service is used only for transactional notifications. API tokens must be supplied through deployment secrets and must not be committed. The durable queue retries Cloudflare `429` and server errors; validation and permanent-bounce responses are retained immediately as failed deliveries. Cloudflare Email Routing for inbound comment replies is a separate Milestone 4 feature and is not enabled by these outbound settings. ## Persistent Data Compose mounts: - `./data:/workspace/data` for the database and attachments - `./content:/workspace/content` for editable Markdown source Both mounts must remain writable because uploads, browser edits, and sync writebacks modify them. ## Reverse Proxy Expose container port `8080` through a TLS-terminating reverse proxy. Preserve the original hostname and forwarded client address headers. The configured `CAIRNQUIRE_PUBLIC_ORIGIN` must exactly match the external origin, including `https://`. ## Backup And Restore Stop writes or stop the app container before copying a fully consistent local database backup: ```bash docker compose stop app tar czf cairnquire-backup.tgz data content docker compose up -d app ``` Restore by stopping the app, replacing `data` and `content` from the archive, and starting the app again. ## Upgrade ```bash docker compose stop app tar czf cairnquire-backup-before-upgrade.tgz data content git pull docker compose up -d --build curl http://127.0.0.1:8080/health ``` Database migrations run automatically on startup. ## Troubleshooting ```bash docker compose logs -f app docker compose ps curl http://127.0.0.1:8080/health ``` If passkey creation fails after deployment, confirm that the browser URL and `CAIRNQUIRE_PUBLIC_ORIGIN` are the same HTTPS origin.