This commit is contained in:
2026-02-03 22:20:03 -03:00
parent aea93418c5
commit db298babfc
14 changed files with 672 additions and 0 deletions

42
.env Normal file
View File

@@ -0,0 +1,42 @@
LOCAL_TIMEZONE="America/Argentina/Buenos_Aires"
STORAGE_VOLUME="/storage"
APPS_VOLUME="/containers"
GID="${UID}"
PUBLIC_IP="181.14.212.99"
LOCAL_IP="192.168.1.239"
OLLAMA_SERVER="192.168.1.135:11434"
DOMAIN_NAME="sudestec.ar"
PROXY_HTTP_PORT="7700"
PROXY_HTTPS_PORT="7701"
PROXY_PORT="7702"
NEXTCLOUD_PORT="7710"
ONLYOFFICE_PORT="7711"
POCKETBASE_PORT="7720"
GITEA_PORT="7730"
GITEA_PORT2="7731"
FLATNOTE_PORT="7740"
FLATNOTE_NAME="flatnote"
SUPABASE_DATABASE_PASSWORD="facU8990"
SUPABASE_DATABASE="supabase-db"
NEXTCLOUD_TRUSTED_DOMAINS="${LOCAL_IP} ${DOMAIN_NAME}"
NEXTCLOUD_REDIS_ENABLED=""
NEXTCLOUD_VERSION="stable"
OPENWEBUI_PORT=""
VIRTMANAGER_PORT=""
WIREGUARD_PORT=""
GITEA_NAME="git-sudeste"
WIREGUARD_PEERS="10"
WIREGUARD_SUBNET="7.7.7.0"
ADMIN_EMAIL="facu.vdp@gmail.com"
ADMIN_USERNAME="sudeste"
ADMIN_PASSWORD="facU8990!$"
POCKETBASE_VERSION="latest"
POCKETBASE_NAME="pocketbase"
POCKETBASE_TRUSTED_DOMAINS="127.0.0.1,localhost,server-sudestec,192.168.1.239,own.sudestec.ar"
SPEEDTEST_SCHEDULE="*/30 * * * *"
SPEEDTEST_SERVERS="13449,54503"
PHP_MEMORY_LIMIT="1024M"
PHP_UPLOAD_LIMIT="10240M"
JWT_SECRET="100847038046ae5cb373204d405da78c0817285b17e3212a8a2970bf4605cfc2"
ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODg2OTQ4LCJleHAiOjE3Njg4OTA1NDh9.HlpsCqlfMYmHH8ZO-X-Q099awds6o71CEq0qwpbGUSY"
SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3Njg4ODY5NDgsImV4cCI6MjA4NDI0Njk0OH0.MW_D_r9FBh9EC-DR8G1mJedXC8EaZyTZNYkGafDE3fU"

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@ test-servers/
.bundle .bundle
vendor vendor
test
secret

2
.gitignore copy Normal file
View File

@@ -0,0 +1,2 @@
test
secret

View File

@@ -0,0 +1,33 @@
#! /bin/bash
source .env
proxy_path=${APPS_VOLUME}/proxy
proxy_data=${proxy_path}/data
proxy_letsencrypt=${proxy_path}/letsencrypt
for p in "${proxy_data}" "${proxy_letsencrypt}"; do
if ! ls ${p} >/dev/null 2>&1; then
echo "Creating ${p} directory..."
mkdir -p ${p}
else
echo "directory exists ${p}"
fi
done
podman run -d --replace \
--name proxy_server \
--hostname proxy_server \
--network container-bridge \
--tz=local \
-p ${PROXY_PORT}:81 \
-p ${PROXY_HTTP_PORT}:80 \
-p ${PROXY_HTTPS_PORT}:443 \
-v ${proxy_data}:/data \
-v ${proxy_letsencrypt}:/etc/letsencrypt \
--health-cmd="/usr/bin/check-health" \
--health-interval=10s \
--health-timeout=3s \
-e DISABLE_IPV6=true \
-e INITIAL_ADMIN_EMAIL=${ADMIN_EMAIL} \
-e INITIAL_ADMIN_PASSWORD=${ADMIN_PASSWORD} \
docker.io/jc21/nginx-proxy-manager:latest

28
commands/02-start-redis.sh Executable file
View File

@@ -0,0 +1,28 @@
#! /bin/bash
source .env
redis_path=${APPS_VOLUME}/redis
for p in "${redis_path}"; do
if ! ls ${p} >/dev/null 2>&1; then
echo "Creating ${p} directory..."
mkdir -p ${p}
else
echo "directory exists ${p}"
fi
done
podman run -d --replace \
--name redis \
--hostname redis \
--network container-bridge \
--tz=local \
-v ${redis_path}:/data \
--health-cmd="redis-cli ping" \
--health-interval=10s \
--health-timeout=5s \
--health-retries=5 \
docker.io/redis:6 \
--databases 1
podman generate systemd --new --files --name redis

40
commands/03-start-nextcloud.sh Executable file
View File

@@ -0,0 +1,40 @@
#! /bin/bash
source .env
# Create container directories
nc_app=${APPS_VOLUME}/nextcloud
nc_data=${STORAGE_VOLUME}/nextcloud
for p in "${nc_app} ${nc_data}"; do
if ! ls ${p} >/dev/null 2>&1; then
echo "Creating ${p} directory..."
mkdir -p ${p}
else
echo "directory exists ${p}"
fi
done
podman run -d --replace \
--name nextcloud_server \
--hostname nextcloud_server \
--restart=always \
--network container-bridge \
--tz=local \
-p ${NEXTCLOUD_PORT}:80 \
-e NEXTCLOUD_ADMIN_USER=${ADMIN_USERNAME} \
-e NEXTCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD} \
-e NEXTCLOUD_TRUSTED_DOMAINS="cloud.${DOMAIN_NAME} ${LOCAL_IP} ${PUBLIC_IP}" \
-e TRUSTED_PROXIES=proxy_server \
-e TRUSTED_PROXIES=${LOCAL_IP}:${HTTPS_PORT} \
-e OVERWRITECLIURL=https://cloud.${DOMAIN_NAME} \
-e SQLITE_DATABASE=nextcloud_db \
-e REDIS_HOST=redis \
-e PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT} \
-e PHP_UPLOAD_LIMIT=${PHP_UPLOAD_LIMIT} \
-v ${nc_app}:/var/www/html \
-v ${nc_data}:/var/www/html/data \
--health-interval=30s \
--health-timeout=10s \
--health-retries=5 \
--health-cmd="curl -f ${LOCAL_IP}:${NEXTCLOUD_PORT}" \
docker.io/nextcloud:${NEXTCLOUD_VERSION}

30
commands/04-start-pocketbase.sh Executable file
View File

@@ -0,0 +1,30 @@
#! /bin/bash
source .env
pb_ap=${STORAGE_VOLUME}/${POCKETBASE_NAME}
for p in "${pb_ap}/data ${pb_ap}/public"; do
if ! ls ${p} >/dev/null 2>&1; then
echo "Creating ${p} directory..."
mkdir -p ${p}
else
echo "directory exists ${p}"
fi
done
podman run -d --replace \
--name ${POCKETBASE_NAME} \
--hostname ${POCKETBASE_NAME} \
--restart=always \
--network container-bridge \
-p ${POCKETBASE_PORT}:${POCKETBASE_PORT} \
-e PB_PORT=${POCKETBASE_PORT} \
-e PB_ADMIN_EMAIL=${ADMIN_EMAIL} \
-e PB_ADMIN_PASSWORD=${ADMIN_PASSWORD} \
-v ${pb_ap}/data:/pb_data \
-v ${pb_ap}/public:/public \
--health-cmd="wget --no-verbose --tries=1 --spider ${LOCAL_IP}:${POCKETBASE_PORT}/api/health" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=5 \
ghcr.io/muchobien/pocketbase:latest

31
commands/05-start-gitea.sh Executable file
View File

@@ -0,0 +1,31 @@
#! /bin/bash
source .env
git_data=${STORAGE_VOLUME}/${GITEA_NAME}
for p in "${git_data}"; do
if ! ls ${p} >/dev/null 2>&1; then
echo "Creating ${p} directory..."
mkdir -p ${p}
else
echo "directory exists ${p}"
fi
done
podman run -d --replace \
--name ${GITEA_NAME} \
--hostname ${GITEA_NAME} \
--restart=always \
--network container-bridge \
-p ${GITEA_PORT}:3000 \
-p ${GITEA_PORT2}:2222 \
-v ${git_data}:/data \
-v /etc/timezone:/etc/timezone:ro \
-v /etc/localtime:/etc/localtime:ro \
-e USER_UID=1000 \
-e USER_GID=1000 \
--health-cmd="curl -f ${LOCAL_IP}:${GITEA_PORT}/api/healthz" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=5 \
docker.gitea.com/gitea:latest

33
commands/06-start-flatnotes.sh Executable file
View File

@@ -0,0 +1,33 @@
#! /bin/bash
source .env
git_data=${STORAGE_VOLUME}/${FLATNOTE_NAME}
for p in "${git_data}"; do
if ! ls ${p} >/dev/null 2>&1; then
echo "Creating ${p} directory..."
mkdir -p ${p}
else
echo "directory exists ${p}"
fi
done
podman run -d --replace \
--name ${FLATNOTE_NAME} \
--hostname ${FLATNOTE_NAME} \
--restart=always \
--network container-bridge \
-p ${FLATNOTE_PORT}:8080 \
-v ${git_data}:/data \
--userns=keep-id:uid=1000,gid=1000 \
-e PUID=1000 \
-e PGID=1000 \
-e FLATNOTES_AUTH_TYPE=password \
-e FLATNOTES_USERNAME=${ADMIN_USERNAME} \
-e FLATNOTES_PASSWORD=${ADMIN_PASSWORD} \
-e FLATNOTES_SECRET_KEY=${ANON_KEY} \
--health-cmd="curl -f ${LOCAL_IP}:${FLATNOTE_PORT}/health" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=5 \
docker.io/dullage/flatnotes:latest

20
commands/DELETE.sh Executable file
View File

@@ -0,0 +1,20 @@
#! /bin/bash
echo "⚠️ WARNING: This will permanently delete data."
select choice in "Abort" "Continue"; do
case "$choice" in
"Abort")
echo "Aborted."
exit 1
;;
"Continue")
break
;;
esac
done
podman container stop --all
podman system prune --all
sudo rm -rdf /storage/flatnote

55
commands/generate-keys.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/sh
set -eu
# === config ===
JWT_EXP_ANON=3600
JWT_EXP_SERVICE=315360000 # 10 years
JWT_ISSUER="supabase"
# === helpers ===
b64url() {
openssl base64 -A | tr '+/' '-_' | tr -d '='
}
jwt_sign() {
header=$1
payload=$2
secret=$3
header_b64=$(printf '%s' "$header" | b64url)
payload_b64=$(printf '%s' "$payload" | b64url)
sig=$(printf '%s.%s' "$header_b64" "$payload_b64" |
openssl dgst -binary -sha256 -hmac "$secret" | b64url)
printf '%s.%s.%s\n' "$header_b64" "$payload_b64" "$sig"
}
# === generate JWT secret ===
JWT_SECRET=$(openssl rand -hex 32)
NOW=$(date +%s)
JWT_HEADER='{"alg":"HS256","typ":"JWT"}'
ANON_PAYLOAD=$(
cat <<EOF
{"role":"anon","iss":"$JWT_ISSUER","iat":$NOW,"exp":$((NOW + JWT_EXP_ANON))}
EOF
)
SERVICE_PAYLOAD=$(
cat <<EOF
{"role":"service_role","iss":"$JWT_ISSUER","iat":$NOW,"exp":$((NOW + JWT_EXP_SERVICE))}
EOF
)
ANON_KEY=$(jwt_sign "$JWT_HEADER" "$ANON_PAYLOAD" "$JWT_SECRET")
SERVICE_ROLE_KEY=$(jwt_sign "$JWT_HEADER" "$SERVICE_PAYLOAD" "$JWT_SECRET")
# === output .env-compatible ===
cat <<EOF
JWT_SECRET=$JWT_SECRET
ANON_KEY=$ANON_KEY
SERVICE_ROLE_KEY=$SERVICE_ROLE_KEY
EOF

63
commands/init.sh Executable file
View File

@@ -0,0 +1,63 @@
#! /bin/bash
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
cd "$SCRIPT_DIR" || exit 1
# write new secret:
if [[ -f "secret" ]]; then
echo "Secret found..." >&2
else
echo "Secret created..." >&2
openssl rand -hex 32 >./secret
fi
# create network
if ! podman network inspect container-bridge >/dev/null 2>&1; then
echo "Creating container-bridge network..."
podman network create container-bridge
else
echo "container-bridge network already exists"
fi
# Get podman start scripts
shopt -s nullglob
# start_files=( *-start-*.sh )
containers=()
# Run every start stcript
for f in *-start-*.sh; do
trim="${f%.sh}"
container="${trim#*start-}"
containers+=("$container")
echo "Running $container"
chmod +x "$f"
./"$f"
done
# Generate systemd Units
# mkdir -p ~/.config/systemd/user
# for f in "${containers[@]}"; do
# echo "Generating systemd Unit for $f"
# podman generate systemd --files --name $f
# if ! [ -e "patch-${f}-service.sh" ]; then
# echo "No patch for container-${f}.service"
# else
# echo "Patching container-${f}.service..."
#
# fi
# mv container-$f.service ~/.config/systemd/user/
# done
# TODO
# Add to container-owncloud_server.service
# Requires=container-owncloud_mariadb.service
# Requires=container-owncloud_redis.service
# After=container-owncloud_mariadb.service
# After=container-owncloud_redis.service
# Add to container-proxy_server.service
#
#
#
#
#

283
commands/kong.yml Normal file
View File

@@ -0,0 +1,283 @@
_format_version: '2.1'
_transform: true
###
### Consumers / Users
###
consumers:
- username: DASHBOARD
- username: anon
keyauth_credentials:
- key: $SUPABASE_ANON_KEY
- username: service_role
keyauth_credentials:
- key: $SUPABASE_SERVICE_KEY
###
### Access Control List
###
acls:
- consumer: anon
group: anon
- consumer: service_role
group: admin
###
### Dashboard credentials
###
basicauth_credentials:
- consumer: DASHBOARD
username: $DASHBOARD_USERNAME
password: $DASHBOARD_PASSWORD
###
### API Routes
###
services:
## Open Auth routes
- name: auth-v1-open
url: http://auth:9999/verify
routes:
- name: auth-v1-open
strip_path: true
paths:
- /auth/v1/verify
plugins:
- name: cors
- name: auth-v1-open-callback
url: http://auth:9999/callback
routes:
- name: auth-v1-open-callback
strip_path: true
paths:
- /auth/v1/callback
plugins:
- name: cors
- name: auth-v1-open-authorize
url: http://auth:9999/authorize
routes:
- name: auth-v1-open-authorize
strip_path: true
paths:
- /auth/v1/authorize
plugins:
- name: cors
## Secure Auth routes
- name: auth-v1
_comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*'
url: http://auth:9999/
routes:
- name: auth-v1-all
strip_path: true
paths:
- /auth/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Secure REST routes
- name: rest-v1
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
url: http://rest:3000/
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Secure GraphQL routes
- name: graphql-v1
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
url: http://rest:3000/rpc/graphql
routes:
- name: graphql-v1-all
strip_path: true
paths:
- /graphql/v1
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
- name: request-transformer
config:
add:
headers:
- Content-Profile:graphql_public
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Secure Realtime routes
- name: realtime-v1-ws
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
url: http://realtime-dev.supabase-realtime:4000/socket
protocol: ws
routes:
- name: realtime-v1-ws
strip_path: true
paths:
- /realtime/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
- name: realtime-v1-rest
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
url: http://realtime-dev.supabase-realtime:4000/api
protocol: http
routes:
- name: realtime-v1-rest
strip_path: true
paths:
- /realtime/v1/api
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Storage routes: the storage server manages its own auth
- name: storage-v1
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
url: http://storage:5000/
routes:
- name: storage-v1-all
strip_path: true
paths:
- /storage/v1/
plugins:
- name: cors
## Edge Functions routes
- name: functions-v1
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
url: http://functions:9000/
routes:
- name: functions-v1-all
strip_path: true
paths:
- /functions/v1/
plugins:
- name: cors
## Analytics routes
- name: analytics-v1
_comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
url: http://analytics:4000/
routes:
- name: analytics-v1-all
strip_path: true
paths:
- /analytics/v1/
## Secure Database routes
- name: meta
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
url: http://meta:8080/
routes:
- name: meta-all
strip_path: true
paths:
- /pg/
plugins:
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
## Block access to /api/mcp
- name: mcp-blocker
_comment: 'Block direct access to /api/mcp'
url: http://studio:3000/api/mcp
routes:
- name: mcp-blocker-route
strip_path: true
paths:
- /api/mcp
plugins:
- name: request-termination
config:
status_code: 403
message: "Access is forbidden."
## MCP endpoint - local access
- name: mcp
_comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)'
url: http://studio:3000/api/mcp
routes:
- name: mcp
strip_path: true
paths:
- /mcp
plugins:
# Block access to /mcp by default
- name: request-termination
config:
status_code: 403
message: "Access is forbidden."
# Enable local access (danger zone!)
# 1. Comment out the 'request-termination' section above
# 2. Uncomment the entire section below, including 'deny'
# 3. Add your local IPs to the 'allow' list
#- name: cors
#- name: ip-restriction
# config:
# allow:
# - 127.0.0.1
# - ::1
# deny: []
## Protected Dashboard - catch all remaining routes
- name: dashboard
_comment: 'Studio: /* -> http://studio:3000/*'
url: http://studio:3000/
routes:
- name: dashboard-all
strip_path: true
paths:
- /
plugins:
- name: cors
- name: basic-auth
config:
hide_credentials: true

10
commands/secret.sh Executable file
View File

@@ -0,0 +1,10 @@
#! /bin/bash
if [[ -f "secret" ]]; then
echo "Found a SECRET! Shhhhh..." >&2
SECRET=$(<secret)
else
echo "Creating New SECRET! Shhhhh..." >&2
openssl rand -hex 32 >./secret
SECRET=$(<secret)
fi