Deploying on Docker
Running Curiosity Workspace as a Docker container is the most common way to install it. The same image runs in local dev, in CI, and in production behind a reverse proxy.
This page covers single-host Docker deployments. For Kubernetes, see Kubernetes; for managed cloud variants, see AWS, Azure, GCP, and OpenShift.
Requirements
- A 64-bit Linux, macOS, or Windows host with Docker installed. Follow the official Docker docs.
- 8 GB of RAM available to Docker (16 GB+ for production with embeddings).
- A persistent volume mount for
MSK_GRAPH_STORAGE. - A network plan: bind to
127.0.0.1for local dev, put a TLS-terminating proxy in front for shared deployments.
Pulling the image
The image is published as curiosityai/curiosity:
docker pull curiosityai/curiosity:nightly
To pin to a specific release, use the numeric build tag:
docker pull curiosityai/curiosity:66474
Pin in production
:nightly rolls forward on every push. Pin to a numeric build tag in production (and staging) so upgrades are explicit. See Upgrades and migrations.
Local development — single docker run
The simplest invocation, suitable only for a developer's own machine:
mkdir -p ~/curiosity/storage
docker run --name curiosity \
-p 127.0.0.1:8080:8080 \
-v ~/curiosity/storage:/data \
-e MSK_GRAPH_STORAGE=/data/curiosity \
-e MSK_ADMIN_PASSWORD="$(openssl rand -base64 24)" \
curiosityai/curiosity:nightly
Notes:
127.0.0.1:8080:8080binds the workspace to loopback only. Drop the IP prefix when you're ready to expose it on the host network.MSK_ADMIN_PASSWORDreplaces the defaultadmin/admin. Read it back from your shell history ordocker inspect.
On Windows, swap the volume syntax:
mkdir c:\curiosity\storage
docker run --name curiosity `
-p 127.0.0.1:8080:8080 `
-v c:/curiosity/storage:/data `
-e MSK_GRAPH_STORAGE=/data/curiosity `
-e MSK_ADMIN_PASSWORD=ChangeMeNow! `
curiosityai/curiosity:nightly
Docker on Windows expects forward slashes inside volume paths.
Staging / production — Docker Compose
For anything beyond local dev, use Docker Compose so the configuration is versioned and reproducible.
docker-compose.yml:
services:
curiosity:
image: curiosityai/curiosity:66474
container_name: curiosity
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080" # exposed via the reverse proxy below
volumes:
- curiosity-data:/data
- curiosity-backups:/backups
- ./certs:/certs:ro
environment:
MSK_GRAPH_STORAGE: /data/curiosity
MSK_GRAPH_BACKUP_FOLDER: /backups
MSK_GRAPH_JOURNAL_FOLDER: /data/journal
MSK_PUBLIC_ADDRESS: https://workspace.example.com
MSK_USE_HSTS: "true"
MSK_REDIRECT_TO_HTTPS: "true"
# Secrets injected via env_file (kept out of git)
env_file:
- .env
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/api/login/check", "-o", "/dev/null"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
volumes:
curiosity-data:
curiosity-backups:
.env (kept out of source control — see your secret manager):
MSK_ADMIN_PASSWORD=<from secret manager>
MSK_ADMIN_EMAIL=ops@example.com
MSK_JWT_KEY=<32+ random bytes, from secret manager>
MSK_LICENSE=<your license token>
MSK_GRAPH_MASTER_KEY=<32+ random bytes, backed up separately>
Bring it up:
docker compose up -d
docker compose logs -f curiosity
Environment variables
The container is configured almost entirely through MSK_* environment variables — there is no file-based config inside the image. Pass them with -e flags on docker run, an environment: block in Compose, or an env_file: for secrets. The tables below cover the variables you're most likely to set on a Docker host; see Configuration reference for the full list including integration-specific keys (OpenAI, S3, Azure OCR, etc.).
Booleans
Boolean variables accept true / false (case-insensitive). In Compose, quote them ("true") so YAML doesn't coerce them to native booleans before they reach the container.
Storage and data
| Variable | Type | Default | Description |
|---|---|---|---|
MSK_GRAPH_STORAGE |
path | system temp | Primary on-disk location for the graph database. Mount a persistent volume here. |
MSK_GRAPH_BACKUP_FOLDER |
path | — | Destination for graph snapshots. Mount a separate volume for off-host backup. |
MSK_GRAPH_TEMP_FOLDER |
path | system temp | Scratch space for graph operations. |
MSK_GRAPH_RESTORE_FROM_INDEX |
int | -1 |
If set, restore the graph from this backup index on startup. |
MSK_GRAPH_CHECK_INTEGRITY |
bool | false |
Verify graph integrity on boot (slower startup). |
MSK_GRAPH_CHECK_BLOBS_INTEGRITY |
bool | false |
Verify blob-store integrity on boot. |
MSK_ROCKSDB_CACHE_SIZE_MB |
int | — | RocksDB block-cache size in MB. Tune for available RAM. |
Networking and addresses
| Variable | Type | Default | Description |
|---|---|---|---|
MSK_PORT |
int | 8080 |
HTTP(S) listening port inside the container. |
MSK_PUBLIC_ADDRESS |
URL | — | Public-facing URL (e.g. https://workspace.example.com). Used to generate links in emails and SSO callbacks. |
MSK_SERVER_ADDRESS |
URL | — | Internal address used by components when it differs from the public address. |
MSK_SERVER_BASE_PATH |
string | — | URL base path when mounted under a subpath of the reverse proxy. |
MSK_CORS |
string | — | Comma-separated list of additional CORS origins. |
MSK_DISABLE_CORS_LOCALHOST |
bool | false |
Drop localhost from the default CORS allowlist. |
MSK_MAX_BODY_SIZE |
int (MB) | 512 |
Maximum request body size in megabytes. |
MSK_HTTP_PROXY |
URL | — | Outbound HTTP proxy address. |
MSK_HTTP_PROXY_USER |
string | — | Outbound proxy username. |
MSK_HTTP_PROXY_PASSWORD |
string | — | Outbound proxy password. |
TLS and certificates
| Variable | Type | Default | Description |
|---|---|---|---|
MSK_CERT_FILE |
path | — | Path to the TLS certificate (PEM) inside the container. |
MSK_CERT_FILE_PRIVATE_KEY |
path | — | Path to the matching private key. |
MSK_CERT_PWD |
string | — | Password for an encrypted certificate / private key. |
MSK_CERT_FROM_STORE_NAME |
string | — | Load certificate by name from the OS certificate store (rare in containers). |
MSK_CERT_SELF_SIGNED |
bool | false |
Generate and use a self-signed certificate on boot. Dev only. |
MSK_LETS_ENCRYPT_SUBDOMAIN |
string | — | Subdomain for in-container Let's Encrypt issuance. |
MSK_USE_HSTS |
bool | false |
Send HSTS headers. Enable once TLS is stable. |
MSK_REDIRECT_TO_HTTPS |
bool | false |
Redirect plain HTTP to HTTPS. |
Authentication and security
| Variable | Type | Default | Description |
|---|---|---|---|
MSK_JWT_KEY |
string | — | Required. Signing key for session tokens. Use 32+ random bytes; rotate carefully. |
MSK_GRAPH_MASTER_KEY |
string | — | Required. Master encryption key for the graph database. Back up separately from data. |
MSK_ADMIN_USER |
string | admin |
Initial admin username created on first boot. |
MSK_ADMIN_EMAIL |
string | — | Initial admin email address. |
MSK_ADMIN_PASSWORD |
string | — | Initial admin password. Replace the admin/admin default. |
MSK_ADMIN_INVITE |
string | — | Comma-separated list of emails to invite as admins on first boot. |
MSK_SAME_SITE_COOKIE |
bool | true |
Set SameSite on auth cookies. |
MSK_PASSWORDS_MIN_LENGTH |
int | — | Minimum password length enforced on user changes. |
MSK_PASSWORDS_USER_EXPIRATIONDAYS |
int | 180 |
Days before a user password must be rotated. |
MSK_PASSWORDS_ADMIN_EXPIRATIONDAYS |
int | 90 |
Days before an admin password must be rotated. |
MSK_LOGIN_EXP |
int (h) | — | Session token lifetime in hours. |
MSK_LOGIN_RENEW_EXP |
int (h) | — | Refresh-window lifetime in hours. |
Logging and diagnostics
| Variable | Type | Default | Description |
|---|---|---|---|
MSK_LOG_LEVEL |
enum | Information |
One of Trace, Debug, Information, Warning, Error. |
MSK_LOG_PATH |
path | — | Directory for application logs. Mount a volume to persist them. |
MSK_AUDIT_LOG_PATH |
path | — | Directory for the audit log (separate from application logs). |
MSK_DISABLE_TELEMETRY |
bool | false |
Disable anonymous usage telemetry. |
MSK_DISABLE_CLOUD_ERROR_REPORT |
bool | false |
Disable outbound crash reporting. |
MSK_DISABLE_RESOURCE_MONITORING |
bool | false |
Disable in-process memory/disk health checks. |
MSK_RAM_THRESHOLD_UNHEALTHY |
string | — | RAM threshold (e.g. 90%) above which the healthcheck reports unhealthy. |
MSK_DISK_THRESHOLD_UNHEALTHY |
string | — | Disk threshold above which the healthcheck reports unhealthy. |
Replication and state tracking
| Variable | Type | Default | Description |
|---|---|---|---|
MSK_REPLICA |
bool | false |
Run as a read-only replica that follows a primary. |
MSK_PRIMARY_ADDRESS |
URL | — | URL of the primary workspace, required when MSK_REPLICA=true. |
MSK_GIT_TRACK_STATE_PATH |
path | — | Local path used to track admin/config state in git. |
MSK_GIT_TRACK_STATE_BRANCH |
string | main |
Branch used by the state tracker. |
MSK_GIT_REMOTE_URL |
URL | — | Remote repository for pushing tracked state. |
MSK_GIT_REMOTE_USERNAME / MSK_GIT_REMOTE_PASSWORD |
string | — | Credentials for the remote git repository. |
In-product updates
These variables control the Software Updates admin page (Operate → Software Updates), which lets system admins pull a newer image tag and recreate the running container from the UI. See In-product updates below for the full flow and security trade-offs.
| Variable | Type | Default | Description |
|---|---|---|---|
MSK_DISABLE_DOCKER_UPDATE |
bool | false |
Hide the Software Updates page and refuse update requests. Set to true when upgrades are owned by an external orchestrator (Compose CI, Kubernetes, watchtower, etc.). |
MSK_DOCKER_IMAGE_REPOSITORY |
string | curiosityai/curiosity |
Registry repository to query for newer tags. Only override when running a private mirror or a fork. Must match ^[A-Za-z0-9][A-Za-z0-9_./-]{0,255}$. |
MSK_DOCKER_SOCKET |
path | /var/run/docker.sock |
Path to the Docker Engine socket as seen from inside the container. The conventional DOCKER_HOST=unix://… is also honoured. |
MSK_CONTAINER_ID |
string | auto-detected | Override the detected container id. Auto-detection reads /proc/self/mountinfo and /proc/self/cgroup; set this when running under a runtime that hides the id (some rootless or PaaS setups). |
Performance and runtime
| Variable | Type | Default | Description |
|---|---|---|---|
MSK_LOW_MEMORY |
bool | false |
Enable low-memory mode (smaller caches, slower queries). |
Licensing and application metadata
| Variable | Type | Description |
|---|---|---|
MSK_LICENSE |
string | License token issued by Curiosity. |
MSK_LICENSED_TO |
string | Customer name on the license. |
MSK_APPNAME |
string | Override the application display name. |
MSK_FONTS_PATH |
path | Directory of custom fonts to make available to renderers. |
MSK_WWW_FOLDER |
path | Override the static web-assets directory. |
Production checklist
A short list of must-haves before a Docker-based workspace serves real users:
- Numeric build tag (
curiosityai/curiosity:66474), not:nightly. - Persistent volume for
MSK_GRAPH_STORAGEon durable storage (SSD or better) - ideally with snapshot support. - Optional backup volume for
MSK_GRAPH_BACKUP_FOLDER, off-host on a schedule. - All secrets injected from a secret manager (never baked into the image or compose file).
- TLS terminated at a reverse proxy or in-container (set
MSK_CERT_FILE,MSK_CERT_FILE_PRIVATE_KEY,MSK_USE_HSTS=true,MSK_REDIRECT_TO_HTTPS=true). -
MSK_PUBLIC_ADDRESSset so generated links use the right host. - Container memory/CPU limits sized for embedding workload (start at 16 GB / 8 vCPU).
- Healthcheck enabled so the orchestrator can restart the container on hangs.
- Log routing to your aggregator — either
MSK_LOG_PATHon a mounted volume or the platform's stdout collector. - Backup/restore drill completed and documented.
See Production deployment for the broader checklist that also covers identity, monitoring, and disaster recovery.
Healthcheck details
Use the /api/health endpoint to check application health. Use /api/ping to see if the container is responding to requests.
In-product updates
System admins can pull a newer image tag and recreate the running container from the Software Updates admin page (Operate → Software Updates), without dropping to the host shell. The page lists every numeric build tag in the configured repository that is newer than the running build; picking one triggers a watchtower-style cutover.
This is intended for small single-host deployments and lab environments. If upgrades are owned by Compose CI, Kubernetes, or an external watchtower, set MSK_DISABLE_DOCKER_UPDATE=true and ignore the page.
How the swap works
A container cannot replace itself — it holds the host ports the replacement needs and would have to keep issuing daemon calls after stopping. The workspace therefore delegates the cutover to a short-lived updater helper container spawned from its own image:
- The workspace pulls the chosen tag through the Docker Engine API.
- It inspects its own container to capture the current
Config,HostConfig, and network attachments. - It creates a helper container (named
<original>-curiosity-updater, networknone,AutoRemove: true) and starts it. - The helper waits ~5 s so the HTTP response that triggered the update flushes, then stops + removes the old container and creates a replacement under the same name with the new image and the captured config.
- The daemon re-attaches the same networks. Static IPs (
IPAMConfig) are preserved; service discovery by container name keeps working because the name is reused.
The workspace is unreachable for the duration of the swap — usually a few seconds, plus the new container's startup time.
Requirements
For the page to be functional:
- The workspace must be running inside a container (
/.dockerenvpresent, orDOTNET_RUNNING_IN_CONTAINER=true). - The Docker Engine socket must be bind-mounted into the container — by default at
/var/run/docker.sock. Override the path withMSK_DOCKER_SOCKET. - The current user inside the container must be able to read/write the socket.
- The configured
MSK_DOCKER_IMAGE_REPOSITORYmust be reachable on Docker Hub (or whichever registry serves it).
A minimal docker run:
docker run --name curiosity \
-p 127.0.0.1:8080:8080 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/curiosity/storage:/data \
-e MSK_GRAPH_STORAGE=/data/curiosity \
curiosityai/curiosity:66474
The only addition over the local development command is the second -v that bind-mounts the Docker socket.
The equivalent Compose snippet:
services:
curiosity:
image: curiosityai/curiosity:66474
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- curiosity-data:/data
environment:
MSK_GRAPH_STORAGE: /data/curiosity
# Optional: pin a non-default repository (e.g. private mirror).
# MSK_DOCKER_IMAGE_REPOSITORY: registry.example.com/curiosity/workspace
Mounting the Docker socket grants root-equivalent host access
Anything that can talk to /var/run/docker.sock can launch privileged containers, mount any host path, and read every other container on the host. Only mount it when you trust every system admin of the workspace — the page is gated by the AuthorizeSystemAdmin policy, but the socket itself has no finer-grained ACL. For multi-tenant or PCI/HIPAA environments, leave it unmounted and set MSK_DISABLE_DOCKER_UPDATE=true.
Picking a tag
Only numeric build tags are listed in the UI — latest and nightly are intentionally excluded because the page is about pinning to a concrete build. The dropdown shows tags newer than the running build, newest-first, with their push date and compressed size when the registry reports them.
The build number for the running container comes from MSK_BUILD_NUMBER, which is baked into every official image. If the workspace cannot determine the current build (e.g. when running an unofficial build), every tag in the repository is offered.
Disabling the feature
Set MSK_DISABLE_DOCKER_UPDATE=true. The admin sidebar entry disappears, the controller refuses status/list/update calls, and the helper-container path is unreachable. Use this whenever the deployment is managed by an external upgrade pipeline.
Troubleshooting
| Symptom | What to check |
|---|---|
| Page shows "Not running inside a Docker container — updates are managed by your installer." | The container is not detected. Set DOTNET_RUNNING_IN_CONTAINER=true or run the official image. |
| Page shows "The Docker socket (…) is not reachable." | Add -v /var/run/docker.sock:/var/run/docker.sock. If the socket lives elsewhere on the host, mount it and point MSK_DOCKER_SOCKET at the in-container path. |
| Page shows "Could not identify this container via the Docker API." | Container id auto-detection failed. Set MSK_CONTAINER_ID to the long id from docker inspect. |
| Update appears to start but the container never comes back. | Inspect the helper container's logs before it auto-removes: docker logs <name>-curiosity-updater. The helper prints [docker-swap] progress lines and the final error if the create/start step failed. |
| "Refusing to use suspicious image repository '…'." | MSK_DOCKER_IMAGE_REPOSITORY must match ^[A-Za-z0-9][A-Za-z0-9_./-]{0,255}$. Remove tag/digest suffixes — the repository is the part before the :. |
REST API
The page is a thin wrapper over three endpoints under /api/update/docker/, all gated by AuthorizeSystemAdmin. Useful for scripting upgrades without driving the UI:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/update/docker/status |
Current container snapshot and whether a self-update is possible. |
GET |
/api/update/docker/available |
Status plus the list of newer numeric tags. |
POST |
/api/update/docker/update |
Body { "tag": "<tag>" } — kicks off the updater helper. Returns immediately; the swap happens a few seconds later. |
Upgrading
Pinning a version means upgrades are deliberate. From the host:
docker compose pull
docker compose up -d
Or from inside the workspace, use the Software Updates admin page when the Docker socket is mounted.
For breaking changes, follow Upgrades and migrations: take a backup first, run on staging, validate, then promote.
Common pitfalls
- Ephemeral storage — running without
-vor with a tmpfs mount loses data on container restart. Always mount a real volume onMSK_GRAPH_STORAGE. - No
MSK_PUBLIC_ADDRESSbehind a proxy — generated links (SSO callback URLs, email links) point at the internal hostname instead of the user-facing one. - Insufficient memory — the workspace process maps indexes into memory. OOM-killed containers leave the indexes in a recovery state on the next boot. Size accordingly.
See also
- Configuration reference — every
MSK_*variable. - Kubernetes — when you outgrow a single host.
- Backup and restore — what to do when something goes wrong.
- Troubleshooting — install and startup failures.