Beautiful note-taking app with real-time collaboration, file attachments, OnlyOffice document editing, version history, sharing, 2FA, and more.
┌─────────────┐ ┌──────────────┐
│ Browser │────▶│ Cowrite App │────▶ SQLite
└─────────────┘ │ (Node.js) │
│ └──────┬───────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ OnlyOffice │◀────│ Internal │
│ Document │ │ Proxy /oo │
│ Server │ └──────────────┘
└─────────────┘
The app proxies /oo/* requests to OnlyOffice's internal server. When using useDirectStorageUrls: true, OnlyOffice generates direct-storage URLs so the browser loads cached document assets directly from the public domain.
services:
app:
image: certyiknofetch/cowrite:0.1
environment:
JWT_SECRET: <generate a 64-char hex string>
ONLYOFFICE_SECRET: <generate a 32-char hex string>
FILE_ENCRYPTION_KEY: <generate a 64-char hex string>
PORT: "3000"
APP_INTERNAL_URL: "https://your-domain.com"
PUBLIC_OO_URL: "/oo"
ONLYOFFICE_INTERNAL_URL: "http://onlyoffice:80"
CORS_ORIGIN: "https://your-domain.com"
NODE_ENV: "production"
JSON_PAYLOAD_LIMIT: "10 MB"
UPLOAD_FILE_LIMIT: "100 MB"
volumes:
- ./data:/app/data
- ./uploads:/app/uploads
depends_on:
- onlyoffice
restart: unless-stopped
networks:
cowrite:
ipv4_address: 172.29.99.2
onlyoffice:
image: onlyoffice/documentserver:latest
expose:
- "80"
entrypoint:
- /bin/bash
- -c
- sed -i 's/"useDirectStorageUrls": false/"useDirectStorageUrls": true/'
/etc/onlyoffice/documentserver/default.json
&& exec /app/ds/run-document-server.sh
environment:
JWT_ENABLED: "true"
JWT_SECRET: <same as ONLYOFFICE_SECRET above>
volumes:
- oo_data:/var/www/onlyoffice/Data
- oo_logs:/var/log/onlyoffice
restart: unless-stopped
networks:
cowrite:
ipv4_address: 172.29.99.3
networks:
cowrite:
driver: bridge
ipam:
config:
- subnet: 172.29.99.0/29
gateway: 172.29.99.1
volumes:
oo_data:
oo_logs:
Generate secrets — run these commands to create strong random keys:
JWT_SECRET=$(openssl rand -hex 64)
ONLYOFFICE_SECRET=$(openssl rand -hex 32)
FILE_ENCRYPTION_KEY=$(openssl rand -hex 64)
echo "JWT_SECRET=$JWT_SECRET"
echo "ONLYOFFICE_SECRET=$ONLYOFFICE_SECRET"
echo "FILE_ENCRYPTION_KEY=$FILE_ENCRYPTION_KEY"
Replace the placeholders in docker-compose.yml with the generated secrets.
Configure your reverse proxy to forward https://your-domain.com to http://172.29.99.2:3000.
For Nginx Proxy Manager:
your-domain.com172.29.99.23000proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
Start the stack:
docker compose up -d
Open https://your-domain.com and register your account.
The compose file uses a static private subnet (172.29.99.0/29) with no host port exposure:
| Service | Internal IP | Port |
|---|---|---|
| App | 172.29.99.2 | 3000 |
| OnlyOffice | 172.29.99.3 | 80 |
Your reverse proxy runs on the host network and proxies requests to these static IPs. This ensures the proxy never breaks on container restart (Docker's bridge network assigns the same IP via ipam config).
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET | Yes | — | 64-char hex string for JWT signing |
ONLYOFFICE_SECRET | Yes | — | 32-char hex string shared with OnlyOffice |
FILE_ENCRYPTION_KEY | Yes | — | 64-char hex string for AES-256-CBC file encryption |
PORT | No | 3000 | Internal port |
APP_INTERNAL_URL | Yes | — | Your public domain (e.g. https://notes.example.com) |
PUBLIC_OO_URL | No | /oo | URL prefix for OnlyOffice proxy |
ONLYOFFICE_INTERNAL_URL | No | http://onlyoffice:80 | Internal OnlyOffice address |
CORS_ORIGIN | Yes | — | Same as APP_INTERNAL_URL |
NODE_ENV | No | production | Environment mode |
JSON_PAYLOAD_LIMIT | No | 500 MB | Max JSON request body size |
UPLOAD_FILE_LIMIT | No | 500 MB | Max file upload size |
SESSION_SECRET | No | auto-generated | Session cookie secret |
ALTCHA_HMAC_SECRET | No | random (reset on restart) | CAPTCHA HMAC key |
ALTCHA_HMAC_KEY_SECRET | No | random (reset on restart) | CAPTCHA key |
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_ENABLED | No | true | Enable JWT auth |
JWT_SECRET | Yes | — | Must match app's ONLYOFFICE_SECRET |
| Category | Formats |
|---|---|
| Images | JPG, JPEG, PNG, GIF, WebP, BMP, TIFF, TIF, HEIC, HEIF, PSD, AI |
| Audio | MP3, WAV, OGG, FLAC, AAC, WMA, M4A, Opus |
| Video | MP4, WebM, AVI, MKV, MOV, WMV, FLV, M4V |
| Documents | PDF, DOCX, DOC, XLSX, XLS, PPTX, PPT, ODT, ODS, ODP |
| E-books / Data | EPUB, RTF, IPYNB, TXT, CSV, JSON, XML, MD, LOG, YAML, YML |
| Archives | ZIP, 7z, RAR, TAR, TGZ, GZ, BZ2, XZ, APK |
| Type | View Action |
|---|---|
| Images | Rendered inline via <img> |
| Audio | Inline player with <audio> |
| Video | Inline player with <video> |
| In-browser PDF viewer | |
| DOCX, DOC, ODT | Rendered to HTML + OnlyOffice edit |
| XLSX, XLS, ODS | Rendered to HTML table + OnlyOffice edit |
| PPTX, PPT, ODP | Rendered slide preview + OnlyOffice edit |
| EPUB | Full reader with chapter navigation (TOC sidebar) |
| RTF | Rendered with basic formatting (bold, italic, underline) |
| CSV | Sortable HTML table |
| MD | Rendered Markdown with edit toggle |
| IPYNB | Rendered notebook |
| Archives (ZIP, 7z, RAR, etc.) | File listing with size/compressed columns |
| HEIC, HEIF, PSD, AI | Converted to JPEG on-the-fly via Sharp |
| Editor | Formats |
|---|---|
| OnlyOffice | DOCX, DOC, XLSX, XLS, PPTX, PPT, ODT, ODS, ODP |
| Text editor | MD, TXT, CSV |
sql.js — stored in ./data/ volume./uploads/ volume and served via streaming decryptionoo_data and oo_logsThe certyiknofetch/cowrite image supports both:
linux/amd64 — Intel/AMD servers, desktops, VPSlinux/arm64 — Raspberry Pi, Mac M-series, ARM serversDocker automatically selects the correct architecture when pulling.
sanitize-html on all rendered contentMIT
Content type
Image
Digest
sha256:dd5a3056b…
Size
114.3 MB
Last updated
3 months ago
docker pull certyiknofetch/cowrite