From c9541bdecca65ab0f396142f7ca3f9902e5522f0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 3 Oct 2026 11:26:13 -0400 Subject: [PATCH 1/2] Skerry Sync: new service --- README.md | 1 + services/skerry-sync/.env | 28 +++++++++++++ services/skerry-sync/README.md | 63 +++++++++++++++++++++++++++++ services/skerry-sync/compose.yaml | 66 +++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 services/skerry-sync/.env create mode 100644 services/skerry-sync/README.md create mode 100644 services/skerry-sync/compose.yaml diff --git a/README.md b/README.md index 558dad95..dce5f27c 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/services/skerry-sync/.env b/services/skerry-sync/.env new file mode 100644 index 00000000..2079683c --- /dev/null +++ b/services/skerry-sync/.env @@ -0,0 +1,28 @@ +#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 +# 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= diff --git a/services/skerry-sync/README.md b/services/skerry-sync/README.md new file mode 100644 index 00000000..47efb03c --- /dev/null +++ b/services/skerry-sync/README.md @@ -0,0 +1,63 @@ +# 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://..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`). PostgreSQL is an upstream option but is not wired into this stack; it would need an additional `db` service. +- 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. + +## 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` diff --git a/services/skerry-sync/compose.yaml b/services/skerry-sync/compose.yaml new file mode 100644 index 00000000..a6f59592 --- /dev/null +++ b/services/skerry-sync/compose.yaml @@ -0,0 +1,66 @@ +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 : 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_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 + # Healthcheck: defined by the image (wget -qO- http://localhost:8080/healthz), so this file does not override it. + restart: always From c93c724ab758109a8de72bbd973a10009ff9032c Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 3 Oct 2026 11:42:42 -0400 Subject: [PATCH 2/2] Skerry Sync: add optional PostgreSQL and document data volume ownership --- services/skerry-sync/.env | 13 +++++++++++++ services/skerry-sync/README.md | 16 ++++++++++++++-- services/skerry-sync/compose.yaml | 27 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/services/skerry-sync/.env b/services/skerry-sync/.env index 2079683c..bcefd858 100644 --- a/services/skerry-sync/.env +++ b/services/skerry-sync/.env @@ -19,6 +19,9 @@ TZ=Europe/Amsterdam # See: https://en.wikipedia.org/wiki/List_of_tz_database_tim # 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. @@ -26,3 +29,13 @@ 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" diff --git a/services/skerry-sync/README.md b/services/skerry-sync/README.md index 47efb03c..2db5243e 100644 --- a/services/skerry-sync/README.md +++ b/services/skerry-sync/README.md @@ -44,11 +44,23 @@ The commented `0.0.0.0:${SERVICEPORT}:${SERVICEPORT}` mapping stays removed for ## Service-specific notes -- SQLite is the default with zero configuration (`SKERRY_DB_URL=jdbc:sqlite:/data/skerry-sync.db`). PostgreSQL is an upstream option but is not wired into this stack; it would need an additional `db` service. +- 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) @@ -60,4 +72,4 @@ The commented `0.0.0.0:${SERVICEPORT}:${SERVICEPORT}` mapping stays removed for 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` +- `.env` // Main variables `TS_AUTHKEY`, `SKERRY_JWT_SECRET`, `SKERRY_ADMIN_TOKEN` (plus `SKERRY_DB_*`/`POSTGRES_PASSWORD` when using PostgreSQL) diff --git a/services/skerry-sync/compose.yaml b/services/skerry-sync/compose.yaml index a6f59592..1bba13e3 100644 --- a/services/skerry-sync/compose.yaml +++ b/services/skerry-sync/compose.yaml @@ -55,6 +55,8 @@ services: 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: @@ -62,5 +64,30 @@ services: 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