Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
33 changes: 33 additions & 0 deletions services/cozy/.env
Original file line number Diff line number Diff line change
@@ -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=
72 changes: 72 additions & 0 deletions services/cozy/README.md
Original file line number Diff line number Diff line change
@@ -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.<tailnet>.ts.net` (login and API) |
| Apps | `https://cozy-home.<tailnet>.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 `<SERVICE>-<app>.<tailnet>.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-<app>`. 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-<service>-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 `<tailnet>.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 `<SERVICE>.<tailnet>.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.<tailnet>.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-<app>`. 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.<tailnet>.ts.net <app>`.

## 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)
186 changes: 186 additions & 0 deletions services/cozy/compose.yaml
Original file line number Diff line number Diff line change
@@ -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 <addr>:<port> 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 <addr>:<port> 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
Loading