Self-hosted markdown notes with fast search, tags, attachments, and zero database.
CopyCat is a distraction-free note app for people who want plain markdown files, fast full-text search, and a simple self-hosted setup. Notes stay on disk as regular files, metadata lives in a lightweight hidden app directory, and the search index is rebuildable cache rather than a database.
Keywords: self-hosted, markdown, notes, wiki, search, tags, attachments, docker, fastapi, vue
- Plain markdown storage
- Fast full-text search
- Custom tags and favorites
- Attachments
- Raw and WYSIWYG editor modes
- Wikilinks like
[[Another Note]] - Mobile-friendly UI
- Light and dark themes
- Authentication modes: none, read-only, password, TOTP
- API docs exposed at
/docs - Multi-group access model for managed users
docker build -t ghcr.io/yanadevops/copycat:1.4.7 .docker run -d \
--name copycat \
-e PUID=1000 \
-e PGID=1000 \
-e COPYCAT_AUTH_TYPE=password \
-e COPYCAT_USERNAME=admin \
-e COPYCAT_PASSWORD='changeMe!' \
-e COPYCAT_SECRET_KEY='replace-this-with-a-long-random-secret' \
-v "$(pwd)/data:/data" \
-p 8080:8080 \
ghcr.io/yanadevops/copycat:1.4.7Open http://localhost:8080.
services:
copycat:
container_name: copycat
build: .
image: ghcr.io/yanadevops/copycat:1.4.7
environment:
PUID: 1000
PGID: 1000
COPYCAT_AUTH_TYPE: password
COPYCAT_USERNAME: admin
COPYCAT_PASSWORD: changeMe!
COPYCAT_SECRET_KEY: replace-this-with-a-long-random-secret
volumes:
- ./data:/data
ports:
- "8080:8080"
restart: unless-stoppedStart it with:
docker compose up --build -dThe repository includes a Helm chart at helm/copycat for Kubernetes deployments.
What the chart creates:
DeploymentorStatefulSetServicePersistentVolumeClaimor StatefulSetvolumeClaimTemplate- headless
ServiceforStatefulSet - auth
Secretor support for an existing Secret - optional
Ingress
Important defaults:
controller.type=Deployment- single replica
Recreatedeployment strategy- persistent data mounted at
/data
These defaults are intentional for a stateful single-volume setup.
helm upgrade --install copycat ./helm/copycat \
--namespace copycat \
--create-namespace \
--set image.repository=your-registry/copycat \
--set image.tag=1.4.7 \
--set auth.username=admin \
--set auth.password='changeMe!' \
--set auth.secretKey='replace-this-with-a-long-random-secret'Use this when you want a StatefulSet-managed pod identity. For new installs, the cleanest option is a volumeClaimTemplate.
Keep replicaCount: 1 for the normal single-volume setup.
controller:
type: StatefulSet
persistence:
volumeClaimTemplate:
enabled: true
size: 10GiInstall it with:
helm upgrade --install copycat ./helm/copycat \
--namespace copycat \
--create-namespace \
-f ./helm/copycat/values-statefulset.yaml \
--set image.repository=your-registry/copycat \
--set image.tag=1.4.7 \
--set auth.username=admin \
--set auth.password='changeMe!' \
--set auth.secretKey='replace-this-with-a-long-random-secret'persistence:
existingClaim: copycat-dataThis also works with controller.type=StatefulSet if you already have a claim and do not want Helm to create per-pod claims.
The existing Secret should expose these keys when auth.type is password or totp:
COPYCAT_USERNAMECOPYCAT_PASSWORDCOPYCAT_SECRET_KEYCOPYCAT_TOTP_KEYfor TOTP only
Example:
auth:
type: password
existingSecret: copycat-authingress:
enabled: true
className: nginx
hosts:
- host: copycat.example.com
paths:
- path: /
pathType: PrefixIf you publish CopyCat under a subpath, set both the ingress path and pathPrefix:
pathPrefix: /copycat
ingress:
enabled: true
className: nginx
hosts:
- host: example.com
paths:
- path: /copycat
pathType: PrefixIf you already have a persistent /data volume, the app keeps using the same data. Root metadata from an explicit legacy .flatnotes directory is copied into /data/.copycat automatically only when /data/.copycat/metadata.json does not already exist.
Keep CopyCat at a single replica for the normal single-volume setup. The app is designed around one writable notes directory and one writable search index.
Prefer Kubernetes fsGroup and fsGroupChangePolicy: OnRootMismatch for volume
ownership. Avoid custom init containers that recursively modify or recreate
/data, and never run cleanup commands against the mounted data directory.
If a pod starts with an unexpectedly empty /data, stop the workload before
restoring data and inspect the PVC/PV and CSI node logs. CopyCat logs the
resolved notes, metadata, and index paths on startup to make mount mismatches
easier to diagnose.
CopyCat also validates the data root at startup. A brand-new empty volume is
allowed and receives /data/.copycat/install.json. If a later start sees only
cache/index files or sees that previously known durable data disappeared, the
default COPYCAT_DATA_GUARD_MODE=fail_existing stops startup before new data is
written. Use COPYCAT_DATA_GUARD_MODE=warn only for emergency inspection.
For Velero/Kopia backups, verify that CopyCat pod volume backups are non-empty:
kubectl -n velero get podvolumebackups.velero.io \
-l velero.io/backup-name=<backup-name> \
-o wide | grep copycat
kubectl -n velero describe podvolumerestores.velero.io \
-l velero.io/restore-name=<restore-name>CopyCat keeps the note viewer lightweight for slower laptops and mobile devices. The markdown editor is loaded only when a note enters edit mode, code highlighting assets are loaded only for notes that contain fenced code blocks, the home timeline renders notes in batches, and the page background uses static image textures instead of live blur/gradient effects.
Search result previews keep highlighted matches but normalize the preview markup so compact note cards stay within their fixed layout.
| Variable | Required | Default | Purpose |
|---|---|---|---|
COPYCAT_AUTH_TYPE |
No | password |
Auth mode: none, read_only, password, totp |
COPYCAT_USERNAME |
Password/TOTP | - | Bootstrap admin username |
COPYCAT_PASSWORD |
Password/TOTP | - | Bootstrap admin password |
COPYCAT_SECRET_KEY |
Password/TOTP | - | Session signing secret |
COPYCAT_TOTP_KEY |
TOTP only | - | TOTP seed for admin login |
COPYCAT_PATH |
No | /data |
Data directory inside the container |
COPYCAT_HOST |
No | 0.0.0.0 |
Bind address |
COPYCAT_PORT |
No | 8080 |
HTTP port |
COPYCAT_DATA_GUARD_MODE |
No | fail_existing |
Data-root guard mode: fail_existing, warn, or off |
PUID |
No | 1000 |
Runtime user ID for mounted volumes |
PGID |
No | 1000 |
Runtime group ID for mounted volumes |
| Variable | Default | Purpose |
|---|---|---|
COPYCAT_PATH_PREFIX |
empty | Serve the app behind a reverse-proxy subpath |
COPYCAT_QUICK_ACCESS_HIDE |
false |
Hide the quick-access block on the home page |
COPYCAT_QUICK_ACCESS_TITLE |
RECENTLY MODIFIED |
Custom home page block title |
COPYCAT_QUICK_ACCESS_TERM |
* |
Custom search term for the home page block |
COPYCAT_QUICK_ACCESS_SORT |
lastModified |
Sort mode for the home page block |
COPYCAT_QUICK_ACCESS_LIMIT |
4 |
Number of items in the home page block |
COPYCAT_LOGIN_RATE_LIMIT_ENABLED |
true |
Enable login rate limiting |
COPYCAT_LOGIN_RATE_LIMIT_WINDOW_SECONDS |
60 |
Login rate-limit window |
COPYCAT_LOGIN_RATE_LIMIT_IP_MAX |
10 |
Max failed attempts per IP |
COPYCAT_LOGIN_RATE_LIMIT_USER_IP_MAX |
5 |
Max failed attempts per username and IP |
COPYCAT_CSP_MODE |
report-only |
Content Security Policy mode |
COPYCAT_MAX_ATTACHMENT_BYTES |
26214400 |
Max attachment size in bytes |
COPYCAT_ATTACHMENT_BLOCK_ACTIVE_CONTENT |
false |
Block risky attachment types |
COPYCAT_ATTACHMENT_BLOCKED_EXTENSIONS |
safe default list | Override blocked file extensions |
COPYCAT_SET_HTTPONLY_AUTH_COOKIE |
false |
Store auth token in an HTTP-only cookie |
COPYCAT_LEGACY_METADATA_DIRS |
.flatnotes |
Comma-separated legacy metadata dirs to copy from on startup |
CopyCat always logs to stdout/stderr for kubectl logs and Docker logs. It also
writes rotating application logs to /data/.copycat/logs/copycat.log by
default, so the most important startup and runtime errors survive pod restarts.
| Variable | Default | Purpose |
|---|---|---|
COPYCAT_LOG_LEVEL |
INFO |
Application and access log level |
COPYCAT_LOG_TO_FILE |
true |
Enable persistent file logging |
COPYCAT_LOG_FILE |
/data/.copycat/logs/copycat.log |
Persistent log file path |
COPYCAT_LOG_RETENTION_DAYS |
7 |
Daily log retention window |
COPYCAT_LOG_MAX_BYTES |
10485760 |
Rotate early when the active log exceeds this size |
COPYCAT_LOG_BACKUP_COUNT |
7 |
Maximum rotated files to keep |
COPYCAT_ACCESS_LOG |
true |
Enable HTTP access logs, excluding /health |
Everything under /data persists across container restarts.
/data/
.copycat/
auth/
groups.json
users.json
metadata.json
index/
attachments/
groups/
<group-slug>/
notes/
attachments/
.copycat/
metadata.json
index/
Notes:
- Bootstrap admin credentials come from environment variables, not from files inside
/data. - Managed users and groups are stored inside
/data/.copycat/auth. - Favorites, tags, and note metadata are stored in
metadata.json. - Search index files are cache and can be rebuilt.
- Explicit legacy root metadata directories are copied into
.copycatonly when configured and only if target metadata is missing.
Useful checks inside the container:
cd /data
ls -la
ls -la /data/.copycat
ls -la /data/.copycat/auth
cat /data/.copycat/metadata.json
cat /data/.copycat/auth/groups.json
cat /data/.copycat/auth/users.json
ls -la /data/groups/<group-slug>/.copycat
cat /data/groups/<group-slug>/.copycat/metadata.jsonnpm install
npm run devnpm run buildThe backend runs with FastAPI and Uvicorn through the container entrypoint. For local container-based development:
docker compose up --buildIssues and pull requests are welcome. Read CONTRIBUTING.md before opening a PR.
If CopyCat saves you time, you can support the project here:
This project is released under the MIT License.
- Whoosh for search indexing
- TOAST UI Editor for markdown editing




