diff --git a/README.md b/README.md index a36bbc5a..2fef40c3 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ Every stack starts from the same [standard setup](documentation/standard-setup.m | **BentoPDF** | PDF toolkit that merges, splits, compresses, converts, and edits PDFs in your browser. | [Details](services/bentopdf) | | **ClipCascade** | Server that syncs your clipboard across devices, with end-to-end encryption. | [Details](services/clipcascade) | | **Copyparty** | File server with resumable uploads, browser access, and WebDAV. | [Details](services/copyparty) | +| **Cozy** | Personal cloud (now Twake Workplace) for files, photos, notes, and contacts. | [Details](services/cozy) | | **Docmost** | Team wiki with real-time collaborative editing, diagrams, comments, and permissions. | [Details](services/docmost) | | **Donetick** | Task and chore manager for households and small groups. | [Details](services/donetick) | | **DumbDo** | To-do list app that keeps its lists in one file, with no database. | [Details](services/dumbdo) | diff --git a/services/cozy/.env b/services/cozy/.env new file mode 100644 index 00000000..a4973e22 --- /dev/null +++ b/services/cozy/.env @@ -0,0 +1,33 @@ +#URL=https://github.com/tailscale-dev/ScaleTail +# To run a second copy, copy the folder under another name and change SERVICE. + +# Service Configuration +# Service name. Used as hostname in Tailscale and for container naming. +# Use letters and digits only, with no hyphen: cozy-stack joins the instance name and the app name with a hyphen. +# The apps get their own devices, named after the service: the service name, then -home, -settings, or -drive. +SERVICE=cozy +# Docker image URL from container registry. +IMAGE_URL=cozy/cozy-stack:latest + +# Network Configuration +# Port to expose to local network. Uncomment the "ports:" section in compose.yaml to enable. +SERVICEPORT=8080 +# Preferred DNS server for Tailscale. Uncomment the "dns:" section in compose.yaml to enable. +DNS_SERVER=9.9.9.9 + +# Tailscale Configuration +# Auth key from https://console.tailscale.com/admin/settings/keys. See: https://tailscale.com/kb/1085/auth-keys#generate-an-auth-key for instructions. +# The same key is used for all four devices of this stack, so it must be reusable. +TS_AUTHKEY= + +# Time Zone setting for containers. See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +TZ=Europe/Amsterdam + +# CouchDB +COUCHDB_USER=cozy +# Required: password of the CouchDB database. Use letters and digits only, because it is part of an address. Generate it with: openssl rand -hex 24 +COUCHDB_PASSWORD= + +# cozy-stack +# Required: administrator passphrase for the cozy-stack command line, stored as a hash at the first start. Generate it with: openssl rand -hex 24 +COZY_ADMIN_PASSPHRASE= diff --git a/services/cozy/README.md b/services/cozy/README.md new file mode 100644 index 00000000..68a0dca9 --- /dev/null +++ b/services/cozy/README.md @@ -0,0 +1,72 @@ +# Cozy + +[Cozy](https://cozy.io/) (now Twake Workplace) is a self-hosted personal cloud with apps for files, photos, notes, and contacts. The `cozy-stack` server runs the apps, and each app has its own address. + +This stack runs Cozy with a Tailscale sidecar, as described in [the standard setup](../../documentation/standard-setup.md). Because Cozy serves every app on its own host name, it adds one extra Tailnet device per app. + +## At a glance + +| Item | Value | +| ------------- | ------------------------------------------------------------------------------------------------------------------ | +| Web interface | `https://cozy..ts.net` (login and API) | +| Apps | `https://cozy-home..ts.net`, `-settings`, and `-drive`, each its own device | +| Service port | `8080` | +| Images | `cozy/cozy-stack` | +| | `couchdb:3.3` | +| Data | `./cozy-data/app/etc` (admin passphrase hash and keys), `./cozy-data/app/data` (files), `./cozy-data/db` (CouchDB) | + +## Before you start + +Set these values in `.env`: + +- **`SERVICE`.** Keep letters and digits only, with no hyphen. Cozy joins the instance name and the app name with a hyphen, so `my-cozy` is rejected as an instance name. +- **`TS_AUTHKEY`.** The stack starts four Tailnet devices with this key. Use a reusable key, because a single-use key only lets the first device join. +- **`COUCHDB_PASSWORD`.** The password of the database. Use letters and digits only, because it is part of an address. Compose stops with an error until it is set. +- **`COZY_ADMIN_PASSPHRASE`.** The passphrase for the `cozy-stack` command line. The image stores its hash at the first start. Compose stops with an error until it is set. + +Remove any old devices called `cozy`, `cozy-home`, `cozy-settings`, or `cozy-drive` from the [Tailscale admin console](https://login.tailscale.com/admin/machines) first. A device that is still listed makes Tailscale name the new one `cozy-1`, and the addresses below stop matching. + +## Deviations from the standard setup + +- **One device per app.** `cozy-stack` serves every app at `-..ts.net` (the `flat` subdomain layout, set in the `cozy-config` entry of `compose.yaml`), and Tailscale Serve publishes one name per device. The stack therefore runs the `tailscale-home`, `tailscale-settings`, and `tailscale-drive` containers next to `tailscale`, each with its own device and its own state folder `./ts/state-`. They have no `./config` folder, because their Serve configuration needs no file on the host. They forward to `cozy-stack` in the network of the `tailscale` container. Tailscale Serve keeps the `Host` header, which is how `cozy-stack` picks the app. +- **Extra container.** The `couchdb` container (`app--db`) runs the CouchDB database. It stays on the Compose network and joins neither the Tailnet nor the network of the `tailscale` container. The stack reaches it by the name `couchdb`. +- **Configuration file.** `compose.yaml` writes the `cozy-stack` configuration to `/etc/cozy/cozy.yml` with a `configs` entry. It is the configuration of the upstream Compose repository, except that the administration port `6060` listens on the loopback address only, so your Tailnet cannot reach it. +- **Port 8080 on the Tailnet.** `cozy-stack` listens on all addresses of the `tailscale` container, so the sibling devices can reach it. The port is also reachable at the Tailscale IP address of the device when your Tailnet policy allows it, but `cozy-stack` needs a valid instance name in the `Host` header to answer. +- **No mail server.** The configuration has no mail relay, and the embedded Postfix is off. Cozy cannot send e-mail, for example for sharing invitations and password resets. +- **Health check.** `cozy-stack` calls its own `/status` endpoint with `curl`. CouchDB calls its `/_up` endpoint with `curl`. Neither image defines a check. + +## First run + +Create your instance with the `cozy-stack` command line. Replace `.ts.net` with your [Tailnet DNS name](https://tailscale.com/kb/1217/tailnet-name), and use your own e-mail address, language, and time zone. The instance name must be `..ts.net`, the address of the main device: + +```sh +docker exec app-cozy cozy-stack instances add \ + --apps home,settings,drive \ + --email you@example.com \ + --locale en \ + --tz Europe/Amsterdam \ + cozy..ts.net +``` + +The command prints a link that ends in `?registerToken=...`. Open it in a browser on a device that is on your Tailnet, and choose your password. The first request can take up to a minute while Tailscale requests the certificates of the devices. Then open the apps at the addresses in the table above. + +## Configuration + +To use another app, such as `photos` or `notes`, give it a device and install it: + +1. Copy the `tailscale-drive` block in `compose.yaml` and rename it to `tailscale-`. Change `container_name`, `hostname`, and the state folder to match, then run `docker compose up -d`. +2. Install the app: `docker exec app-cozy cozy-stack apps install --domain cozy..ts.net `. + +## Troubleshooting + +- **A device is named `cozy-1`.** An older device with the same name is still in your Tailnet. Remove it in the admin console, delete the `./ts/state*` folders, and start the stack again. +- **`Domain name contains illegal characters`.** The instance name has a hyphen in its first part. Use a `SERVICE` without a hyphen. +- **The browser shows a certificate error on the first visit.** Wait up to a minute for each device and reload the page. Each of the four devices requests its own certificate. +- **The certificate error stays, and the Tailscale log shows `rateLimited`.** Let's Encrypt limits new registrations to 10 per 3 hours for each IP address, and every device that starts without `./ts/state*` uses one, so this stack uses four. Keep the `./ts/state*` folders when you recreate the stack, and retry at the time the log gives. + +## Links + +- [Cozy self-hosting documentation](https://docs.cozy.io/en/tutorials/selfhosting/docker/) +- [cozy-stack documentation](https://docs.cozy.io/en/cozy-stack/) +- [cozy-stack source code](https://github.com/cozy/cozy-stack) +- [cozy-stack Docker Compose repository](https://github.com/cozy/cozy-stack-compose) diff --git a/services/cozy/compose.yaml b/services/cozy/compose.yaml new file mode 100644 index 00000000..5abfbaf4 --- /dev/null +++ b/services/cozy/compose.yaml @@ -0,0 +1,186 @@ +configs: + # Serve configuration of the main device: the instance itself (login and API). + ts-serve: + content: | + {"TCP":{"443":{"HTTPS":true}}, + "Web":{"$${TS_CERT_DOMAIN}:443": + {"Handlers":{"/": + {"Proxy":"http://127.0.0.1:8080"}}}}, + "AllowFunnel":{"$${TS_CERT_DOMAIN}:443":false}} + # Serve configuration of the app devices: forwards to cozy-stack in the network of the main device. + ts-serve-app: + content: | + {"TCP":{"443":{"HTTPS":true}}, + "Web":{"$${TS_CERT_DOMAIN}:443": + {"Handlers":{"/": + {"Proxy":"http://tailscale:8080"}}}}, + "AllowFunnel":{"$${TS_CERT_DOMAIN}:443":false}} + # cozy-stack configuration. The {{.Env.X}} values are read by cozy-stack at start from the environment of the application. + cozy-config: + content: | + host: 0.0.0.0 + port: 8080 + admin: + host: 127.0.0.1 + port: 6060 + subdomains: flat + couchdb: + url: {{.Env.COUCHDB_PROTOCOL}}://{{.Env.COUCHDB_USER}}:{{.Env.COUCHDB_PASSWORD}}@{{.Env.COUCHDB_HOST}}:{{.Env.COUCHDB_PORT}} + fs: + url: file:///var/lib/cozy/data + vault: + credentials_encryptor_key: /etc/cozy/vault.enc + credentials_decryptor_key: /etc/cozy/vault.dec + konnectors: + cmd: /usr/local/bin/konnector-node-run.sh + log: + level: info + syslog: false + registries: + default: + - https://apps-registry.cozycloud.cc/selfhosted + - https://apps-registry.cozycloud.cc/mespapiers + - https://apps-registry.cozycloud.cc/banks + - https://apps-registry.cozycloud.cc/ + +# Settings shared by the Tailscale devices of the apps. Each app needs its own device, +# because cozy-stack serves every app on its own host name. +x-tailscale-app: &tailscale-app + image: tailscale/tailscale:latest # Image to be used + environment: + - TS_AUTHKEY=${TS_AUTHKEY} + - TS_STATE_DIR=/var/lib/tailscale + - TS_SERVE_CONFIG=/config/serve.json # Tailscale Serve configuration to expose the app on your local Tailnet + - TS_USERSPACE=false + - TS_ENABLE_HEALTH_CHECK=true # Enable healthcheck endpoint: "/healthz" + - TS_LOCAL_ADDR_PORT=127.0.0.1:41234 # The : for the healthz endpoint + - TS_AUTH_ONCE=true + configs: + - source: ts-serve-app + target: /config/serve.json + devices: + - /dev/net/tun:/dev/net/tun # Network configuration for Tailscale to work + cap_add: + - net_admin # Tailscale requirement + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:41234/healthz"] # Check Tailscale has a Tailnet IP and is operational + interval: 1m # How often to perform the check + timeout: 10s # Time to wait for the check to succeed + retries: 3 # Number of retries before marking as unhealthy + start_period: 10s # Time to wait before starting health checks + restart: always + +services: +# Make sure you have updated/checked the .env file with the correct variables. +# Every variable used in this file must be defined there, unless it has a default such as ${VAR:-value}. + # Tailscale Sidecar Configuration + tailscale: + image: tailscale/tailscale:latest # Image to be used + container_name: tailscale-${SERVICE} # Name for local container management + hostname: ${SERVICE} # Name used within your Tailscale environment + environment: + - TS_AUTHKEY=${TS_AUTHKEY} + - TS_STATE_DIR=/var/lib/tailscale + - TS_SERVE_CONFIG=/config/serve.json # Tailscale Serve configuration to expose the web interface on your local Tailnet - remove this line if not required + - TS_USERSPACE=false + - TS_ENABLE_HEALTH_CHECK=true # Enable healthcheck endpoint: "/healthz" + - TS_LOCAL_ADDR_PORT=127.0.0.1:41234 # The : for the healthz endpoint + #- TS_ACCEPT_DNS=true # Uncomment only if the service must resolve MagicDNS names - this replaces Docker DNS, so Compose service names no longer resolve + - TS_AUTH_ONCE=true + configs: + - source: ts-serve + target: /config/serve.json + volumes: + - ./config:/config # Config folder used to store Tailscale files - you may need to change the path + - ./ts/state:/var/lib/tailscale # Tailscale requirement - you may need to change the path + devices: + - /dev/net/tun:/dev/net/tun # Network configuration for Tailscale to work + cap_add: + - net_admin # Tailscale requirement + #ports: + # - 0.0.0.0:${SERVICEPORT}:${SERVICEPORT} # Binding the service port to the local network - may be removed if only exposure to your Tailnet is required + # If any DNS issues arise, use your preferred DNS provider by uncommenting the config below + #dns: + # - ${DNS_SERVER} + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:41234/healthz"] # Check Tailscale has a Tailnet IP and is operational + interval: 1m # How often to perform the check + timeout: 10s # Time to wait for the check to succeed + retries: 3 # Number of retries before marking as unhealthy + start_period: 10s # Time to wait before starting health checks + restart: always + + # Tailscale devices of the apps, named after the service and the app. To add an app, copy one of these blocks and see the README. + tailscale-home: + <<: *tailscale-app + container_name: tailscale-${SERVICE}-home + hostname: ${SERVICE}-home + volumes: + - ./ts/state-home:/var/lib/tailscale + + tailscale-settings: + <<: *tailscale-app + container_name: tailscale-${SERVICE}-settings + hostname: ${SERVICE}-settings + volumes: + - ./ts/state-settings:/var/lib/tailscale + + tailscale-drive: + <<: *tailscale-app + container_name: tailscale-${SERVICE}-drive + hostname: ${SERVICE}-drive + volumes: + - ./ts/state-drive:/var/lib/tailscale + + # Application + application: + image: ${IMAGE_URL} # Image to be used + network_mode: service:tailscale # Sidecar configuration to route the service through Tailscale + container_name: app-${SERVICE} # Name for local container management + environment: # Variables are declared in .env file. + - TZ=${TZ} + - COZY_ADMIN_PASSPHRASE=${COZY_ADMIN_PASSPHRASE:?Set COZY_ADMIN_PASSPHRASE in .env} # Used once: the image stores its hash in the etc folder below + - START_EMBEDDED_POSTFIX=false + - COUCHDB_PROTOCOL=http + - COUCHDB_HOST=couchdb + - COUCHDB_PORT=5984 + - COUCHDB_USER=${COUCHDB_USER} + - COUCHDB_PASSWORD=${COUCHDB_PASSWORD:?Set COUCHDB_PASSWORD in .env} + configs: + - source: cozy-config + target: /etc/cozy/cozy.yml + volumes: + - ./${SERVICE}-data/app/etc:/etc/cozy # Admin passphrase hash and vault keys + - ./${SERVICE}-data/app/data:/var/lib/cozy/data # Files of all instances + depends_on: + tailscale: + condition: service_healthy + couchdb: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/status"] # Check if the service is responding + interval: 1m # How often to perform the check + timeout: 10s # Time to wait for the check to succeed + retries: 3 # Number of retries before marking as unhealthy + start_period: 30s # Time to wait before starting health checks + restart: always + + # Database + couchdb: + image: couchdb:3.3 # Major.minor tag, as the cozy-stack documentation uses + container_name: app-${SERVICE}-db + environment: + - COUCHDB_USER=${COUCHDB_USER} + - COUCHDB_PASSWORD=${COUCHDB_PASSWORD:?Set COUCHDB_PASSWORD in .env} + volumes: + - ./${SERVICE}-data/db:/opt/couchdb/data + ulimits: + nofile: 65536 + # Healthcheck: the image defines none, so call the CouchDB endpoint with the curl that ships with the image. + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:5984/_up"] # Check if CouchDB is up + interval: 10s # How often to perform the check + timeout: 5s # Time to wait for the check to succeed + retries: 5 # Number of retries before marking as unhealthy + start_period: 30s # Time to wait before starting health checks + restart: always