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 @@ -158,6 +158,7 @@ ScaleTail provides ready-to-run [Docker Compose](https://docs.docker.com/compose
| 📅 **Radicale** | A lightweight CalDAV and CardDAV server for self-hosted calendar, to-do, and contact sync. | [Details](services/radicale) |
| 🔄 **Resilio Sync** | A fast, reliable, and simple file sync and share solution. | [Details](services/resilio-sync) |
| 📁 **Seafile** | A self-hosted file syncing and collaboration platform with file sharing, versioning, and team library support. | [Details](services/seafile) |
| 🔄 **Skerry Sync** | A self-hosted, zero-knowledge sync server for the Skerry SSH client. | [Details](services/skerry-sync) |
| 🗂️ **Stirling-PDF** | A web application for managing and editing PDF files. | [Details](services/stirlingpdf) |
| 💰 **Sure Finance** | A self-hosted personal finance and budgeting app with optional AI insights. | [Details](services/sure) |
| 🏦 **Subtrackr** | A self-hosted web app to track subscriptions, renewal dates, costs, and payment methods. | [Details](services/subtrackr) |
Expand Down
41 changes: 41 additions & 0 deletions services/skerry-sync/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#version=1.1
#URL=https://github.com/tailscale-dev/ScaleTail
#COMPOSE_PROJECT_NAME= # Optional: only use when running multiple deployments on the same infrastructure.

# Service Configuration
SERVICE=skerry-sync
IMAGE_URL=secherkasov/skerry-sync:latest

# Network Configuration
SERVICEPORT=8080
DNS_SERVER=9.9.9.9

# Tailscale Configuration
TS_AUTHKEY=

# Time Zone setting for containers
TZ=Europe/Amsterdam # See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

# Skerry Sync Configuration
# SQLite database URL inside the container. Default file is /data/skerry-sync.db.
SKERRY_DB_URL=jdbc:sqlite:/data/skerry-sync.db
# Database credentials (PostgreSQL). Leave empty for SQLite.
SKERRY_DB_USER=
SKERRY_DB_PASSWORD=
# REQUIRED: stable JWT signing secret. Generate with: openssl rand -base64 48
# Keep it stable across restarts, otherwise all issued tokens are invalidated.
# The server refuses to start with the upstream default unless SKERRY_DEV=1.
SKERRY_JWT_SECRET="REPLACE_WITH_STABLE_SECRET"
# Operator console token for /console and /admin/*. Generate with: openssl rand -hex 16
# Empty means the admin data endpoints stay closed.
SKERRY_ADMIN_TOKEN=

# --- PostgreSQL (optional, instead of SQLite) ---
# To switch, uncomment the `db` service in compose.yaml, uncomment the
# `depends_on` entry for it, and set:
#SKERRY_DB_URL=jdbc:postgresql://localhost:5432/skerry
#SKERRY_DB_USER=skerry
#SKERRY_DB_PASSWORD="REPLACE_WITH_DB_PASSWORD"
# Initial database password. Generate with: openssl rand -hex 24
# Keep it in sync with SKERRY_DB_PASSWORD above.
#POSTGRES_PASSWORD="REPLACE_WITH_DB_PASSWORD"
75 changes: 75 additions & 0 deletions services/skerry-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Skerry Sync with Tailscale Sidecar Configuration

This Docker Compose configuration sets up [Skerry Sync](https://github.com/SeCherkasov/SkerrySSH) with Tailscale as a sidecar container to keep the app reachable over your Tailnet.

## Skerry Sync

[Skerry Sync](https://github.com/SeCherkasov/SkerrySSH) is the optional self-hosted sync server for the Skerry SSH client (Linux, Windows, macOS, Android). It stores only ciphertext and sync metadata, verifies passwords with SRP-6a without ever receiving them, and pushes live updates over WebSocket. Pairing it with Tailscale keeps vault sync off the public internet while remaining reachable from all your Tailnet devices.

## Configuration Overview

In this setup, the `tailscale` service (container `tailscale-skerry-sync`) runs Tailscale, which manages secure networking for Skerry Sync. The `application` service (container `app-skerry-sync`) uses the Tailscale network stack via Docker's `network_mode: service:tailscale` configuration. This keeps the app Tailnet-only unless you intentionally expose ports.

Tailscale Serve terminates HTTPS for the Tailnet and proxies to the server's plain HTTP port 8080. That satisfies the upstream requirement to put a TLS-terminating reverse proxy in front of any non-local deployment: the admin token and account metadata never cross the public internet unencrypted.

## Prerequisites

- Docker Compose and host user membership in the `docker` group.
- A Tailscale auth key in `.env` (`TS_AUTHKEY`).
- A stable `SKERRY_JWT_SECRET`, for example generated with `openssl rand -base64 48`. Keep it stable across restarts or all issued tokens are invalidated.
- An optional `SKERRY_ADMIN_TOKEN`, for example generated with `openssl rand -hex 16`. Empty leaves the operator console (`/console`) and `/admin/*` endpoints closed.

## Volumes

- `./skerry-sync-data/data:/data` holds the SQLite database (`skerry-sync.db`).
- The image runs as unprivileged UID/GID `999:999`. Pre-create the directory so Docker does not create a root-owned folder:

```sh
mkdir -p skerry-sync-data/data
sudo chown -R 999:999 skerry-sync-data
```

- Back up the SQLite file (or a PostgreSQL dump if you switch databases). The data is encrypted, but it is your only restore point.

## Tailnet access

- Serve proxies `https://<skerry-sync-tailnet-name>.<tailnet>.ts.net` to `http://127.0.0.1:8080`.
- Public page: `/`, account area: `/account`, operator console: `/console` (requires `SKERRY_ADMIN_TOKEN`).
- Liveness: `/healthz`, readiness: `/readyz`. Prometheus `/metrics` stays off unless you configure `SKERRY_METRICS` upstream.
- In the Skerry app, go to Settings, Sync, enter the Tailnet HTTPS URL, then register or sign in. The `/sync` WebSocket switches to `wss://` automatically.

## Ports

The commented `0.0.0.0:${SERVICEPORT}:${SERVICEPORT}` mapping stays removed for Tailnet-only access. The server itself speaks plain HTTP on internal port 8080. Expose it on LAN only for deliberate local testing, and note that upstream treats trusted-LAN cleartext as acceptable because payloads are end-to-end encrypted.

## Service-specific notes

- SQLite is the default with zero configuration (`SKERRY_DB_URL=jdbc:sqlite:/data/skerry-sync.db`). For PostgreSQL, uncomment the `db` service and its `depends_on` entry in `compose.yaml`, then set `SKERRY_DB_URL=jdbc:postgresql://localhost:5432/skerry` plus `SKERRY_DB_USER`, `SKERRY_DB_PASSWORD`, and `POSTGRES_PASSWORD` in `.env` (the database shares the Tailscale network namespace, so the app reaches it at `localhost`).
- The server refuses to start with the upstream default JWT secret unless `SKERRY_DEV=1`. Always set a real `SKERRY_JWT_SECRET`.
- The bundled `skerry-admin` CLI is available inside the app container: `docker exec app-skerry-sync skerry-admin --help`.
- The image defines its own `HEALTHCHECK` (`wget -qO- http://localhost:8080/healthz`), so this stack does not override it.

## Troubleshooting

- `SQLiteException: [SQLITE_CANTOPEN] Unable to open the database file` at startup means the bind-mounted `/data` directory is not writable by the container's unprivileged user (`999:999`). This happens when Docker auto-creates `skerry-sync-data/data` as root. Fix it with:

```sh
docker compose down
sudo rm -rf skerry-sync-data/data
mkdir -p skerry-sync-data/data
sudo chown -R 999:999 skerry-sync-data
docker compose up -d
```

## Upstream documentation

- [Skerry Sync server README](https://github.com/SeCherkasov/SkerrySSH/blob/main/server/README.md)
- [Skerry SSH repository](https://github.com/SeCherkasov/SkerrySSH)
- [Skerry install and first run guide](https://skerry.sech.uk/guide/)
- [Docker Hub image](https://hub.docker.com/r/secherkasov/skerry-sync)

## Files to check

Please check the following contents for validity as some variables need to be defined upfront.

- `.env` // Main variables `TS_AUTHKEY`, `SKERRY_JWT_SECRET`, `SKERRY_ADMIN_TOKEN` (plus `SKERRY_DB_*`/`POSTGRES_PASSWORD` when using PostgreSQL)
93 changes: 93 additions & 0 deletions services/skerry-sync/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
configs:
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}}

services:
# Make sure you have updated/checked the .env file with the correct variables.
# Every variable used in this file must be defined there.
# 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

# 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}
- SKERRY_DB_URL=${SKERRY_DB_URL}
- SKERRY_DB_USER=${SKERRY_DB_USER}
- SKERRY_DB_PASSWORD=${SKERRY_DB_PASSWORD}
- SKERRY_JWT_SECRET=${SKERRY_JWT_SECRET:?Set SKERRY_JWT_SECRET in .env}
- SKERRY_ADMIN_TOKEN=${SKERRY_ADMIN_TOKEN}
volumes:
- ./${SERVICE}-data/data:/data
depends_on:
tailscale:
condition: service_healthy
# Uncomment together with the `db` service below when using PostgreSQL.
#db:
# condition: service_healthy
# Healthcheck: defined by the image (wget -qO- http://localhost:8080/healthz), so this file does not override it.
restart: always

# # Optional PostgreSQL database (instead of SQLite).
# # To use it: uncomment this service and the `depends_on` entry above, then set
# # SKERRY_DB_URL, SKERRY_DB_USER, SKERRY_DB_PASSWORD, and POSTGRES_PASSWORD in .env.
# # The service shares the Tailscale network namespace, so the app reaches it at localhost.
#db:
# image: postgres:17-alpine
# network_mode: service:tailscale # Join the same network namespace to be accessible via localhost
# container_name: app-${SERVICE}-db # Name for local container management
# environment:
# - POSTGRES_DB=skerry
# - POSTGRES_USER=${SKERRY_DB_USER}
# - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# volumes:
# - ./${SERVICE}-data/db:/var/lib/postgresql/data
# healthcheck:
# test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d skerry"] # Check if PostgreSQL accepts connections
# 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