docker: fix compose self-hosting and add external Mongo/MinIO/Garage examples (#106)

* docker: update .env and docker-compose.yml for SMTP and new services

* docker: add external MinIO, MongoDB, and GarageHQ examples with setup instructions

* docs: update README with dotenv docker setup instructions and configuration redirect for external services

* docker: update service dependency condition for setup-themes in docker-compose.yml

* chore: update example URLs in .env for public services
This commit is contained in:
Ibaraki Douji
2026-07-29 09:07:52 +05:00
committed by GitHub
parent 7ce7f02646
commit 3c7327d3c7
20 changed files with 663 additions and 18 deletions
+50
View File
@@ -0,0 +1,50 @@
# Docker Compose Examples
Reference deployments that split infrastructure from the Notesnook stack. Each example overrides the root [`docker-compose.yml`](../docker-compose.yml) instead of duplicating it.
## Pattern
Run all commands from the **repository root**.
| Step | Compose files | Role |
|------|---------------|------|
| 1. Infra | `examples/<example>/infra.compose.yml` | MongoDB, MinIO, or Garage on `notesnook-shared` |
| 2. Notesnook | `docker-compose.yml` + `examples/<example>/notesnook.override.yml` | Disables embedded services; patches connection strings |
```bash
# 1. Merge example vars into root .env (see examples/<example>/.env.example)
# 2. Start infrastructure
docker compose -f examples/<example>/infra.compose.yml up -d
# 3. Start Notesnook (one-time setup for garage — see example README)
docker compose -f docker-compose.yml -f examples/<example>/notesnook.override.yml up -d
# Optional extras
docker compose -f docker-compose.yml -f examples/<example>/notesnook.override.yml --profile extras up -d
# Tear down (reverse order)
docker compose -f docker-compose.yml -f examples/<example>/notesnook.override.yml down
docker compose -f examples/<example>/infra.compose.yml down
```
## Environment
The root [`.env`](../.env) is loaded automatically when running from the repository root. Each example ships a **delta-only** [`.env.example`](external-mongodb/.env.example) listing only variables that differ — merge those lines into your root `.env`.
Infra-only variables (e.g. `GARAGE_RPC_SECRET`) are in `infra.env.example` where applicable; pass with `--env-file` or merge into root `.env`.
## Examples
| Example | External dependency | Notes |
|---------|---------------------|-------|
| [external-mongodb](external-mongodb/) | MongoDB | Embedded MinIO stays enabled |
| [external-minio](external-minio/) | MinIO | Embedded MongoDB stays enabled |
| [garage](garage/) | [GarageHQ](https://garagehq.deuxfleurs.fr/) S3 | Manual bucket setup via `setup-garage.sh` |
## Shared network
Infra compose files create **`notesnook-shared`**. Override files attach Notesnook services to it as an external network. Run only one infra example at a time on the same host.
## Root compose
The all-in-one stack: [`docker-compose.yml`](../docker-compose.yml) (no override file).
+6
View File
@@ -0,0 +1,6 @@
# Merge these into the repository root .env (see /.env for all other variables).
#
# MINIO_ROOT_USER and MINIO_ROOT_PASSWORD must match examples/external-minio/infra.env.example
# when starting the external MinIO infra stack.
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
+43
View File
@@ -0,0 +1,43 @@
# External MinIO
MinIO runs in a separate compose project. The Notesnook stack uses the root [`docker-compose.yml`](../../docker-compose.yml) with [`notesnook.override.yml`](notesnook.override.yml), which disables embedded `notesnook-s3` / `setup-s3` and points at external MinIO. Embedded MongoDB is unchanged.
## Setup
From the **repository root**:
```bash
# Ensure MINIO_* in root .env match infra.env.example
# Merge notesnook delta if needed (see .env.example)
# 1. MinIO + bucket setup
docker compose -f examples/external-minio/infra.compose.yml \
--env-file examples/external-minio/infra.env.example up -d
# Wait for notesnook-minio-setup to complete
docker compose -f examples/external-minio/infra.compose.yml ps -a
# 2. Notesnook
docker compose -f docker-compose.yml -f examples/external-minio/notesnook.override.yml up -d
```
## Environment
| File | Purpose |
|------|---------|
| [`infra.env.example`](infra.env.example) | `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` for infra stack |
| [`.env.example`](.env.example) | Notesnook delta (credentials must match infra) |
Merge `MINIO_*` into root [`.env`](../../.env) so the sync server uses the same credentials.
## Stop
```bash
docker compose -f docker-compose.yml -f examples/external-minio/notesnook.override.yml down
docker compose -f examples/external-minio/infra.compose.yml down
```
## Notes
- Bucket `attachments` is created by `notesnook-minio-setup` on first infra start.
- `S3_INTERNAL_SERVICE_URL` is overridden to `http://notesnook-minio:9000`.
+47
View File
@@ -0,0 +1,47 @@
name: notesnook-external-minio-infra
services:
notesnook-minio:
image: minio/minio:RELEASE.2024-07-29T22-14-52Z
hostname: notesnook-minio
ports:
- 9000:9000
networks:
- notesnook-shared
volumes:
- s3data:/data/s3
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
MINIO_BROWSER: "on"
command: server /data/s3 --console-address :9090
healthcheck:
test: timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1
interval: 40s
timeout: 30s
retries: 3
start_period: 60s
notesnook-minio-setup:
image: minio/mc:RELEASE.2024-07-26T13-08-44Z
depends_on:
notesnook-minio:
condition: service_healthy
networks:
- notesnook-shared
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
command:
- /bin/bash
- -c
- |
mc alias set minio http://notesnook-minio:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin}
mc mb minio/attachments -p
networks:
notesnook-shared:
name: notesnook-shared
volumes:
s3data:
@@ -0,0 +1,2 @@
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
@@ -0,0 +1,20 @@
services:
notesnook-s3:
profiles: [embedded-disabled]
setup-s3:
profiles: [embedded-disabled]
notesnook-server:
networks:
- notesnook
- notesnook-shared
depends_on: !override
identity-server:
condition: service_started
environment:
S3_INTERNAL_SERVICE_URL: "http://notesnook-minio:9000"
networks:
notesnook-shared:
external: true
+4
View File
@@ -0,0 +1,4 @@
# Merge these into the repository root .env (see /.env for all other variables).
MONGODB_CONNECTION_STRING_IDENTITY=mongodb://notesnook-mongodb:27017/identity?replSet=rs0
MONGODB_CONNECTION_STRING=mongodb://notesnook-mongodb:27017/?replSet=rs0
+36
View File
@@ -0,0 +1,36 @@
# External MongoDB
MongoDB runs in a separate compose project. The Notesnook stack uses the root [`docker-compose.yml`](../../docker-compose.yml) with [`notesnook.override.yml`](notesnook.override.yml), which disables the embedded `notesnook-db` service and points at external Mongo. Embedded MinIO is unchanged.
## Setup
From the **repository root**:
```bash
# Merge into root .env
cat examples/external-mongodb/.env.example >> .env
# set NOTESNOOK_API_SECRET and other required root .env values
# 1. MongoDB
docker compose -f examples/external-mongodb/infra.compose.yml up -d
# 2. Notesnook
docker compose -f docker-compose.yml -f examples/external-mongodb/notesnook.override.yml up -d
```
## Environment (delta)
See [`.env.example`](.env.example) — only `MONGODB_CONNECTION_STRING*` overrides are needed beyond the root `.env`.
## Stop
```bash
docker compose -f docker-compose.yml -f examples/external-mongodb/notesnook.override.yml down
docker compose -f examples/external-mongodb/infra.compose.yml down
```
## Notes
- Replica set `rs0` is required; the infra healthcheck initializes it.
- Host port `27017` is exposed for debugging.
- If infra stops, `identity-server` and `notesnook-server` fail until MongoDB is back.
@@ -0,0 +1,26 @@
name: notesnook-external-mongodb-infra
services:
notesnook-mongodb:
image: mongo:7.0.12
hostname: notesnook-mongodb
ports:
- 27017:27017
volumes:
- dbdata:/data/db
networks:
- notesnook-shared
command: --replSet rs0 --bind_ip_all
healthcheck:
test: echo 'try { rs.status() } catch (err) { rs.initiate() }; db.runCommand("ping").ok' | mongosh mongodb://localhost:27017 --quiet
interval: 40s
timeout: 30s
retries: 3
start_period: 60s
networks:
notesnook-shared:
name: notesnook-shared
volumes:
dbdata:
@@ -0,0 +1,31 @@
services:
notesnook-db:
profiles: [embedded-disabled]
identity-server:
depends_on: !override
validate:
condition: service_completed_successfully
networks:
- notesnook
- notesnook-shared
environment:
MONGODB_CONNECTION_STRING: ${MONGODB_CONNECTION_STRING_IDENTITY}
notesnook-server:
networks:
- notesnook
- notesnook-shared
depends_on: !override
notesnook-s3:
condition: service_started
setup-s3:
condition: service_started
identity-server:
condition: service_started
environment:
MONGODB_CONNECTION_STRING: ${MONGODB_CONNECTION_STRING}
networks:
notesnook-shared:
external: true
+6
View File
@@ -0,0 +1,6 @@
# Merge these into the repository root .env (see /.env for all other variables).
# Fill after running examples/garage/setup-garage.sh
GARAGE_ACCESS_KEY_ID=
GARAGE_ACCESS_KEY_SECRET=
ATTACHMENTS_SERVER_PUBLIC_URL=http://localhost:3900
+57
View File
@@ -0,0 +1,57 @@
# GarageHQ S3
[Garage](https://garagehq.deuxfleurs.fr/) replaces embedded MinIO. The Notesnook stack uses the root [`docker-compose.yml`](../../docker-compose.yml) with [`notesnook.override.yml`](notesnook.override.yml). MongoDB stays embedded.
Based on [PR #79](https://github.com/streetwriters/notesnook-sync-server/pull/79), with community review adjustments:
- Notesnook uses **presigned URLs** — no public bucket policy required.
- Do **not** use `aws s3api put-bucket-policy` for public read ([Garage S3 compatibility](https://garagehq.deuxfleurs.fr/documentation/reference-manual/s3-compatibility/)).
- Use port **3900** (S3 API) for `ATTACHMENTS_SERVER_PUBLIC_URL` when not behind a reverse proxy.
## Setup
From the **repository root**:
```bash
# 1. Generate RPC secret and add to root .env or use --env-file
openssl rand -hex 32
# add GARAGE_RPC_SECRET=... to .env (see infra.env.example)
# 2. Garage infra
docker compose -f examples/garage/infra.compose.yml up -d
# 3. One-time bucket/key setup (from repo root)
./examples/garage/setup-garage.sh
# 4. Merge keys into root .env (see .env.example)
# 5. Notesnook
docker compose -f docker-compose.yml -f examples/garage/notesnook.override.yml up -d
```
`notesnook-server` may stay unhealthy until valid `GARAGE_ACCESS_KEY_*` values are in root `.env`:
```bash
docker compose -f docker-compose.yml -f examples/garage/notesnook.override.yml up -d --force-recreate notesnook-server
```
## Environment
| File | Purpose |
|------|---------|
| [`infra.env.example`](infra.env.example) | `GARAGE_RPC_SECRET` for infra |
| [`.env.example`](.env.example) | `GARAGE_ACCESS_KEY_*` and `ATTACHMENTS_SERVER_PUBLIC_URL` (merge into root `.env`) |
## Stop
```bash
docker compose -f docker-compose.yml -f examples/garage/notesnook.override.yml down
docker compose -f examples/garage/infra.compose.yml down
```
## Manual setup
If `setup-garage.sh` fails, follow PR #79 layout/bucket/key steps manually. Skip public bucket policy. Optional bucket alias only for Garage web port 3902.
## MongoDB
This example only externalizes S3. For external MongoDB, combine with [external-mongodb](../external-mongodb/) or use the root all-in-one compose.
+21
View File
@@ -0,0 +1,21 @@
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
replication_factor = 1
consistency_mode = "consistent"
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
[s3_api]
s3_region = "us-east-1"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.garage"
[admin]
api_bind_addr = "[::]:3903"
+31
View File
@@ -0,0 +1,31 @@
name: notesnook-garage-infra
services:
notesnook-garage:
image: dxflrs/garage:v2.3.0
hostname: notesnook-garage
restart: unless-stopped
ports:
- 3900:3900
- 3901:3901
- 3902:3902
networks:
- notesnook-shared
volumes:
- s3data:/var/lib/garage
- ./garage.toml:/etc/garage.toml:ro
environment:
GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET}
healthcheck:
test: ["CMD", "/garage", "status"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
notesnook-shared:
name: notesnook-shared
volumes:
s3data:
+2
View File
@@ -0,0 +1,2 @@
# Generate with: openssl rand -hex 32
GARAGE_RPC_SECRET=
+22
View File
@@ -0,0 +1,22 @@
services:
notesnook-s3:
profiles: [embedded-disabled]
setup-s3:
profiles: [embedded-disabled]
notesnook-server:
networks:
- notesnook
- notesnook-shared
depends_on: !override
identity-server:
condition: service_started
environment:
S3_INTERNAL_SERVICE_URL: "http://notesnook-garage:3900"
S3_ACCESS_KEY_ID: "${GARAGE_ACCESS_KEY_ID}"
S3_ACCESS_KEY: "${GARAGE_ACCESS_KEY_SECRET}"
networks:
notesnook-shared:
external: true
+107
View File
@@ -0,0 +1,107 @@
#!/bin/bash
set -euo pipefail
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
BUCKET="${GARAGE_BUCKET:-attachments}"
ZONE="${GARAGE_ZONE:-us-east-1}"
CAPACITY="${GARAGE_CAPACITY:-10G}"
KEY_NAME="${GARAGE_KEY_NAME:-notesnook-key}"
ENDPOINT="${GARAGE_S3_ENDPOINT:-http://127.0.0.1:3900}"
echo -e "${GREEN}--- Notesnook Garage setup ---${NC}"
CONTAINER_NAME="${GARAGE_CONTAINER_NAME:-}"
if [ -z "$CONTAINER_NAME" ]; then
CONTAINER_NAME=$(docker ps --format '{{.Names}}' | grep -E 'notesnook-garage|garage' | head -n 1 || true)
fi
if [ -z "$CONTAINER_NAME" ]; then
echo -e "${RED}Error: no running Garage container found.${NC}"
echo "Start infra first (from repo root): docker compose -f examples/garage/infra.compose.yml up -d"
exit 1
fi
echo -e "Using container: ${GREEN}${CONTAINER_NAME}${NC}"
echo -e "\n${YELLOW}[1/5] Layout${NC}"
NODE_ID=""
for _ in $(seq 1 10); do
NODE_ID=$(docker exec "$CONTAINER_NAME" /garage node id -q | cut -d '@' -f 1 || true)
[ -n "$NODE_ID" ] && break
sleep 2
done
if [ -z "$NODE_ID" ]; then
echo -e "${RED}Error: could not read Garage node id.${NC}"
exit 1
fi
echo "Node ID: $NODE_ID"
docker exec "$CONTAINER_NAME" /garage layout assign -z "$ZONE" -c "$CAPACITY" "$NODE_ID"
echo "yes" | docker exec -i "$CONTAINER_NAME" /garage layout apply --version 1
echo -e "\n${YELLOW}[2/5] Bucket${NC}"
docker exec "$CONTAINER_NAME" /garage bucket create "$BUCKET" || true
if [ -n "${GARAGE_BUCKET_ALIAS:-}" ]; then
echo "Aliasing bucket to ${GARAGE_BUCKET_ALIAS} (optional website access)"
docker exec "$CONTAINER_NAME" /garage bucket alias "$BUCKET" "$GARAGE_BUCKET_ALIAS"
fi
echo -e "\n${YELLOW}[3/5] API key${NC}"
KEY_INFO=$(docker exec "$CONTAINER_NAME" /garage key create "$KEY_NAME")
echo "$KEY_INFO"
KEY_ID=$(echo "$KEY_INFO" | awk '/Key ID:/ {print $3}')
SECRET_KEY=$(echo "$KEY_INFO" | awk '/Secret key:/ {print $3}')
if [ -z "$KEY_ID" ] || [ -z "$SECRET_KEY" ]; then
echo -e "${RED}Error: failed to parse key id/secret from garage output.${NC}"
exit 1
fi
echo -e "\n${YELLOW}[4/5] Bucket permissions${NC}"
docker exec "$CONTAINER_NAME" /garage bucket allow "$BUCKET" --read --write --owner --key "$KEY_ID"
echo -e "\n${YELLOW}[5/5] CORS (aws-cli)${NC}"
docker run --rm \
--env AWS_ACCESS_KEY_ID="$KEY_ID" \
--env AWS_SECRET_ACCESS_KEY="$SECRET_KEY" \
--add-host=host.docker.internal:host-gateway \
amazon/aws-cli \
--endpoint-url "$ENDPOINT" \
s3api put-bucket-cors --bucket "$BUCKET" --cors-configuration '{
"CORSRules": [
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedOrigins": ["*"],
"ExposeHeaders": ["ETag"]
}
]
}'
cat <<EOF
${GREEN}--- Setup complete ---${NC}
Merge these into the repository root .env (see examples/garage/.env.example):
GARAGE_ACCESS_KEY_ID=$KEY_ID
GARAGE_ACCESS_KEY_SECRET=$SECRET_KEY
ATTACHMENTS_SERVER_PUBLIC_URL=$ENDPOINT
Notes:
- Notesnook uses presigned URLs for attachments; public bucket policies are not required.
- Use the S3 API URL (port 3900) for ATTACHMENTS_SERVER_PUBLIC_URL unless you proxy it.
- Do not use aws s3api put-bucket-policy for public read; Garage does not support this like MinIO.
Then restart the sync server (from repo root):
docker compose -f docker-compose.yml -f examples/garage/notesnook.override.yml up -d --force-recreate notesnook-server
EOF