Skip to content

Commit b90b7fa

Browse files
authored
Joplin Server: new service (#382)
1 parent 2697ad9 commit b90b7fa

4 files changed

Lines changed: 172 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ Every stack starts from the same [standard setup](documentation/standard-setup.m
162162
| **Gramps Web** | Genealogy web app for browsing and editing family trees together. | [Details](services/grampsweb) |
163163
| **Haptic** | Markdown note editor that keeps notes in the browser, not on the server. | [Details](services/haptic) |
164164
| **Isley** | Cannabis grow journal for logging plants, watering, feeding, and harvests. | [Details](services/isley) |
165+
| **Joplin Server** | Sync server for the Joplin note-taking apps, with notes kept in a PostgreSQL database. | [Details](services/joplin) |
165166
| **Kaneo** | Open-source project tracker with boards and tasks, an alternative to Trello and Linear. | [Details](services/kaneo) |
166167
| **Karakeep** | Bookmark manager that archives links, notes, and images, with full-text search. | [Details](services/karakeep) |
167168
| **LanguageTool** | Grammar and spelling checker server for browser extensions, with no web interface. | [Details](services/languagetool) |

‎services/joplin/.env‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#URL=https://github.com/tailscale-dev/ScaleTail
2+
# To run a second copy, copy the folder under another name and change SERVICE.
3+
4+
# Service Configuration
5+
# Service name (e.g., adguard). Used as hostname in Tailscale and for container naming.
6+
SERVICE=joplin
7+
# Docker image URL from container registry (e.g., adguard/adguardhome:latest).
8+
IMAGE_URL=joplin/server:latest
9+
10+
# Network Configuration
11+
# Port to expose to local network. Uncomment the "ports:" section in compose.yaml to enable.
12+
SERVICEPORT=22300
13+
# Preferred DNS server for Tailscale. Uncomment the "dns:" section in compose.yaml to enable.
14+
DNS_SERVER=9.9.9.9
15+
16+
# Tailscale Configuration
17+
# Auth key from https://console.tailscale.com/admin/settings/keys. See: https://tailscale.com/kb/1085/auth-keys#generate-an-auth-key for instructions.
18+
TS_AUTHKEY=
19+
20+
# Time Zone setting for containers. See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
21+
TZ=Europe/Amsterdam
22+
23+
# Any Container environment variables are declared below. See https://docs.docker.com/compose/how-tos/environment-variables/
24+
25+
# Required: your Tailnet name, the part between the service name and ".ts.net" in https://joplin.<tailnet name>.ts.net, for example tail1234.
26+
TAILNET_NAME=
27+
# Required: password of the database. Generate it with: openssl rand -hex 32
28+
POSTGRES_PASSWORD=

‎services/joplin/README.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Joplin Server
2+
3+
[Joplin Server](https://joplinapp.org/) is the sync server for the Joplin note-taking apps. Your notes, notebooks, and attachments stay in sync between your devices without a cloud service.
4+
5+
This stack runs Joplin Server with a Tailscale sidecar, as described in [the standard setup](../../documentation/standard-setup.md).
6+
7+
## At a glance
8+
9+
| Item | Value |
10+
| ------------- | ------------------------------------------- |
11+
| Web interface | `https://joplin.<tailnet>.ts.net` |
12+
| Service port | `22300` |
13+
| Images | `joplin/server` |
14+
| | `postgres:16` |
15+
| Data | `./joplin-data/db` (PostgreSQL database) |
16+
17+
## Before you start
18+
19+
Set these values in `.env`. Compose stops with an error if one of them is empty.
20+
21+
- **`TAILNET_NAME`.** Your Tailnet name, the part between the service name and `.ts.net`, for example `tail1234`. `compose.yaml` builds the base address `https://<SERVICE>.<TAILNET_NAME>.ts.net` from it.
22+
- **`POSTGRES_PASSWORD`.** The password of the database. Generate one with `openssl rand -hex 32`. PostgreSQL applies it only when it first creates the database.
23+
24+
## Deviations from the standard setup
25+
26+
- **Extra container.** The stack runs a `db` container with PostgreSQL. It uses the network of the `tailscale` container as well, so Joplin Server reaches it at `localhost`. PostgreSQL listens only on the loopback address of the device, so other devices on your Tailnet cannot reach it. Joplin Server keeps all notes and attachments in this database, so the stack has no other data folder.
27+
- **Base address.** `APP_BASE_URL` in `compose.yaml` is the Tailnet address of the service. Joplin Server answers only requests for that address, and it uses the address in its links and redirects. If you change `SERVICE` or `TAILNET_NAME`, the address changes with them. For the same reason the optional `ports` block does not give access from your local network: a request to the address of the Docker host gets the error `Invalid origin`.
28+
29+
## First run
30+
31+
Open the web interface and log in with the default administrator account, `admin@localhost` with the password `admin`. Use the Profile button in the upper right to change the administrator password right away: until you do, anyone on your Tailnet can log in as administrator.
32+
33+
Joplin recommends a separate user for syncing. Open the Users page and create one.
34+
35+
To sync a Joplin app, open its synchronisation settings and choose Joplin Server as the synchronisation target. Enter `https://joplin.<tailnet>.ts.net` as the URL, and the email address and password of your user. The device must be connected to your Tailnet.
36+
37+
## Troubleshooting
38+
39+
- **`db: Could not connect ... host.docker.internal` in the logs.** `POSTGRES_HOST` is set to `localhost` or `127.0.0.1`. Joplin Server in Docker replaces those values with `host.docker.internal`, which does not exist in this stack. Remove the variable from `compose.yaml`, so that Joplin Server connects to `localhost`.
40+
- **`Invalid origin` when you open the service.** You used an address other than `APP_BASE_URL`, such as the IP address of the Docker host. Use `https://<SERVICE>.<TAILNET_NAME>.ts.net`.
41+
42+
## Links
43+
44+
- [Joplin Server installation guide](https://github.com/laurent22/joplin/blob/dev/packages/server/README.md)
45+
- [Joplin synchronisation documentation](https://joplinapp.org/help/apps/sync/)
46+
- [Joplin source code](https://github.com/laurent22/joplin)

‎services/joplin/compose.yaml‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
configs:
2+
ts-serve:
3+
content: |
4+
{"TCP":{"443":{"HTTPS":true}},
5+
"Web":{"$${TS_CERT_DOMAIN}:443":
6+
{"Handlers":{"/":
7+
{"Proxy":"http://127.0.0.1:22300"}}}},
8+
"AllowFunnel":{"$${TS_CERT_DOMAIN}:443":false}}
9+
10+
services:
11+
# Make sure you have updated/checked the .env file with the correct variables.
12+
# Every variable used in this file must be defined there, unless it has a default such as ${VAR:-value}.
13+
# Tailscale Sidecar Configuration
14+
tailscale:
15+
image: tailscale/tailscale:latest # Image to be used
16+
container_name: tailscale-${SERVICE} # Name for local container management
17+
hostname: ${SERVICE} # Name used within your Tailscale environment
18+
environment:
19+
- TS_AUTHKEY=${TS_AUTHKEY}
20+
- TS_STATE_DIR=/var/lib/tailscale
21+
- TS_SERVE_CONFIG=/config/serve.json # Tailscale Serve configuration to expose the web interface on your local Tailnet - remove this line if not required
22+
- TS_USERSPACE=false
23+
- TS_ENABLE_HEALTH_CHECK=true # Enable healthcheck endpoint: "/healthz"
24+
- TS_LOCAL_ADDR_PORT=127.0.0.1:41234 # The <addr>:<port> for the healthz endpoint
25+
#- TS_ACCEPT_DNS=true # Uncomment only if the service must resolve MagicDNS names - this replaces Docker DNS, so Compose service names no longer resolve
26+
- TS_AUTH_ONCE=true
27+
configs:
28+
- source: ts-serve
29+
target: /config/serve.json
30+
volumes:
31+
- ./config:/config # Config folder used to store Tailscale files - you may need to change the path
32+
- ./ts/state:/var/lib/tailscale # Tailscale requirement - you may need to change the path
33+
devices:
34+
- /dev/net/tun:/dev/net/tun # Network configuration for Tailscale to work
35+
cap_add:
36+
- net_admin # Tailscale requirement
37+
#ports:
38+
# - 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
39+
# If any DNS issues arise, use your preferred DNS provider by uncommenting the config below
40+
#dns:
41+
# - ${DNS_SERVER}
42+
healthcheck:
43+
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:41234/healthz"] # Check Tailscale has a Tailnet IP and is operational
44+
interval: 1m # How often to perform the check
45+
timeout: 10s # Time to wait for the check to succeed
46+
retries: 3 # Number of retries before marking as unhealthy
47+
start_period: 10s # Time to wait before starting health checks
48+
restart: always
49+
50+
# Application
51+
application:
52+
image: ${IMAGE_URL} # Image to be used
53+
network_mode: service:tailscale # Sidecar configuration to route the service through Tailscale
54+
container_name: app-${SERVICE} # Name for local container management
55+
environment: # Variables are declared in .env file.
56+
- APP_PORT=22300
57+
- APP_BASE_URL=https://${SERVICE}.${TAILNET_NAME:?Set TAILNET_NAME in .env}.ts.net
58+
- DB_CLIENT=pg
59+
# POSTGRES_HOST stays unset: Joplin then connects to localhost. Setting it to localhost or 127.0.0.1 makes Joplin use host.docker.internal.
60+
- POSTGRES_PORT=5432
61+
- POSTGRES_USER=joplin
62+
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
63+
- POSTGRES_DATABASE=joplin
64+
- TZ=${TZ}
65+
depends_on:
66+
tailscale:
67+
condition: service_healthy
68+
db:
69+
condition: service_healthy
70+
# Joplin answers only requests whose Host header matches APP_BASE_URL, so the check sends that host to the public /api/ping endpoint.
71+
healthcheck:
72+
test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:22300/api/ping',{headers:{Host:new URL(process.env.APP_BASE_URL).host}},r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"] # Check if the service is responding
73+
interval: 1m # How often to perform the check
74+
timeout: 10s # Time to wait for the check to succeed
75+
retries: 3 # Number of retries before marking as unhealthy
76+
start_period: 60s # Time to wait before starting health checks
77+
restart: always
78+
79+
# Database
80+
db:
81+
image: postgres:16
82+
network_mode: service:tailscale # Join the same network namespace to be accessible via localhost
83+
container_name: app-${SERVICE}-db # Name for local container management
84+
command: ["postgres", "-c", "listen_addresses=localhost"] # Listen on localhost only, so the Tailnet cannot reach the database
85+
environment:
86+
- POSTGRES_USER=joplin
87+
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
88+
- POSTGRES_DB=joplin
89+
volumes:
90+
- ./${SERVICE}-data/db:/var/lib/postgresql/data
91+
healthcheck:
92+
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U joplin -d joplin"] # Check if PostgreSQL accepts connections
93+
interval: 10s # How often to perform the check
94+
timeout: 5s # Time to wait for the check to succeed
95+
retries: 5 # Number of retries before marking as unhealthy
96+
start_period: 30s # Time to wait before starting health checks
97+
restart: always

0 commit comments

Comments
 (0)