Prepare QSfera production release
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
QSFERA_RPI_IMAGE=qsfera-cloud-server:rpi5-local
|
||||
QSFERA_RPI_HOST_PORT=9200
|
||||
QSFERA_RPI_CONFIG_PATH=/mnt/data/qsfera/cloud-server/config
|
||||
QSFERA_RPI_DATA_PATH=/mnt/data/qsfera/cloud-server/data
|
||||
|
||||
OC_URL=https://qsfera.example.com
|
||||
OC_LOG_LEVEL=info
|
||||
OC_INSECURE=false
|
||||
PROXY_TLS=true
|
||||
FRONTEND_CHECK_FOR_UPDATES=false
|
||||
+2
-1
@@ -61,8 +61,9 @@ protogen/buf.sha1.lock
|
||||
go.work
|
||||
go.work.sum
|
||||
.env
|
||||
.env.server
|
||||
.envrc
|
||||
.DS_Store
|
||||
|
||||
# example deployments
|
||||
**/qsfera-sandbox-*
|
||||
**/qsfera-sandbox-*
|
||||
|
||||
@@ -15,4 +15,6 @@ This deployment example is documented in two locations for different audiences:
|
||||
Providing details which are more developer focused. This description can also be used when deviating from the default.\
|
||||
Note that this examples uses self signed certificates and is intended for testing purposes.
|
||||
|
||||
|
||||
For production rollouts from this compose directory, use `env.production.example` as the starting point for `.env`.
|
||||
It keeps TLS validation enabled, disables demo users, and requires explicit admin, Keycloak, and S3 secrets instead of demo defaults.
|
||||
Run `./validate-production-env.sh .env` before `docker compose config` and rollout.
|
||||
|
||||
@@ -9,6 +9,6 @@ services:
|
||||
# decomposeds3 specific settings
|
||||
STORAGE_USERS_DECOMPOSEDS3_ENDPOINT: ${DECOMPOSEDS3_ENDPOINT:-http://minio:9000}
|
||||
STORAGE_USERS_DECOMPOSEDS3_REGION: ${DECOMPOSEDS3_REGION:-default}
|
||||
STORAGE_USERS_DECOMPOSEDS3_ACCESS_KEY: ${DECOMPOSEDS3_ACCESS_KEY:-qsfera}
|
||||
STORAGE_USERS_DECOMPOSEDS3_SECRET_KEY: ${DECOMPOSEDS3_SECRET_KEY:-qsfera-secret-key}
|
||||
STORAGE_USERS_DECOMPOSEDS3_BUCKET: ${DECOMPOSEDS3_BUCKET:-qsfera-bucket}
|
||||
STORAGE_USERS_DECOMPOSEDS3_ACCESS_KEY: ${DECOMPOSEDS3_ACCESS_KEY:?Set DECOMPOSEDS3_ACCESS_KEY before starting decomposeds3 in production}
|
||||
STORAGE_USERS_DECOMPOSEDS3_SECRET_KEY: ${DECOMPOSEDS3_SECRET_KEY:?Set DECOMPOSEDS3_SECRET_KEY before starting decomposeds3 in production}
|
||||
STORAGE_USERS_DECOMPOSEDS3_BUCKET: ${DECOMPOSEDS3_BUCKET:?Set DECOMPOSEDS3_BUCKET before starting decomposeds3 in production}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
## Production-safe qsfera_full environment template.
|
||||
## Copy to .env and fill every placeholder before running docker compose.
|
||||
|
||||
COMPOSE_PATH_SEPARATOR=:
|
||||
QSFERA=:qsfera.yml
|
||||
|
||||
## Public deployment identity.
|
||||
OC_DOMAIN=<cloud.example.com>
|
||||
TRAEFIK_ACME_MAIL=<admin@example.com>
|
||||
|
||||
## Use the production image and pin a release tag for repeatable rollouts.
|
||||
OC_DOCKER_IMAGE=qsfera/qsfera
|
||||
OC_DOCKER_TAG=<release-tag>
|
||||
|
||||
## Production must validate TLS certificates and must not create demo users.
|
||||
INSECURE=false
|
||||
DEMO_USERS=false
|
||||
|
||||
## Required by qsfera.yml.
|
||||
ADMIN_PASSWORD=<replace-with-strong-password>
|
||||
|
||||
## Optional: enable Keycloak-backed production identity.
|
||||
## Uncomment KEYCLOAK and fill all values before enabling it.
|
||||
#KEYCLOAK=:keycloak.yml
|
||||
KEYCLOAK_DOMAIN=<keycloak.example.com>
|
||||
KEYCLOAK_REALM=qsfera
|
||||
KEYCLOAK_POSTGRES_PASSWORD=<replace-with-strong-password>
|
||||
KEYCLOAK_ADMIN_USER=<replace-with-admin-user>
|
||||
KEYCLOAK_ADMIN_PASSWORD=<replace-with-strong-password>
|
||||
|
||||
## Optional: enable S3-backed user storage.
|
||||
## Uncomment DECOMPOSEDS3 and fill all values before enabling it.
|
||||
#DECOMPOSEDS3=:decomposeds3.yml
|
||||
DECOMPOSEDS3_ENDPOINT=<https://s3.example.com>
|
||||
DECOMPOSEDS3_REGION=<region>
|
||||
DECOMPOSEDS3_ACCESS_KEY=<replace-with-access-key>
|
||||
DECOMPOSEDS3_SECRET_KEY=<replace-with-secret-key>
|
||||
DECOMPOSEDS3_BUCKET=<bucket>
|
||||
|
||||
## Optional: local MinIO is intended for test/lab installs, not public production.
|
||||
#DECOMPOSEDS3_MINIO=:minio.yml
|
||||
#MINIO_DOMAIN=<minio.example.com>
|
||||
|
||||
## Optional: use host paths for backup-friendly config and data storage.
|
||||
#OC_CONFIG_DIR=/srv/qsfera/config
|
||||
#OC_DATA_DIR=/srv/qsfera/data
|
||||
|
||||
## Keep this last: it assembles the selected compose files.
|
||||
COMPOSE_FILE=docker-compose.yml${QSFERA:-}${DECOMPOSEDS3:-}${DECOMPOSEDS3_MINIO:-}${KEYCLOAK:-}
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_DB: keycloak
|
||||
POSTGRES_USER: keycloak
|
||||
POSTGRES_PASSWORD: keycloak
|
||||
POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:?Set KEYCLOAK_POSTGRES_PASSWORD before starting Keycloak in production}
|
||||
logging:
|
||||
driver: ${LOG_DRIVER:-local}
|
||||
restart: always
|
||||
@@ -53,10 +53,10 @@ services:
|
||||
KC_DB: postgres
|
||||
KC_DB_URL: "jdbc:postgresql://postgres:5432/keycloak"
|
||||
KC_DB_USERNAME: keycloak
|
||||
KC_DB_PASSWORD: keycloak
|
||||
KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:?Set KEYCLOAK_POSTGRES_PASSWORD before starting Keycloak in production}
|
||||
KC_FEATURES: impersonation
|
||||
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER:-admin}
|
||||
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
|
||||
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER:?Set KEYCLOAK_ADMIN_USER before starting Keycloak in production}
|
||||
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?Set KEYCLOAK_ADMIN_PASSWORD before starting Keycloak in production}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.keycloak.entrypoints=https"
|
||||
|
||||
@@ -10,13 +10,13 @@ services:
|
||||
command:
|
||||
[
|
||||
"-c",
|
||||
"mkdir -p /data/${DECOMPOSEDS3_BUCKET:-qsfera-bucket} && minio server --console-address ':9001' /data",
|
||||
"mkdir -p /data/${DECOMPOSEDS3_BUCKET:?Set DECOMPOSEDS3_BUCKET before starting MinIO in production} && minio server --console-address ':9001' /data",
|
||||
]
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${DECOMPOSEDS3_ACCESS_KEY:-qsfera}
|
||||
MINIO_ROOT_PASSWORD: ${DECOMPOSEDS3_SECRET_KEY:-qsfera-secret-key}
|
||||
MINIO_ROOT_USER: ${DECOMPOSEDS3_ACCESS_KEY:?Set DECOMPOSEDS3_ACCESS_KEY before starting MinIO in production}
|
||||
MINIO_ROOT_PASSWORD: ${DECOMPOSEDS3_SECRET_KEY:?Set DECOMPOSEDS3_SECRET_KEY before starting MinIO in production}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.minio.entrypoints=https"
|
||||
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
# basic auth (not recommended, but needed for eg. WebDav clients that do not support OpenID Connect)
|
||||
PROXY_ENABLE_BASIC_AUTH: "${PROXY_ENABLE_BASIC_AUTH:-false}"
|
||||
# admin user password
|
||||
IDM_ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" # this overrides the admin password from the configuration file
|
||||
IDM_ADMIN_PASSWORD: "${ADMIN_PASSWORD:?Set ADMIN_PASSWORD before starting qsfera_full in production}" # this overrides the admin password from the configuration file
|
||||
# demo users
|
||||
IDM_CREATE_DEMO_USERS: "${DEMO_USERS:-false}"
|
||||
# idp login form settings
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
ENV_FILE="${1:-.env}"
|
||||
errors=0
|
||||
warnings=0
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "ERROR: env file not found: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
value() {
|
||||
key="$1"
|
||||
awk -v key="$key" '
|
||||
/^[[:space:]]*#/ || /^[[:space:]]*$/ { next }
|
||||
{
|
||||
line = $0
|
||||
sub(/\r$/, "", line)
|
||||
split(line, parts, "=")
|
||||
current = parts[1]
|
||||
gsub(/^[ \t]+|[ \t]+$/, "", current)
|
||||
if (current == key) {
|
||||
sub(/^[^=]*=/, "", line)
|
||||
gsub(/^"/, "", line)
|
||||
gsub(/"$/, "", line)
|
||||
gsub(/^'\''/, "", line)
|
||||
gsub(/'\''$/, "", line)
|
||||
print line
|
||||
exit
|
||||
}
|
||||
}
|
||||
' "$ENV_FILE"
|
||||
}
|
||||
|
||||
is_placeholder() {
|
||||
case "${1:-}" in
|
||||
""|"<"*">"|*example.com*|"change-me"|"changeme"|"password"|"secret")
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
error() {
|
||||
errors=$((errors + 1))
|
||||
echo "ERROR: $1" >&2
|
||||
}
|
||||
|
||||
warn() {
|
||||
warnings=$((warnings + 1))
|
||||
echo "WARN: $1" >&2
|
||||
}
|
||||
|
||||
require_value() {
|
||||
key="$1"
|
||||
label="${2:-$1}"
|
||||
current="$(value "$key")"
|
||||
if is_placeholder "$current"; then
|
||||
error "$label must be set to a production value."
|
||||
fi
|
||||
}
|
||||
|
||||
require_secret() {
|
||||
key="$1"
|
||||
current="$(value "$key")"
|
||||
if is_placeholder "$current"; then
|
||||
error "$key must be set."
|
||||
return
|
||||
fi
|
||||
case "$current" in
|
||||
admin|demo|keycloak|qsfera|qsfera-secret-key)
|
||||
error "$key uses a known demo/default value."
|
||||
return
|
||||
;;
|
||||
esac
|
||||
if [ "${#current}" -lt 16 ]; then
|
||||
error "$key must be at least 16 characters."
|
||||
fi
|
||||
}
|
||||
|
||||
enabled() {
|
||||
current="$(value "$1")"
|
||||
[ -n "$current" ]
|
||||
}
|
||||
|
||||
require_value OC_DOMAIN "OC_DOMAIN"
|
||||
require_value TRAEFIK_ACME_MAIL "TRAEFIK_ACME_MAIL"
|
||||
require_secret ADMIN_PASSWORD
|
||||
|
||||
case "$(value INSECURE)" in
|
||||
true|TRUE|1|yes|YES)
|
||||
error "INSECURE must be false for production."
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(value DEMO_USERS)" in
|
||||
true|TRUE|1|yes|YES)
|
||||
error "DEMO_USERS must be false for production."
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(value OC_DOCKER_IMAGE)" in
|
||||
qsfera/qsfera)
|
||||
;;
|
||||
"")
|
||||
warn "OC_DOCKER_IMAGE is empty; compose default must be verified before rollout."
|
||||
;;
|
||||
*)
|
||||
error "OC_DOCKER_IMAGE must be qsfera/qsfera for production."
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(value OC_DOCKER_TAG)" in
|
||||
""|latest|"<"*">")
|
||||
error "OC_DOCKER_TAG must pin a concrete production release tag."
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! enabled QSFERA; then
|
||||
error "QSFERA=:qsfera.yml must be enabled."
|
||||
fi
|
||||
|
||||
if enabled DECOMPOSEDS3; then
|
||||
require_value DECOMPOSEDS3_ENDPOINT "DECOMPOSEDS3_ENDPOINT"
|
||||
require_value DECOMPOSEDS3_REGION "DECOMPOSEDS3_REGION"
|
||||
require_secret DECOMPOSEDS3_ACCESS_KEY
|
||||
require_secret DECOMPOSEDS3_SECRET_KEY
|
||||
require_value DECOMPOSEDS3_BUCKET "DECOMPOSEDS3_BUCKET"
|
||||
fi
|
||||
|
||||
if enabled DECOMPOSEDS3_MINIO; then
|
||||
error "DECOMPOSEDS3_MINIO is for test/lab installs and must not be enabled for public production."
|
||||
fi
|
||||
|
||||
if enabled KEYCLOAK; then
|
||||
require_value KEYCLOAK_DOMAIN "KEYCLOAK_DOMAIN"
|
||||
require_value KEYCLOAK_REALM "KEYCLOAK_REALM"
|
||||
require_secret KEYCLOAK_POSTGRES_PASSWORD
|
||||
require_value KEYCLOAK_ADMIN_USER "KEYCLOAK_ADMIN_USER"
|
||||
require_secret KEYCLOAK_ADMIN_PASSWORD
|
||||
fi
|
||||
|
||||
if [ -z "$(value OC_CONFIG_DIR)" ] || [ -z "$(value OC_DATA_DIR)" ]; then
|
||||
warn "OC_CONFIG_DIR and OC_DATA_DIR are not both set; docker volumes are harder to back up and restore."
|
||||
fi
|
||||
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
echo "Production env validation failed: $errors error(s), $warnings warning(s)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Production env validation passed: $warnings warning(s)."
|
||||
@@ -0,0 +1,35 @@
|
||||
services:
|
||||
qsfera-cloud-server:
|
||||
container_name: qsfera-cloud-server
|
||||
image: ${QSFERA_RPI_IMAGE:-qsfera-cloud-server:rpi5-local}
|
||||
platform: linux/arm64
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
TARGETARCH: arm64
|
||||
restart: unless-stopped
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
- -c
|
||||
- qsfera init || true; exec qsfera server
|
||||
environment:
|
||||
OC_LOG_LEVEL: ${OC_LOG_LEVEL:-info}
|
||||
FRONTEND_CHECK_FOR_UPDATES: ${FRONTEND_CHECK_FOR_UPDATES:-false}
|
||||
OC_CONFIG_DIR: /etc/qsfera
|
||||
OC_BASE_DATA_PATH: /var/lib/qsfera
|
||||
OC_URL: ${OC_URL:?OC_URL must be set in .env}
|
||||
OC_INSECURE: ${OC_INSECURE:-false}
|
||||
PROXY_TLS: ${PROXY_TLS:-true}
|
||||
ports:
|
||||
- "127.0.0.1:${QSFERA_RPI_HOST_PORT:-9200}:9200"
|
||||
volumes:
|
||||
- ${QSFERA_RPI_CONFIG_PATH:?QSFERA_RPI_CONFIG_PATH must be set in .env}:/etc/qsfera
|
||||
- ${QSFERA_RPI_DATA_PATH:?QSFERA_RPI_DATA_PATH must be set in .env}:/var/lib/qsfera
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -kfsS https://127.0.0.1:9200/status.php >/dev/null || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
@@ -0,0 +1,107 @@
|
||||
# Raspberry Pi 5 Docker Run
|
||||
|
||||
This server repository contains a Dockerized QSfera path for Raspberry Pi 5 and other
|
||||
64-bit ARM Linux hosts.
|
||||
|
||||
Files used for this deployment:
|
||||
|
||||
- `Dockerfile`
|
||||
- `docker-compose.rpi5.yml`
|
||||
- `.env.example`
|
||||
|
||||
## What This Runs
|
||||
|
||||
- QSfera server from this `Server` source tree.
|
||||
- QSfera config persisted under `/etc/qsfera` inside the container.
|
||||
- QSfera data persisted under `/var/lib/qsfera` inside the container.
|
||||
- Host-side config and data paths controlled by `QSFERA_RPI_CONFIG_PATH` and
|
||||
`QSFERA_RPI_DATA_PATH`.
|
||||
- Host-side backend port bound only to `127.0.0.1:9200`; external access should go
|
||||
through the reverse proxy and HTTPS domain.
|
||||
|
||||
## Prerequisites On Raspberry Pi 5
|
||||
|
||||
1. Install Docker Engine using the official Docker docs: `https://docs.docker.com/engine/install/debian/`.
|
||||
2. Install the Docker Compose plugin using the official Docker docs: `https://docs.docker.com/compose/install/`.
|
||||
3. Use a 64-bit OS. The compose file sets `platform: linux/arm64`, and the Dockerfile builds with `TARGETARCH=arm64`.
|
||||
4. Put `QSFERA_RPI_CONFIG_PATH` and `QSFERA_RPI_DATA_PATH` on durable storage, not on a nearly full SD-card root filesystem.
|
||||
5. Set `OC_URL` to the public HTTPS URL used by clients and the reverse proxy.
|
||||
|
||||
## Start
|
||||
|
||||
Run from the `Server` directory:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose -f docker-compose.rpi5.yml up -d --build
|
||||
```
|
||||
|
||||
## Stop
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.rpi5.yml down
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.rpi5.yml logs -f qsfera-cloud-server
|
||||
```
|
||||
|
||||
## Update After Pulling New Code
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.rpi5.yml up -d --build
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
Local backend status endpoint:
|
||||
|
||||
```bash
|
||||
curl -k https://127.0.0.1:9200/status.php
|
||||
```
|
||||
|
||||
External reverse-proxied status endpoint:
|
||||
|
||||
```bash
|
||||
curl https://qsfera.example.com/status.php
|
||||
```
|
||||
|
||||
## Data Location
|
||||
|
||||
Docker bind mounts:
|
||||
|
||||
- host config: `${QSFERA_RPI_CONFIG_PATH}`
|
||||
- container config: `/etc/qsfera`
|
||||
- host data: `${QSFERA_RPI_DATA_PATH}`
|
||||
- container data: `/var/lib/qsfera`
|
||||
|
||||
For the current Raspberry Pi deployment inspected on 2026-06-08, the existing
|
||||
container uses:
|
||||
|
||||
- host config: `/mnt/data/qsfera/cloud-server/config`
|
||||
- host data: `/mnt/data/qsfera/cloud-server/data`
|
||||
- public URL: `https://qsfera.kusoft.xyz`
|
||||
- loopback port: `127.0.0.1:9200`
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
The server is intentionally bound to loopback. Put Caddy, Nginx, Traefik, or another
|
||||
reverse proxy in front of it for external HTTPS access.
|
||||
|
||||
The inspected Raspberry Pi uses Caddy with this QSfera route:
|
||||
|
||||
```caddyfile
|
||||
qsfera.kusoft.xyz {
|
||||
encode zstd gzip
|
||||
handle {
|
||||
reverse_proxy https://127.0.0.1:9200 {
|
||||
transport http {
|
||||
tls_insecure_skip_verify
|
||||
versions 1.1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user