diff --git a/README.md b/README.md index 558dad95..004780d1 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) | @@ -198,6 +199,7 @@ ScaleTail provides ready-to-run [Docker Compose](https://docs.docker.com/compose | 🖥️ **Portainer** | A lightweight management UI which allows you to easily manage your Docker environments. | [Details](services/portainer) | | 🔍 **searXNG** | A free internet metasearch engine which aggregates results from various search services. | [Details](services/searxng) | | 🧠 **Ollama** | A self-hosted solution for running open large language models (LLMs) locally with an OpenAI-compatible API. | [Details](services/ollama) | +| 🖥️ **Termix** | A self-hosted server management platform with SSH terminals, remote desktops, tunnels, and Docker management. | [Details](services/termix) | ### 📈 Monitoring and Analytics diff --git a/services/matrix/.env b/services/matrix/.env new file mode 100644 index 00000000..d9dfd0bc --- /dev/null +++ b/services/matrix/.env @@ -0,0 +1,32 @@ +#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=matrix +IMAGE_URL=matrixdotorg/synapse:latest + +# Network Configuration +SERVICEPORT=443 ## The webport will be exposed to the tailnet. Change if needed. +DNS_SERVER=9.9.9.9 # Preferred DNS server for Tailscale. Uncomment the "dns:" section in compose.yaml to enable. + +# Tailscale Configuration +TS_AUTHKEY= + +# Time Zone setting for containers +TZ=Europe/Amsterdam # See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + +# Matrix Synapse Settings +SYNAPSE_SERVER_NAME=matrix..ts.net ## Change to your server name (e.g., matrix.yourdomain.com) +SYNAPSE_REPORT_STATS=no ## Set to "yes" to enable anonymous statistics reporting + +# UID/GID for Synapse container (default: 991) +UID=991 +GID=991 + +# Postgres Settings (recommended for production) +POSTGRES_USER=synapse ## Please Change +POSTGRES_PASSWORD= +POSTGRES_DB=synapse + +#EXAMPLE_VAR="Environment variable" diff --git a/services/matrix/compose.yaml b/services/matrix/compose.yaml new file mode 100644 index 00000000..c330caab --- /dev/null +++ b/services/matrix/compose.yaml @@ -0,0 +1,107 @@ +configs: + ts-serve: + content: | + {"TCP":{"443":{"HTTPS":true}}, + "Web":{"$${TS_CERT_DOMAIN}:443": + {"Handlers":{ + "/":{"Proxy":"http://127.0.0.1:8008"}, + "/.well-known/matrix/client":{"Static":{"Path":"/config/well-known-matrix-client.json"}}, + "/.well-known/matrix/server":{"Static":{"Path":"/config/well-known-matrix-server.json"}} + }}}, + "AllowFunnel":{"$${TS_CERT_DOMAIN}:443":true}} + +services: +# Make sure you have updated/checked the .env file with the correct variables. +# All the ${ xx } need to 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 when using MagicDNS + - 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 port ${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 + + # Matrix Synapse + application: + image: ${IMAGE_URL} # Image to be used + container_name: app-${SERVICE} # Name for local container management + network_mode: service:tailscale # Sidecar configuration to route ${SERVICE} through Tailscale + environment: + - TZ=${TZ} + - SYNAPSE_SERVER_NAME=${SYNAPSE_SERVER_NAME} + - SYNAPSE_REPORT_STATS=${SYNAPSE_REPORT_STATS} + - SYNAPSE_CONFIG_DIR=/data + - SYNAPSE_CONFIG_PATH=/data/homeserver.yaml + - SYNAPSE_DATA_DIR=/data + - UID=${UID:-991} + - GID=${GID:-991} + volumes: + - ./${SERVICE}-data:/data + depends_on: + database: + condition: service_healthy + tailscale: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-fSs", "http://localhost:8008/health"] + 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 + + # PostgreSQL Database (recommended for production) + database: + image: postgres:16-alpine + container_name: db-${SERVICE} + restart: always + security_opt: + - no-new-privileges:true + pids_limit: 100 + read_only: true + tmpfs: + - /tmp + - /var/run/postgresql + volumes: + - ./${SERVICE}-data/postgres:/var/lib/postgresql/data + environment: + - TZ=${TZ} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 1m + timeout: 10s + retries: 3 + start_period: 30s diff --git a/services/nextcloud/.env b/services/nextcloud/.env new file mode 100644 index 00000000..3fe975a2 --- /dev/null +++ b/services/nextcloud/.env @@ -0,0 +1,36 @@ +#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=nextcloud +IMAGE_URL=nextcloud:apache # Docker image URL from container registry + +# Network Configuration +SERVICEPORT=80 # Port to expose to local network. Uncomment the "ports:" section in compose.yaml to enable. +DNS_SERVER=9.9.9.9 # Preferred DNS server for Tailscale. Uncomment the "dns:" section in compose.yaml to enable. + +# Tailscale Configuration +TS_AUTHKEY= # Auth key from https://tailscale.com/admin/authkeys. See: https://tailscale.com/kb/1085/auth-keys#generate-an-auth-key for instructions. +TAILNET_NAME= # Your Tailscale network name (found in admin console) + +# Time Zone setting for containers +TZ=Europe/Amsterdam # See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + +# Database Configuration (MariaDB) +MYSQL_ROOT_PASSWORD= +MYSQL_DATABASE=nextcloud +MYSQL_USER=nextcloud +MYSQL_PASSWORD= + +# Nextcloud Admin Credentials (auto-created on first run) +NEXTCLOUD_ADMIN_USER=admin +NEXTCLOUD_ADMIN_PASSWORD= +NEXTCLOUD_TRUSTED_DOMAINS= + +# Optional Service variables +# PUID=1000 +# PHP_MEMORY_LIMIT=512M +# PHP_UPLOAD_LIMIT=512M + +#EXAMPLE_VAR="Environment varibale" diff --git a/services/nextcloud/compose.yaml b/services/nextcloud/compose.yaml new file mode 100644 index 00000000..6189822e --- /dev/null +++ b/services/nextcloud/compose.yaml @@ -0,0 +1,115 @@ +configs: + ts-serve: + content: | + {"TCP":{"443":{"HTTPS":true}}, + "Web":{"$${TS_CERT_DOMAIN}:443": + {"Handlers":{"/": + {"Proxy":"http://127.0.0.1:80"}}}}, + "AllowFunnel":{"$${TS_CERT_DOMAIN}:443":false}} + +services: +# Make sure you have updated/checked the .env file with the correct variables. +# All the ${ xx } need to 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 # Enable MagicDNS for Tailnet DNS resolution + - 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:8085:${SERVICEPORT} # Binding port ${SERVICEPORT} 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 + + # ${SERVICE} + application: + image: ${IMAGE_URL} # Image to be used + network_mode: service:tailscale # Sidecar configuration to route ${SERVICE} through Tailscale + container_name: app-${SERVICE} # Name for local container management + environment: + - MYSQL_HOST=127.0.0.1 + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + - REDIS_HOST=localhost + - REDIS_HOST_PORT=6379 + - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER} + - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD} + - NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS} + - OVERWRITEPROTOCOL=https + - OVERWRITECLI_URL=https://${SERVICE}.${TAILNET_NAME}.ts.net + - TZ=${TZ} + volumes: + - ./${SERVICE}-data/html:/var/www/html + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + tailscale: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/status.php"] # Check if Nextcloud status endpoint responds + 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: 60s # Time to wait before starting health checks - Nextcloud needs time to initialize + restart: always + + db: + image: mariadb:lts # MariaDB LTS for production use + network_mode: service:tailscale # Sidecar configuration to route ${SERVICE} through Tailscale + container_name: app-${SERVICE}-database # Name for local container management + command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW # Required MariaDB configuration for Nextcloud + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + volumes: + - ./${SERVICE}-data/db:/var/lib/mysql + healthcheck: + test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"] # Check if MariaDB is responsive + 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 + + redis: + image: redis:alpine # Redis for caching and file locking + network_mode: service:tailscale # Sidecar configuration to route ${SERVICE} through Tailscale + container_name: app-${SERVICE}-redis # Name for local container management + healthcheck: + test: ["CMD", "redis-cli", "ping"] # Check if Redis responds to ping + 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 diff --git a/services/skerry-sync/.env b/services/skerry-sync/.env new file mode 100644 index 00000000..bcefd858 --- /dev/null +++ b/services/skerry-sync/.env @@ -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" diff --git a/services/skerry-sync/README.md b/services/skerry-sync/README.md new file mode 100644 index 00000000..2db5243e --- /dev/null +++ b/services/skerry-sync/README.md @@ -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://..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) diff --git a/services/skerry-sync/compose.yaml b/services/skerry-sync/compose.yaml new file mode 100644 index 00000000..1bba13e3 --- /dev/null +++ b/services/skerry-sync/compose.yaml @@ -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 : 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 diff --git a/services/termix/.env b/services/termix/.env new file mode 100644 index 00000000..2f739f9f --- /dev/null +++ b/services/termix/.env @@ -0,0 +1,31 @@ +#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=termix +IMAGE_URL=ghcr.io/lukegus/termix: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 + +# Termix Configuration +# HTTP server port inside the container. +PORT=8080 +# Remote desktop (RDP/VNC/Telnet) via guacd. Default false: SSH/terminal works standalone. +# To enable, uncomment the `guacd` service in compose.yaml and set this to true, +# then turn on the RDP/VNC/Telnet toggle in Admin Settings (guacd URL localhost:4822). +ENABLE_GUACAMOLE=false + +# --- External database (optional, instead of SQLite) --- +# SQLite is the default and needs no setup. For PostgreSQL or MySQL, set: +#DATABASE_DIALECT=postgres # sqlite (default), postgres, or mysql +#DATABASE_URL=postgres://termix:"REPLACE_WITH_DB_PASSWORD"@localhost:5432/termix +# Keep the data volume either way: Termix still uses it for keys, certificates, and recordings. diff --git a/services/termix/README.md b/services/termix/README.md new file mode 100644 index 00000000..684274aa --- /dev/null +++ b/services/termix/README.md @@ -0,0 +1,57 @@ +# Termix with Tailscale Sidecar Configuration + +This Docker Compose configuration sets up [Termix](https://github.com/Termix-SSH/Termix) with Tailscale as a sidecar container to keep the app reachable over your Tailnet. + +## Termix + +[Termix](https://github.com/Termix-SSH/Termix) is a free, open-source, self-hosted server management platform and Termius alternative. It puts SSH terminals, remote desktops (RDP, VNC, Telnet), file transfers, tunnels, Docker management, metrics, and automations in one web interface. Pairing it with Tailscale keeps shell access to your infrastructure off the public internet while remaining reachable from all your Tailnet devices. + +## Configuration Overview + +In this setup, the `tailscale` service (container `tailscale-termix`) runs Tailscale, which manages secure networking for Termix. The `application` service (container `app-termix`) uses the Tailscale network stack via Docker's `network_mode: service:tailscale` configuration. This keeps the app Tailnet-only unless you intentionally expose ports. + +## Prerequisites + +- Docker Compose and host user membership in the `docker` group. +- A Tailscale auth key in `.env` (`TS_AUTHKEY`). +- On first launch, create the admin account in the web UI. Registration settings can then be locked in Admin Settings or via `ALLOW_REGISTRATION` (see [environment variables](https://docs.termix.site/setup/environment-variables)). + +## Volumes + +- `./termix-data/data:/app/data` holds the SQLite database, auto-generated secrets (`JWT_SECRET`, `DATABASE_KEY`, and others in `/app/data/.env`), SSL certificates, encryption keys, and session recordings. Back it up: it is your only restore point. +- Pre-create the directory so Docker does not create a root-owned folder (the container runs as `PUID`/`PGID` `1000:1000`): + + ```sh + mkdir -p termix-data/data + sudo chown -R 1000:1000 termix-data + ``` + +## Tailnet access + +- Serve proxies `https://..ts.net` to `http://127.0.0.1:8080`. +- The web UI is available at the Tailnet HTTPS URL once the stack is up. + +## Ports + +The commented `0.0.0.0:${SERVICEPORT}:${SERVICEPORT}` mapping stays removed for Tailnet-only access. The server listens on internal port 8080 (`PORT`). Expose it on LAN only for deliberate local testing. + +## Service-specific notes + +- SQLite is the default and needs no setup. For PostgreSQL or MySQL, set `DATABASE_DIALECT` and `DATABASE_URL` in `.env` (see [database setup](https://docs.termix.site/setup/database)). Keep the data volume either way: Termix still uses it for keys, certificates, and recordings. +- Remote desktop (RDP/VNC/Telnet) needs the Guacamole daemon: uncomment the `guacd` service and its `depends_on` entry in `compose.yaml`, set `ENABLE_GUACAMOLE=true` in `.env`, then turn on the RDP/VNC/Telnet toggle in Admin Settings with guacd URL `localhost:4822` (the daemon shares the Tailscale network namespace). It is disabled by default; SSH and terminal features work standalone. +- Security keys are auto-generated on first startup into `/app/data/.env`. Do not set them manually unless restoring from backup. +- The image defines its own `HEALTHCHECK` (`wget` against `http://localhost:30001/health`), so this stack does not override it. + +## Upstream documentation + +- [Termix Docker install](https://docs.termix.site/install/server/docker) +- [Environment variables](https://docs.termix.site/setup/environment-variables) +- [Database setup](https://docs.termix.site/setup/database) +- [Remote desktop setup](https://docs.termix.site/setup/remote-desktop) +- [Termix repository](https://github.com/Termix-SSH/Termix) + +## Files to check + +Please check the following contents for validity as some variables need to be defined upfront. + +- `.env` // Main variables `TS_AUTHKEY`, `PORT`, `ENABLE_GUACAMOLE` diff --git a/services/termix/compose.yaml b/services/termix/compose.yaml new file mode 100644 index 00000000..844d65df --- /dev/null +++ b/services/termix/compose.yaml @@ -0,0 +1,81 @@ +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. + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + - PORT=${PORT} + - ENABLE_GUACAMOLE=${ENABLE_GUACAMOLE} + volumes: + - ./${SERVICE}-data/data:/app/data + depends_on: + tailscale: + condition: service_healthy + # Uncomment together with the `guacd` service below for remote desktop (RDP/VNC/Telnet). + #guacd: + # condition: service_started + # Healthcheck: defined by the image (wget against http://localhost:30001/health), so this file does not override it. + restart: always + + # # Optional Guacamole daemon for remote desktop (RDP/VNC/Telnet). + # # To use it: uncomment this service and the `depends_on` entry above, set + # # ENABLE_GUACAMOLE=true in .env, and turn on the RDP/VNC/Telnet toggle in + # # Admin Settings with guacd URL localhost:4822. + # # The service shares the Tailscale network namespace, so the app reaches it at localhost. + #guacd: + # image: guacamole/guacd:1.6.0 + # network_mode: service:tailscale # Join the same network namespace to be accessible via localhost + # container_name: app-${SERVICE}-guacd # Name for local container management + # restart: always diff --git a/util/cert-export.sh b/util/cert-export.sh new file mode 100755 index 00000000..0f0f7b0e --- /dev/null +++ b/util/cert-export.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -u + +OUT_DIR="$(pwd)" + +echo "[+] Exporting Tailscale certificates" +echo "[+] Output: $OUT_DIR" +echo + +docker ps --format '{{.Names}}' | +while IFS= read -r container; do + + [[ "$container" == tailscale-* ]] || continue + + name="${container#tailscale-}" + + echo "=== $container ===" + + # Get Tailscale DNS name + hostname="$( + docker exec "$container" \ + tailscale status --json 2>/dev/null | + python3 -c ' +import sys +import json + +data = json.load(sys.stdin) +print(data["Self"]["DNSName"].rstrip(".")) +' 2>/dev/null + )" + + if [[ -z "$hostname" ]]; then + echo "[!] Could not determine hostname" + echo + continue + fi + + echo "[+] Hostname: $hostname" + + # Generate certificate + if ! docker exec "$container" \ + tailscale cert \ + --cert-file="/tmp/tailscale.crt" \ + --key-file="/tmp/tailscale.key" \ + "$hostname" + then + echo "[!] Certificate generation failed" + echo + continue + fi + + # Verify files exist BEFORE docker cp + if ! docker exec "$container" \ + test -f /tmp/tailscale.crt + then + echo "[!] Certificate file was not created" + continue + fi + + if ! docker exec "$container" \ + test -f /tmp/tailscale.key + then + echo "[!] Key file was not created" + continue + fi + + # Export + docker cp \ + "$container:/tmp/tailscale.crt" \ + "$OUT_DIR/${name}.crt" + + docker cp \ + "$container:/tmp/tailscale.key" \ + "$OUT_DIR/${name}.key" + + # Permissions + chmod 644 "$OUT_DIR/${name}.crt" + chmod 600 "$OUT_DIR/${name}.key" + + # Cleanup container + docker exec "$container" \ + rm -f /tmp/tailscale.crt /tmp/tailscale.key + + echo "[✓] Exported:" + echo " ${name}.crt" + echo " ${name}.key" + echo + +done + +echo "[✓] Finished"