From 52588fc8f1c74488ebda82e5f00f666297029b7b Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 4 Dec 2025 01:00:41 +0100 Subject: [PATCH] Initial commit: LetsBe automated server setup scripts --- .claude/settings.local.json | 17 + script/MANUAL_SETUP.md | 1092 +++++++++++++++++ script/README.md | 129 ++ script/backups.sh | 109 ++ script/config.sample.json | 26 + script/env_setup - Copy.shZone.Identifier | 0 script/env_setup.sh | 500 ++++++++ script/id_ed25519 | 8 + script/id_ed25519Zone.Identifier | 3 + script/initial_setup_backup.zip | Bin 0 -> 77323 bytes script/network_plan/plan-netzwerk-docker.txt | 220 ++++ script/network_plan/plan-ports-docker.txt | 71 ++ script/nginx/activepieces.conf | 60 + script/nginx/baserow.conf | 53 + script/nginx/botlab.conf | 39 + script/nginx/bots.conf | 36 + script/nginx/calcom.conf | 53 + script/nginx/chatwoot.conf | 107 ++ script/nginx/documenso.conf | 40 + script/nginx/flame.conf | 53 + script/nginx/ghost.conf | 40 + script/nginx/gitea-drine.conf | 53 + script/nginx/gitea.conf | 53 + script/nginx/glitchtip.conf | 53 + script/nginx/html.conf | 53 + script/nginx/keycloak.conf | 46 + script/nginx/librechat.conf | 44 + script/nginx/listmonk.conf | 49 + script/nginx/minio.conf | 110 ++ script/nginx/n8n.conf | 53 + script/nginx/nextcloud.conf | 185 +++ script/nginx/nocodb.conf | 67 + script/nginx/odoo.conf | 53 + script/nginx/penpot.conf | 53 + script/nginx/poste.conf | 61 + script/nginx/redash.conf | 51 + script/nginx/s3.conf | 52 + script/nginx/squidex.conf | 53 + script/nginx/stirlingpdf.conf | 46 + script/nginx/typebot.conf | 69 ++ script/nginx/umami.conf | 53 + script/nginx/uptime-kuma.conf | 53 + script/nginx/whiteboard.conf | 53 + script/nginx/windmill.conf | 53 + script/nginx/wordpress.conf | 53 + script/setup.sh | 662 ++++++++++ script/stacks/activepieces/.env | 31 + script/stacks/activepieces/docker-compose.yml | 52 + script/stacks/baserow/docker-compose.yml | 61 + script/stacks/calcom/.env | 61 + script/stacks/calcom/docker-compose.yml | 43 + script/stacks/chatwoot/.env | 239 ++++ script/stacks/chatwoot/docker-compose.yml | 121 ++ script/stacks/diun-watchtower/diun.yml | 21 + .../stacks/diun-watchtower/docker-compose.yml | 19 + script/stacks/documenso/.env | 47 + script/stacks/documenso/docker-compose.yml | 60 + script/stacks/ghost/config.production.json | 48 + script/stacks/ghost/docker-compose.yml | 53 + script/stacks/gitea-drone/docker-compose.yml | 37 + script/stacks/gitea/docker-compose.yml | 76 ++ script/stacks/glitchtip/docker-compose.yml | 109 ++ script/stacks/html/docker-compose.yml | 29 + script/stacks/keycloak/.env | 10 + script/stacks/keycloak/docker-compose.yml | 54 + script/stacks/librechat/.env | 574 +++++++++ script/stacks/librechat/docker-compose.yml | 96 ++ script/stacks/librechat/librechat.yaml | 318 +++++ script/stacks/listmonk/config.toml | 15 + script/stacks/listmonk/docker-compose.yml | 57 + script/stacks/minio/docker-compose.yml | 32 + script/stacks/n8n/docker-compose.yml | 63 + script/stacks/nextcloud/docker-compose.yml | 130 ++ script/stacks/nocodb/docker-compose.yml | 55 + script/stacks/odoo/docker-compose.yml | 55 + script/stacks/penpot/docker-compose.yml | 165 +++ script/stacks/portainer/docker-compose.yml | 32 + script/stacks/poste/docker-compose.yml | 47 + script/stacks/redash/.env | 16 + script/stacks/redash/docker-compose.yml | 101 ++ script/stacks/squidex/docker-compose.yml | 61 + script/stacks/stirlingpdf/docker-compose.yml | 54 + script/stacks/sysadmin/docker-compose.yml | 68 + script/stacks/typebot/.env | 37 + script/stacks/typebot/docker-compose.yml | 60 + script/stacks/umami/docker-compose.yml | 53 + script/stacks/uptime-kuma/docker-compose.yml | 29 + script/stacks/windmill/Caddyfile | 6 + script/stacks/windmill/docker-compose.yml | 166 +++ script/stacks/windmill/oauth.json | 1 + script/stacks/wordpress/docker-compose.yml | 55 + script/start.sh | 389 ++++++ 92 files changed, 8693 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 script/MANUAL_SETUP.md create mode 100644 script/README.md create mode 100644 script/backups.sh create mode 100644 script/config.sample.json create mode 100644 script/env_setup - Copy.shZone.Identifier create mode 100644 script/env_setup.sh create mode 100644 script/id_ed25519 create mode 100644 script/id_ed25519Zone.Identifier create mode 100644 script/initial_setup_backup.zip create mode 100644 script/network_plan/plan-netzwerk-docker.txt create mode 100644 script/network_plan/plan-ports-docker.txt create mode 100644 script/nginx/activepieces.conf create mode 100644 script/nginx/baserow.conf create mode 100644 script/nginx/botlab.conf create mode 100644 script/nginx/bots.conf create mode 100644 script/nginx/calcom.conf create mode 100644 script/nginx/chatwoot.conf create mode 100644 script/nginx/documenso.conf create mode 100644 script/nginx/flame.conf create mode 100644 script/nginx/ghost.conf create mode 100644 script/nginx/gitea-drine.conf create mode 100644 script/nginx/gitea.conf create mode 100644 script/nginx/glitchtip.conf create mode 100644 script/nginx/html.conf create mode 100644 script/nginx/keycloak.conf create mode 100644 script/nginx/librechat.conf create mode 100644 script/nginx/listmonk.conf create mode 100644 script/nginx/minio.conf create mode 100644 script/nginx/n8n.conf create mode 100644 script/nginx/nextcloud.conf create mode 100644 script/nginx/nocodb.conf create mode 100644 script/nginx/odoo.conf create mode 100644 script/nginx/penpot.conf create mode 100644 script/nginx/poste.conf create mode 100644 script/nginx/redash.conf create mode 100644 script/nginx/s3.conf create mode 100644 script/nginx/squidex.conf create mode 100644 script/nginx/stirlingpdf.conf create mode 100644 script/nginx/typebot.conf create mode 100644 script/nginx/umami.conf create mode 100644 script/nginx/uptime-kuma.conf create mode 100644 script/nginx/whiteboard.conf create mode 100644 script/nginx/windmill.conf create mode 100644 script/nginx/wordpress.conf create mode 100644 script/setup.sh create mode 100644 script/stacks/activepieces/.env create mode 100644 script/stacks/activepieces/docker-compose.yml create mode 100644 script/stacks/baserow/docker-compose.yml create mode 100644 script/stacks/calcom/.env create mode 100644 script/stacks/calcom/docker-compose.yml create mode 100644 script/stacks/chatwoot/.env create mode 100644 script/stacks/chatwoot/docker-compose.yml create mode 100644 script/stacks/diun-watchtower/diun.yml create mode 100644 script/stacks/diun-watchtower/docker-compose.yml create mode 100644 script/stacks/documenso/.env create mode 100644 script/stacks/documenso/docker-compose.yml create mode 100644 script/stacks/ghost/config.production.json create mode 100644 script/stacks/ghost/docker-compose.yml create mode 100644 script/stacks/gitea-drone/docker-compose.yml create mode 100644 script/stacks/gitea/docker-compose.yml create mode 100644 script/stacks/glitchtip/docker-compose.yml create mode 100644 script/stacks/html/docker-compose.yml create mode 100644 script/stacks/keycloak/.env create mode 100644 script/stacks/keycloak/docker-compose.yml create mode 100644 script/stacks/librechat/.env create mode 100644 script/stacks/librechat/docker-compose.yml create mode 100644 script/stacks/librechat/librechat.yaml create mode 100644 script/stacks/listmonk/config.toml create mode 100644 script/stacks/listmonk/docker-compose.yml create mode 100644 script/stacks/minio/docker-compose.yml create mode 100644 script/stacks/n8n/docker-compose.yml create mode 100644 script/stacks/nextcloud/docker-compose.yml create mode 100644 script/stacks/nocodb/docker-compose.yml create mode 100644 script/stacks/odoo/docker-compose.yml create mode 100644 script/stacks/penpot/docker-compose.yml create mode 100644 script/stacks/portainer/docker-compose.yml create mode 100644 script/stacks/poste/docker-compose.yml create mode 100644 script/stacks/redash/.env create mode 100644 script/stacks/redash/docker-compose.yml create mode 100644 script/stacks/squidex/docker-compose.yml create mode 100644 script/stacks/stirlingpdf/docker-compose.yml create mode 100644 script/stacks/sysadmin/docker-compose.yml create mode 100644 script/stacks/typebot/.env create mode 100644 script/stacks/typebot/docker-compose.yml create mode 100644 script/stacks/umami/docker-compose.yml create mode 100644 script/stacks/uptime-kuma/docker-compose.yml create mode 100644 script/stacks/windmill/Caddyfile create mode 100644 script/stacks/windmill/docker-compose.yml create mode 100644 script/stacks/windmill/oauth.json create mode 100644 script/stacks/wordpress/docker-compose.yml create mode 100644 script/start.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..7040004 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(wsl bash -c 'sshpass -p \"\"enQ3xKdjy5vQFeK\"\" ssh -o StrictHostKeyChecking=accept-new root@46.38.236.53 \"\"cat /var/log/letsbe-setup.log 2>/dev/null | tail -50\"\"')", + "Bash(dir:*)", + "mcp__serena__list_dir", + "Bash(find:*)", + "Bash(xargs -I {} sh -c 'echo \"\"\"\"=== {} ===\"\"\"\" && grep \"\"\"\"server_name\"\"\"\" \"\"\"\"{}\"\"\"\"')", + "Bash(wsl bash:*)", + "Bash(git init:*)", + "Bash(git add:*)", + "Bash(git commit:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/script/MANUAL_SETUP.md b/script/MANUAL_SETUP.md new file mode 100644 index 0000000..5ba9bc2 --- /dev/null +++ b/script/MANUAL_SETUP.md @@ -0,0 +1,1092 @@ +# Manual Setup Guide + +This document contains detailed manual configuration instructions for LetsBe infrastructure components. + +> **Note**: Most of these steps are handled automatically by the deployment scripts. This guide is for manual intervention, troubleshooting, and advanced configuration. + +--- + +## Table of Contents +- [Docker Compose Updates](#updating-any-app-with-docker-compose) +- [DNS Configuration](#domain-dns-entries) +- [Required Subdomains](#required-subdomains-a-records) +- [Post-Installation Admin Setup](#after-installation) +- [Email Server Configuration](#e-mails-dns-settings-example) +- [Tool-Specific Setup](#tool-specific-setup) + - [Calcom](#calcom-setup-post-ansible) + - [Baserow](#baserow-setup) + - [Chatwoot](#chatwoot-setup) + - [MinIO](#minio) + - [Getmail6](#setting-up-getmail6) + - [Nextcloud](#upgrade-nextcloud-over-docker-exec) + - [Whiteboard](#whiteboard) + - [Typebot](#typebot-installation) + - [WordPress](#setting-up-wordpress-site-on-root-domain) +- [SSL Certificates](#finding-certificates-with-certbot) +- [Nginx Configuration Examples](#nginx-configuration-examples) + +--- + +## Updating Any App With Docker Compose + +```bash +docker compose down && docker compose pull && docker image prune -f && docker compose up -d +``` + +## Public Key + +Before running the start.sh script ensure the file "id_ed25519.key" is in the same path as the README.md here or add / decrypt. +Always keep the file "id_ed25519.key" secure as you can access all server with all permissions with it. + +## Domain DNS Entries + +Preset subdomains can be adjusted before executing the start.sh script under "env_setup.sh" part: "# Start - Set subdomain per tool". Note that you cannot use the same domain name more than once, each tool must have its own subdomain name. + +Each subdomain must then be added as an "A" DNS record in the domain settings, with the Server IP that you receive after purchasing a new server for the target customer. + +You should create the "A" DNS entries before starting the "start.sh" script and then wait at least 15 minutes before you go on. + +## Redash First Installation + +Make sure to uncomment ports in docker-compose. + +## Required Subdomains (A Records) + +Set up these A records before running the installation script. All should point to your server's IP address. + +### Core Subdomains +1. {root} - Main domain (e.g., client.com) +2. www - Standard www prefix + +### Tools & Applications +3. activepieces - Workflow automation (automation.client.com) +4. baserow - Database management (database.client.com) +5. calcom - Appointment scheduling (bookings.client.com) +6. chatwoot - Customer support (support.client.com) +7. collabora - Document collaboration (collabora.client.com) +8. documenso - Document signing (documenso.client.com) +9. ghost - Content management/blogging (blog.client.com) +10. gitea - Code repository (code.client.com) +11. glitchtip - Error tracking (debug.client.com) +12. html - Static website hosting (html.client.com) +13. keycloak - Authentication service (auth.client.com) +14. librechat - AI chat service (chat.client.com) +15. listmonk - Email newsletter service (newsletters.client.com) +16. nextcloud - File storage/collaboration (cloud.client.com) +17. nocodb - Low-code database (crm.client.com) +18. odoo - Business management (erp.client.com) +19. penpot - Design platform (design.client.com) +20. portainer - Container management (containers.client.com) +21. poste - Email server (mail.client.com) +22. redash - Data visualization (data.client.com) +23. squidex - Headless CMS (contenthub.client.com) +24. stirlingpdf - PDF manipulation (pdf.client.com) +25. typebot - Conversational forms (botlab.client.com and bots.client.com) +26. umami - Analytics (analytics.client.com) +27. uptime-kuma - Monitoring (uptime.client.com) +28. whiteboard - Collaborative whiteboarding (whiteboard.client.com) +29. windmill - Workflow automation (flows.client.com) +30. wordpress - Website/blog (www.client.com or client.com) + +### Infrastructure Subdomains +31. minio - Object storage admin interface (minio.client.com) +32. s3 - S3-compatible storage endpoint (s3.client.com) +33. n8n - Workflow automation alternative (n8n.client.com) +34. ci - Continuous integration (for Gitea/Drone) (ci.client.com) +35. marketing - Optional marketing site (marketing.client.com) +36. helpdesk - Help desk/documentation (helpdesk.client.com) + +--- + +## After Installation + +All tools must be prepared and the administrator account must be created/changed. This must be done immediately after installation: + +`*` = No Embedding for External Apps + +| Tool | Setup Required | +|------|----------------| +| baserow | Admin account must be created via the website | +| *calcom | Admin account must be created via the website | +| *chatwoot | Need to be changed (probably default: Username: "john@acme.inc", Password: "Password1!") | +| gitea | Admin account must be created via the website | +| glitchtip | Admin account must be created via the website under register new account | +| listmonk | Admin account details auto generated and can be found in initial_setup_backup.zip: stacks/listmonk/config.toml | +| *n8n | Admin account must be created via the website | +| nextcloud | Admin account details auto generated and can be found in initial_setup_backup.zip: stacks/nextcloud/docker-compose.yml under "app" & "environment" | +| nextcloud collabora | Admin account details auto generated and can be found in initial_setup_backup.zip: stacks/nextcloud/docker-compose.yml under "collabora" & "environment" | +| *odoo | Admin account must be created via the website | +| penpot | Admin account must be created via the website under register new account | +| *poste | Admin account must be created via the website | +| *squidex | Admin account must be created via the website under register new account | +| *umami | Need to be changed (default: Username: "admin", Password: "umami") | +| *uptime-kuma | Admin account must be created via the website | +| windmill | Need to be changed (default: E-Mail: "admin@windmill.dev", Password: "changeme") | +| *wordpress | Admin account must be created via the website | + +**Additional Notes:** +- Add typebot subdomains: botlab & bots +- **Restart REDIS container for activepieces after installation!** +- **UNCOMMENT MINIO SSL/HPARAM LINES IN NGINX AFTER SETUP!!** + +--- + +## Calcom Setup Post-Ansible + +1. Go to env and generate a new calendso encryption using: `openssl rand -base64 24` +2. Add `NEXT_PUBLIC_API_V2_URL=https://bookings.{domain}` + +--- + +## Docker-Compose and Nginx Config Location + +- Docker compose: `/opt/letsbe/stacks/AppName` +- Nginx: `/etc/nginx/sites-available` + +--- + +## E-Mails DNS Settings Example + +``` +A, mail.mydomain.com +MX, Name: @ Value: mail.mydomain.com (Priority: 10) +TXT, Name: _dmarc.mydomain.com Value: v=DMARC1; p=none; rua=mailto:administrator@mydomain.com +TXT, Name: @ Value: v=spf1 mx ~all +TXT = DKIM Name & Value: (generate for root domain in poste) +``` + +**IF DNS MANAGEMENT IS WIX:** +``` +TXT, Name: domain.com Value: v=spf1 mx a:mail.qluxurymedia.com a:support.qluxurymedia.com ~all +``` + +**Email Ports:** +- IMAP without SSL (unencrypted): 143 +- IMAP with SSL (encrypted): 993 +- SSL/TLS Outgoing: 465 + +> **IMPORTANT**: You MUST request another certificate in the TLS settings for Poste in order for IMAP to work! + +--- + +## For Containers + +- `127.0.0.1` = local +- `0.0.0.0` = network wide + +--- + +## Clients with Existing Email Inboxes + +1. Run setup of server, ensure client inserts mail.domain.com A level record first (Only setup A level until after sync, then when you're ready to launch you do the rest), and that the SSL certificate is pulled and valid +2. Setup Poste mailserver +3. Ask client to change password on original mail server account (for their protection) +4. IMAP sync (Use internal container IP if hostnames are the same for the destination): + ```bash + imapsync --host1 oldmailserver.com --user1 olduser --password1 oldpassword \ + --host2 newmailserver.com --user2 newuser --password2 newpassword + ``` +5. Once synced, change existing DNS records and create new ones and delete existing conflicting ones - MX records are vital here, do NOT change them until the new server is ready for use + +--- + +## Collabora URL Example for Nextcloud + +``` +https://admin:password@collabora.mydomain.com +``` + +--- + +## Tool-Specific Setup + +### Whiteboard + +> DO NOT install whiteboard before setting it up properly + +If mimetypealiases not found: +```bash +docker exec -u 33 -it customer-nextcloud-app php /var/www/html/occ maintenance:mimetype:update-db +docker exec -u 33 -it customer-nextcloud-app php /var/www/html/occ maintenance:mimetype:update-js +``` + +**Setup Steps:** +1. Setup docker-compose and nginx if you haven't already made them standard +2. Generate JWT token: `openssl rand -base64 32` +3. Set that token for nextcloud and in the docker compose for the whiteboard: + ```bash + docker exec -u 33 -it {CUSTOMER}-nextcloud-app php /var/www/html/occ config:app:set whiteboard jwt_secret_key --value="your-random-key" + ``` +4. Make NGINX config and link with: + ```bash + sudo ln -s /etc/nginx/sites-available/whiteboard.conf /etc/nginx/sites-enabled/ + ``` +5. Generate SSL +6. Restart nginx and reload docker compose + +**Known Issues:** +If the integration_whiteboard app was previously installed there might be a leftover non-standard mimetype configured. In this case opening the whiteboard may fail and a file is downloaded instead. Make sure to remove any entry in config/mimetypealiases.json mentioning whiteboard and run: +```bash +docker exec -u 33 -it customer-nextcloud-app php /var/www/html/occ maintenance:mimetype:update-js -vvv +docker exec -u 33 -it customer-nextcloud-app php /var/www/html/occ maintenance:mimetype:update-db -vvv +``` + +--- + +### Finding Certificates with Certbot + +```bash +sudo certbot certificates +``` + +**Example (portnimara):** +```bash +sudo certbot --nginx --expand -d portnimara.com -d www.portnimara.com \ +-d analytics.portnimara.com \ +-d automation.portnimara.com \ +-d cloud.portnimara.com \ +-d code.portnimara.com \ +-d collabora.portnimara.com \ +-d contenthub.portnimara.com \ +-d crm.portnimara.com \ +-d database.portnimara.com \ +-d debug.portnimara.com \ +-d design.portnimara.com \ +-d flows.portnimara.com \ +-d helpdesk.portnimara.com \ +-d html.portnimara.com \ +-d listmail.portnimara.com \ +-d mail.portnimara.com \ +-d marketing.portnimara.com \ +-d schedule.portnimara.com \ +-d support.portnimara.com \ +-d uptime.portnimara.com \ +-d whiteboard.portnimara.com +``` + +> **IMPORTANT**: DO NOT REQUEST CERTIFICATES BEFORE DOING THE LN COMMAND TO CONNECT SITES-AVAILABLE AND SITES-ENABLED + +--- + +### Upgrade Nextcloud over Docker Exec + +Find docker-compose files: +```bash +sudo find / -name "docker-compose.yml" +``` + +Docker composes are found in `/opt/letsbe/stacks/AppName` + +```bash +sudo docker exec --user www-data exampleCustomer-nextcloud-app php occ upgrade +docker exec -u www-data -it exampleCustomer-nextcloud-app php occ db:add-missing-indices +# (CHANGE IMAGE IN DOCKER COMPOSE FILE) +docker-compose pull && docker-compose up -d +``` + +**Best Practice:** +```bash +sudo docker compose down && sudo docker compose pull && sudo docker compose up -d && sudo docker compose logs -f {customer}-nextcloud-app +``` + +If stuck in maintenance mode: +```bash +sudo docker exec --user www-data {customer}-nextcloud-app php occ maintenance:mode --off +``` + +Remove old images: +```bash +docker image prune -f +``` + +THEN - do the docker exec commands listed above (occ upgrade and add missing indices) + +### Nextcloud Webhook Listeners +```bash +occ app:enable webhook_listeners +``` + +--- + +### Setting Up WordPress Site on Root Domain + +Go to sites-available in the `/etc/nginx/sites-available` directory and edit the wordpress.conf: +```bash +sudo nano wordpress.conf +``` + +Add new server block to point to root domain: + +```nginx +# Redirect HTTP to HTTPS for pitzone.io and www.pitzone.io +server { + listen 80; + server_name {domain}.{suffix} www.{domain}.{suffix}; + + # Redirect all HTTP requests to HTTPS + return 301 https://$host$request_uri; +} + +# HTTPS configuration for pitzone.io with Proxy to Dockerized WordPress +server { + listen 443 ssl http2; + server_name {domain} www.{domain}; + + # SSL certificate paths from Let's Encrypt + ssl_certificate /etc/letsencrypt/live/{domain}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{domain}/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Proxy requests to the Dockerized WordPress ({customer-wordpress} container) + location / { + proxy_pass http://{container_ip}:80; # Proxy to the Docker container's internal web server + # TO FIND: sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' {customer}-wordpress + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Deny access to .htaccess files, if any + location ~ /\.ht { + deny all; + } + + # ACME challenge for SSL certificate renewal + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} +``` + +Then request certificates with certbot: +```bash +sudo certbot --nginx -d {domain} -d www.{domain} +``` + +**Nginx Errors:** +```bash +sudo tail -f /var/log/nginx/error.log +``` + +**Check for auto renewal of certificates:** +```bash +sudo certbot renew --dry-run +``` + +--- + +### Baserow Setup + +1. Go back into docker-compose after initial setup and mailserver setup +2. Update email settings to enable automated emails + +Example environment configuration: +```yaml +environment: + - BASEROW_PUBLIC_URL=https://database.qluxurymedia.com + - DATABASE_URL=postgresql://bzcdowdmi7:xKaDO81KzhRu7yZ4KWjO@baserow-db:5432/baserow + - EMAIL_SMTP=True + - EMAIL_SMTP_USE_TLS=True + - EMAIL_SMTP_HOST=mail.qluxurymedia.com + - EMAIL_SMTP_PORT=587 + - FROM_EMAIL=system@qluxurymedia.com + - EMAIL_SMTP_USER=system@qluxurymedia.com + - EMAIL_SMTP_PASSWORD= +``` + +--- + +### Chatwoot Setup + +> For all passwords, use only letters and numbers here + +1. Setup the mail server and create a "support@[domain]" email address, also ensure minio is an A record +2. Change the .env file to include the password for the support email (you have to set this up when you setup the mail address). The .env file can be found in `/opt/letsbe/stacks/chatwoot` +3. Change the .env file to allow users to setup accounts (set to true), set mailer_inbound_email_domain to top level domain (letsbe.biz, for example) +4. Enter the container using: + ```bash + docker exec -it [customername]-chatwoot-rails sh + ``` +5. Run each of these: + ```bash + bundle exec rails db:setup + bundle exec rails db:migrate + ``` +6. Restart container (MUST BE DONE IN THE DIRECTORY OF THE DOCKER COMPOSE FILE): + ```bash + docker-compose down && docker-compose up -d + ``` +7. Install and configure getmail6 and MinIO for S3 storage + +#### Updating Chatwoot +```bash +docker compose down && docker compose pull && docker image prune -f && docker compose up -d +docker compose run --rm rails bundle exec rails db:chatwoot_prepare +``` + +--- + +### MinIO + +1. Edit docker compose for chatwoot to include MinIO (right above "networks" at the bottom), generate MinIO Root User & Root Password: + +```yaml +minio: + image: quay.io/minio/minio:RELEASE.2024-10-29T16-01-48Z + container_name: {client}-minio + restart: always + volumes: + - {client}-minio-data:/data + environment: + - MINIO_ROOT_USER={minioadminUsername} + - MINIO_ROOT_PASSWORD={minioAdminPassword} + command: server /data --console-address ":9001" + ports: + - "127.0.0.1:9000:9000" + - "127.0.0.1:9001:9001" + networks: + matt-chatwoot: + ipv4_address: 172.20.1.6 + +createbuckets: + image: minio/mc + depends_on: + - minio + entrypoint: > + /bin/sh -c " + sleep 10; + mc alias set minio http://minio:9000 {MinIO Admin User} {MinIO Admin Password}; + mc mb minio/typebot; + mc anonymous set public minio/typebot/public; + echo '{ \"Version\": \"2012-10-17\", \"Statement\": [{ \"Sid\": \"PublicRead\", \"Effect\": \"Allow\", \"Principal\": \"*\", \"Action\": [\"s3:GetBucketLocation\", \"s3:ListBucket\", \"s3:GetObject\"], \"Resource\": [\"arn:aws:s3:::typebot\", \"arn:aws:s3:::typebot/*\"], \"Condition\": {} }] }' > /tmp/policy.json + mc admin policy add minio public-read /tmp/policy.json; + mc admin policy set minio public-read user=minioadmin; + echo '{ \"CORSRules\": [{ \"AllowedHeaders\": [\"*\"], \"AllowedMethods\": [\"PUT\", \"POST\", \"GET\"], \"AllowedOrigins\": [\"*\"], \"ExposeHeaders\": [\"ETag\"] }] }' > /tmp/cors.json; + mc cors set minio/typebot /tmp/cors.json; + exit 0; + " + networks: + {chatwoot network name}: +``` + +And add to "Volumes" at the bottom: +```yaml +{client}-minio-data: +``` + +2. Edit .env file: +```bash +ENABLE_ACCOUNT_SIGNUP=true +SMTP_PASSWORD= +SMTP_PORT=587 +MAILER_INBOUND_EMAIL_DOMAIN=[customer_domain] # letsbe.solutions +RAILS_INBOUND_EMAIL_PASSWORD=[SET] # set this password, you'll need it for getmail6 + +# Storage +ACTIVE_STORAGE_SERVICE=s3_compatible +STORAGE_BUCKET_NAME=chatwoot +STORAGE_ACCESS_KEY_ID=[minioRootUser] +STORAGE_SECRET_ACCESS_KEY=[minioRootPassword] +STORAGE_REGION=eu-central +STORAGE_ENDPOINT=https://minio.[domain.com] +STORAGE_FORCE_PATH_STYLE=true + +# Amazon S3 +S3_BUCKET_NAME=chatwoot +AWS_ACCESS_KEY_ID=[minioRootUser] +AWS_SECRET_ACCESS_KEY=[minioRootPassword] +AWS_REGION=eu-central +AWS_ENDPOINT=https://minio.[domain.com] +AWS_FORCE_PATH_STYLE=true +``` + +3. Go to `/etc/nginx/sites-available` and make a new minio.conf file (see Nginx Configuration Examples below) + +4. Make s3.conf for the s3 subdomain (see Nginx Configuration Examples below) + +5. **DO THIS AFTER YOU REQUEST THE CERTS:** + ```bash + sudo ln -s /etc/nginx/sites-available/minio.conf /etc/nginx/sites-enabled/ + sudo ln -s /etc/nginx/sites-available/s3.conf /etc/nginx/sites-enabled/ + ``` + +6. Request certificate, restart nginx: + ```bash + certbot certificates + sudo certbot --expand -d analytics.qluxurymedia.com,automation.qluxurymedia.com,...,minio.qluxurymedia.com,s3.qluxurymedia.com + ``` + +7. Chatwoot .env changes: + - Set `RAILS_INBOUND_EMAIL_SERVICE=relay` + - Add MinIO config + - Set `RAILS_INBOUND_EMAIL_PASSWORD` to something secure (to use in the configuration for getmail6) + - Change storage from "local" - add the MINIO configs + +**Example .env:** +```bash +MAILER_SENDER_EMAIL=LetsBe +SMTP_DOMAIN=mail.letsbe.solutions +SMTP_ADDRESS=mail.letsbe.solutions +SMTP_PORT=465 +SMTP_USERNAME=support@letsbe.solutions +SMTP_PASSWORD=3eA0GmDttFISUJ9qA0xb94YQz4N9tHObvDq5oBeX7BHg3OK42 +SMTP_AUTHENTICATION=login +SMTP_ENABLE_STARTTLS_AUTO=true +SMTP_OPENSSL_VERIFY_MODE=none +SMTP_TLS=true +SMTP_SSL= + +MAILER_INBOUND_EMAIL_DOMAIN=letsbe.solutions +RAILS_INBOUND_EMAIL_SERVICE=relay +RAILS_INBOUND_EMAIL_PASSWORD=hDZn9KAHlGFDxeyh1z3o67H3971oh4yfmWDTJR9DrTn9dozwu + +ACTIVE_STORAGE_SERVICE=s3_compatible +STORAGE_BUCKET_NAME=chatwoot +STORAGE_ACCESS_KEY_ID=minioadmin +STORAGE_SECRET_ACCESS_KEY=MfHt6x7H9rbfORhkfy9O51OXjFxsC4QHJcobKbs14CMvXJ820 +STORAGE_REGION=eu-central +STORAGE_ENDPOINT=https://s3.[domain].com +STORAGE_FORCE_PATH_STYLE=true +``` + +8. Access MinIO Interface and use the root user and password to login + +9. Create a bucket called "chatwoot" + +10. Check .env variables in the customer-chatwoot-rails container (OPTIONAL): + ```bash + docker exec -it {customer}-chatwoot-rails sh + env | grep -E 'AWS|ACTIVE_STORAGE|STORAGE' + exit + ``` + +--- + +### Setting up Getmail6 + +1. Install getmail6 on the server: + ```bash + sudo apt-get update + sudo apt-get install -y python3-pip + cd / + git clone https://github.com/getmail6/getmail6.git + cd getmail6 + sudo python3 setup.py install + getmail --version + ``` + +2. Create a dedicated getmailuser user, and SWITCH TO THAT USER: + ```bash + sudo adduser getmailuser + sudo su - getmailuser + ``` + +3. Create necessary directories: + ```bash + mkdir -p ~/.getmail + mkdir -p ~/bin + mkdir -p ~/logs + ``` + +4. Make sure your RAILS_INBOUND_EMAIL_PASSWORD (Ingress Password) set in the chatwoot .env file is ready + +5. Create the import email script (1 per account, ingress password remains the same): + ```bash + nano ~/bin/import_mail_to_chatwoot.sh + ``` + + Content: + ```bash + #!/bin/bash + INGRESS_PASSWORD="{insert RAILS_INBOUND_EMAIL_PASSWORD here}" + URL='http://0.0.0.0:3011/rails/action_mailbox/relay/inbound_emails' + + curl -sS -u "actionmailbox:{insert RAILS_INBOUND_EMAIL_PASSWORD here}" \ + -A "Action Mailbox: curl relayer" \ + -H "Content-Type: message/rfc822" \ + --data-binary @- \ + $URL + ``` + +6. Make the script executable: + ```bash + chmod +x ~/bin/import_mail_to_chatwoot.sh + ``` + +7. Create the getmailrc config file (1 per email channel): + ```bash + nano ~/.getmail/getmailrc + ``` + + Content: + ```ini + [retriever1] + type = MultidropIMAPSSLRetriever + server = mail.letsbe.solutions + username = support@letsbe.solutions + password = 3eA0GmDttFISUJ9qA0xb94YQz4N9tHObvDq5oBeX7BHg3OK42 + mailboxes = ("INBOX",) + envelope_recipient = delivered-to:1 + + [retriever2] # Add if you have more mailboxes + type = MultidropIMAPSSLRetriever + server = mail.{domain} + username = help@{domain} + password = {Password} + mailboxes = ("INBOX",) + envelope_recipient = delivered-to:1 + + [destination] + type = MDA_external + path = /home/getmailuser/bin/import_mail_to_chatwoot.sh + + [options] + verbose = 1 + allow_root_commands = true + read_all = false + delete = false + delivered_to = false + received = false + message_log = ~/.getmail/getmail.log + message_log_syslog = false + message_log_verbose = true + ``` + +8. Make config executable: + ```bash + chmod +x ~/.getmail/getmailrc + ``` + +9. Test Getmail manually: + ```bash + getmail --rcfile ~/.getmail/getmailrc --verbose + ``` + +10. Setup cron job: + ```bash + crontab -e + ``` + Add at the bottom: + ``` + */1 * * * * /usr/local/bin/getmail --rcfile /home/getmailuser/.getmail/* --quiet + ``` + +11. Use "exit" to exit the getmailuser + +12. Setup IMAP settings in Chatwoot App (in channel settings) + +13. Do not forward emails to the address provided by Chatwoot in the channel setup + +--- + +### Nextcloud Context Chat + +See: https://docs.nextcloud.com/server/latest/admin_manual/ai/app_context_chat.html#ai-app-context-chat + +--- + +### Apps Only for Tech Savvy People + +1. gitea +2. Squidex +3. Windmill + +--- + +### Typebot Installation + +> Make sure to enact relevant changes in Nginx/Docker compose for s3 and Minio respectively + +1. Create directory: + ```bash + mkdir {client-typebot} in /opt/letsbe/stacks/ + ``` + +2. Create docker-compose.yml: + ```yaml + version: '3.3' + + volumes: + {client}-typebot-db-data: + + services: + {client}-typebot-db: + image: postgres:16 + restart: always + volumes: + - {client}-typebot-db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=typebot + - POSTGRES_PASSWORD=typebot + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + networks: + {chatwoot_network_name}: + ipv4_address: 172.20.1.7 + + typebot-builder: + image: baptistearno/typebot-builder:latest + restart: always + depends_on: + {client}-typebot-db: + condition: service_healthy + ports: + - '9080:3000' + extra_hosts: + - 'host.docker.internal:host-gateway' + env_file: /opt/letsbe/env/typebot.env + networks: + {chatwoot_network_name}: + ipv4_address: 172.20.1.8 + + typebot-viewer: + image: baptistearno/typebot-viewer:latest + depends_on: + {client}-typebot-db: + condition: service_healthy + restart: always + ports: + - '9081:3000' + env_file: /opt/letsbe/env/typebot.env + networks: + {chatwoot_network_name}: + ipv4_address: 172.20.1.9 + + networks: + {chatwoot_network_name}: + external: true + ``` + +3. Create typebot.env in `/opt/letsbe/env/`: + + Generate 64-bit key: + ```bash + openssl rand -base64 24 | tr -d '\n' ; echo + ``` + + Content: + ```bash + ENCRYPTION_SECRET={key} + DATABASE_URL=postgresql://postgres:typebot@typebot-db:5432/typebot + NODE_OPTIONS=--no-node-snapshot + NEXTAUTH_URL=https://botlab.letsbe.solutions + NEXT_PUBLIC_VIEWER_URL=https://bots.letsbe.solutions + DEFAULT_WORKSPACE_PLAN=UNLIMITED + ADMIN_EMAIL=administrator@letsbe.solutions + + # SMTP Configuration + SMTP_USERNAME=noreply@letsbe.biz + SMTP_PASSWORD=NF5joaQH2Q + SMTP_HOST=mail.letsbe.solutions + SMTP_PORT=465 + SMTP_SECURE=true + NEXT_PUBLIC_SMTP_FROM="Typebot Notifications " + SMTP_AUTH_DISABLED=false + + # S3 Configuration for MinIO + S3_ACCESS_KEY=minioadmin + S3_SECRET_KEY=A7wRC52q5BofIjnQFdOaM7aBScS6H7jY54u0aax4P1KCsceP5 + S3_BUCKET=typebot + S3_PORT= + S3_ENDPOINT=s3.letsbe.solutions + S3_SSL=true + S3_REGION=eu-central + S3_PUBLIC_CUSTOM_DOMAIN=https://s3.letsbe.solutions + ``` + +4. Create nginx configs for botlab and bots (see Nginx Configuration Examples below) + +5. Requesting the certs: + ```bash + # Link sites-available to sites-enabled first + sudo certbot --nginx -d botlab.{client}.{domain} -d bots.{client}.{domain} + sudo nginx -t + sudo systemctl reload nginx + ``` + +--- + +## Nginx Configuration Examples + +### Whiteboard Config + +```nginx +server { + if ($host = whiteboard.letsbe.solutions) { + return 301 https://$host$request_uri; + } + + client_max_body_size 64M; + listen 80; + server_name whiteboard.letsbe.solutions; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + listen 443 ssl http2; + server_name whiteboard.letsbe.solutions; + + ssl_certificate /etc/letsencrypt/live/whiteboard.letsbe.solutions/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/whiteboard.letsbe.solutions/privkey.pem; + + location / { + proxy_pass http://0.0.0.0:4014; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + add_header X-Frontend-Host $host; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} +``` + +### MinIO Config + +```nginx +server { + if ($host = minio.letsbe.solutions) { + return 301 https://$host$request_uri; + } + + client_max_body_size 64M; + listen 80; + server_name minio.letsbe.solutions; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + listen 443 ssl http2; + server_name minio.letsbe.solutions; + + location / { + proxy_pass http://0.0.0.0:9001; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded_Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + ssl_certificate /etc/letsencrypt/live/support.letsbe.solutions/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/support.letsbe.solutions/privkey.pem; +} +``` + +### S3 Config + +```nginx +server { + if ($host = s3.qluxurymedia.com) { + return 301 https://$host$request_uri; + } + + client_max_body_size 0; + listen 80; + server_name s3.qluxurymedia.com; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 0; + listen 443 ssl http2; + server_name s3.qluxurymedia.com; + + ssl_certificate /etc/letsencrypt/live/analytics.qluxurymedia.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/analytics.qluxurymedia.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:9000; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_hide_header Access-Control-Allow-Origin; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' '*' always; + add_header 'Access-Control-Expose-Headers' 'Origin, Content-Type, Content-MD5, Content-Disposition, ETag' always; + + if ($request_method = 'OPTIONS') { + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + return 204; + } + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} +``` + +### Typebot Botlab Config + +```nginx +server { + if ($host = botlab.{client}.{domain}) { + return 301 https://$host$request_uri; + } + + client_max_body_size 64M; + listen 80; + server_name botlab.{client}.{domain}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + large_client_header_buffers 4 16k; + listen 443 ssl http2; + server_name botlab.{client}.{domain}; + + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://172.20.1.8:3000; + proxy_http_version 1.1; + proxy_cache_bypass $http_upgrade; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +### Typebot Bots Config + +```nginx +server { + if ($host = bots.{client}.{domain}) { + return 301 https://$host$request_uri; + } + + client_max_body_size 64M; + listen 80; + server_name bots.{client}.{domain}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + large_client_header_buffers 4 16k; + listen 443 ssl http2; + server_name bots.{client}.{domain}; + + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://172.20.1.9:3000; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +--- + +## Nginx Tips + +Always do the link of the nginx files before requesting certs, but comment out any ssl locations. + +```bash +# Link sites-available to sites-enabled +sudo ln -s /etc/nginx/sites-available/{config}.conf /etc/nginx/sites-enabled/ + +# Test nginx configuration +sudo nginx -t + +# Reload nginx +sudo systemctl reload nginx +``` + +--- + +## ToDo + +- Fix backups.sh diff --git a/script/README.md b/script/README.md new file mode 100644 index 0000000..3412d74 --- /dev/null +++ b/script/README.md @@ -0,0 +1,129 @@ +# LetsBe Infrastructure Deployment Scripts + +Automated deployment scripts for LetsBe cloud infrastructure. Designed for use with the LetsBe Cloud Orchestrator and SysAdmin Agent. + +## Quick Start + +### Prerequisites +- SSH key `id_ed25519` in the same directory +- Target server with root access (initial deployment) or stefan user access (subsequent operations) +- DNS A records configured for all required subdomains + +### Automated Deployment + +**Using JSON config file:** +```bash +./start.sh --config config.json --action all +``` + +**Using CLI arguments:** +```bash +./start.sh \ + --host 192.168.1.100 \ + --port 22 \ + --password "root_password" \ + --customer acme \ + --domain acme.com \ + --company "Acme Corp" \ + --tools "portainer,n8n,baserow" \ + --action all +``` + +### Available Actions + +| Action | Description | +|--------|-------------| +| `upload` | Upload scripts and configs to server | +| `env` | Run environment variable setup | +| `setup` | Run server setup (packages, Docker, nginx, SSL) | +| `all` | Run complete deployment (upload + env + setup) | + +### Script Arguments + +**start.sh:** +| Argument | Description | +|----------|-------------| +| `--host` | Server IP address | +| `--port` | SSH port (default: 22) | +| `--password` | SSH password (for root initial setup) | +| `--key` | Path to SSH private key (for stefan access) | +| `--customer` | Customer identifier (lowercase, no spaces) | +| `--domain` | Primary domain | +| `--company` | Company display name | +| `--tools` | Comma-separated tool list or "all" | +| `--skip-ssl` | Skip SSL certificate generation | +| `--config` | Path to JSON config file | +| `--json` | Inline JSON configuration | +| `--action` | Action to perform: upload, env, setup, all | + +**setup.sh:** +| Argument | Description | +|----------|-------------| +| `--tools` | Comma-separated list of tools to deploy, or "all" | +| `--skip-ssl` | Skip SSL certificate generation | + +## Directory Structure (Server) + +``` +/opt/letsbe/ + env/ # Centralized .env files: .env + stacks/ # Docker compose files per tool + nginx/ # Nginx config templates + scripts/ # Maintenance scripts (backups.sh) + config/ # rclone and other configs +``` + +## Config File Format + +Create `config.json`: +```json +{ + "host": "192.168.1.100", + "port": 22, + "password": "initial_root_password", + "customer": "acme", + "domain": "acme.com", + "company_name": "Acme Corp", + "tools": ["portainer", "n8n", "baserow", "chatwoot"], + "skip_ssl": false +} +``` + +See `config.sample.json` for a complete template with all available tools. + +## Available Tools + +- activepieces, baserow, calcom, chatwoot, diun-watchtower +- documenso, ghost, gitea, gitea-drone, glitchtip, html +- keycloak, librechat, listmonk, minio, n8n, nextcloud +- nocodb, odoo, penpot, portainer, poste, redash +- squidex, stirlingpdf, typebot, umami, uptime-kuma +- windmill, wordpress + +## Required DNS Records + +Before deployment, create A records pointing to your server IP: +- Root domain and www +- Tool-specific subdomains (see `MANUAL_SETUP.md` for complete list) + +## Post-Installation + +After automated deployment, some tools require initial admin account setup. +See `MANUAL_SETUP.md` for: +- Admin account creation per tool +- Email server (Poste) configuration +- MinIO S3 storage setup +- Getmail6 configuration for Chatwoot +- SSL certificate management +- Nginx configuration examples + +## Security Notes + +- Root SSH login is disabled after initial setup +- SSH access via `stefan` user with key-based authentication only +- SSH port: 22022 +- Configure B2/rclone credentials separately for backups + +## Manual Setup Guide + +For detailed manual configuration instructions, tool-specific setup, and troubleshooting, see [MANUAL_SETUP.md](MANUAL_SETUP.md). diff --git a/script/backups.sh b/script/backups.sh new file mode 100644 index 0000000..99b28a3 --- /dev/null +++ b/script/backups.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# ============================================================================= +# LetsBe Backup Script +# ============================================================================= +# Backs up Docker volumes and databases to configured rclone remote +# Configure rclone first: rclone config +# ============================================================================= + +set -euo pipefail + +BACKUP_DIR="/tmp/letsbe-backups" +DATE=$(date +%Y%m%d_%H%M%S) +RCLONE_REMOTE="remote" # Configure this in rclone config + +# Create backup directory +mkdir -p "$BACKUP_DIR" + +echo "=== LetsBe Backup - $DATE ===" + +# Function to backup a postgres database +backup_postgres() { + local container=$1 + local db_name=$2 + local db_user=${3:-postgres} + + if docker ps --format '{{.Names}}' | grep -q "^${container}$"; then + echo "Backing up PostgreSQL: $container ($db_name)..." + docker exec "$container" pg_dump -U "$db_user" "$db_name" | gzip > "$BACKUP_DIR/${container}_${DATE}.sql.gz" + fi +} + +# Function to backup a mysql database +backup_mysql() { + local container=$1 + local db_name=$2 + local db_user=${3:-root} + local db_pass=$4 + + if docker ps --format '{{.Names}}' | grep -q "^${container}$"; then + echo "Backing up MySQL: $container ($db_name)..." + docker exec "$container" mysqldump -u"$db_user" -p"$db_pass" "$db_name" | gzip > "$BACKUP_DIR/${container}_${DATE}.sql.gz" + fi +} + +# Function to backup docker volumes +backup_volume() { + local volume=$1 + + if docker volume ls --format '{{.Name}}' | grep -q "^${volume}$"; then + echo "Backing up volume: $volume..." + docker run --rm -v "${volume}:/data" -v "$BACKUP_DIR:/backup" alpine tar czf "/backup/${volume}_${DATE}.tar.gz" -C /data . + fi +} + +# ============================================================================= +# BACKUP DATABASES +# ============================================================================= + +# Chatwoot +backup_postgres "*-chatwoot-postgres" "chatwoot_production" "chatwoot" 2>/dev/null || true + +# NoCoDB +backup_postgres "*-nocodb-postgres" "nocodb" "postgres" 2>/dev/null || true + +# Baserow +backup_postgres "*-baserow-db" "baserow" "baserow" 2>/dev/null || true + +# n8n +backup_postgres "*-n8n-postgres" "n8n" "n8n" 2>/dev/null || true + +# Nextcloud +backup_postgres "*-nextcloud-postgres" "nextcloud" "nextcloud" 2>/dev/null || true + +# Typebot +backup_postgres "*-typebot-db" "typebot" "postgres" 2>/dev/null || true + +# ============================================================================= +# BACKUP VOLUMES (important data) +# ============================================================================= + +# Get all letsbe-related volumes +for vol in $(docker volume ls --format '{{.Name}}' | grep -E '(storage|data|uploads)'); do + backup_volume "$vol" 2>/dev/null || true +done + +# ============================================================================= +# UPLOAD TO REMOTE +# ============================================================================= + +if command -v rclone &> /dev/null; then + if rclone listremotes | grep -q "^${RCLONE_REMOTE}:"; then + echo "Uploading backups to $RCLONE_REMOTE..." + rclone copy "$BACKUP_DIR" "${RCLONE_REMOTE}:letsbe-backups/${DATE}/" --progress + echo "Upload complete." + else + echo "WARNING: rclone remote '$RCLONE_REMOTE' not configured. Backups stored locally only." + fi +else + echo "WARNING: rclone not installed. Backups stored locally only." +fi + +# ============================================================================= +# CLEANUP OLD LOCAL BACKUPS (keep last 7 days) +# ============================================================================= + +find "$BACKUP_DIR" -type f -mtime +7 -delete 2>/dev/null || true + +echo "=== Backup Complete ===" +echo "Local backups: $BACKUP_DIR" diff --git a/script/config.sample.json b/script/config.sample.json new file mode 100644 index 0000000..c66aac9 --- /dev/null +++ b/script/config.sample.json @@ -0,0 +1,26 @@ +{ + "host": "192.168.1.100", + "port": 22, + "password": "initial_root_password_for_first_setup", + "customer": "acme", + "domain": "acme.com", + "company_name": "Acme Corporation", + "tools": [ + "portainer", + "n8n", + "baserow", + "chatwoot", + "nextcloud", + "uptime-kuma" + ], + "skip_ssl": false, + "action": "all", + + "_comment_tools": "Available tools: activepieces, baserow, calcom, chatwoot, diun-watchtower, documenso, ghost, gitea, gitea-drone, glitchtip, html, keycloak, librechat, listmonk, minio, n8n, nextcloud, nocodb, odoo, penpot, portainer, poste, redash, squidex, stirlingpdf, typebot, umami, uptime-kuma, windmill, wordpress. Use 'all' to deploy everything.", + + "_comment_action": "Available actions: upload (upload files only), env (run env_setup.sh), setup (run setup.sh), all (complete deployment)", + + "_comment_skip_ssl": "Set to true to skip SSL certificate generation during setup", + + "_comment_password_vs_key": "Use 'password' for initial root setup, or use 'key' with path to SSH private key for subsequent access via stefan user" +} diff --git a/script/env_setup - Copy.shZone.Identifier b/script/env_setup - Copy.shZone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/script/env_setup.sh b/script/env_setup.sh new file mode 100644 index 0000000..e85244d --- /dev/null +++ b/script/env_setup.sh @@ -0,0 +1,500 @@ +#!/bin/bash +# +# LetsBe Cloud Environment Setup Script +# Non-interactive version for Orchestrator/SysAdmin Agent integration +# +# Usage: +# ./env_setup.sh --customer "acme" --domain "acme.com" --company "Acme Corp" +# ./env_setup.sh --json '{"customer":"acme","domain":"acme.com","company_name":"Acme Corp"}' +# ./env_setup.sh --config /path/to/config.json +# + +set -euo pipefail + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +LETSBE_BASE="/opt/letsbe" +STACKS_DIR="${LETSBE_BASE}/stacks" +NGINX_DIR="${LETSBE_BASE}/nginx" +ENV_DIR="${LETSBE_BASE}/env" +SCRIPTS_DIR="${LETSBE_BASE}/scripts" + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +usage() { + cat <&2 +} + +die() { + log_error "$*" + exit 1 +} + +# Generate random string of specified length +generate_random_string() { + local length=$1 + tr -dc A-Za-z0-9 /dev/null; then + die "jq is required for JSON parsing. Install with: apt-get install jq" + fi + + customer=$(echo "${json_input}" | jq -r '.customer // empty') + domain=$(echo "${json_input}" | jq -r '.domain // empty') + company_name=$(echo "${json_input}" | jq -r '.company_name // empty') +} + +# ============================================================================ +# ARGUMENT PARSING +# ============================================================================ + +customer="" +domain="" +company_name="" + +while [[ $# -gt 0 ]]; do + case $1 in + --customer) + customer="$2" + shift 2 + ;; + --domain) + domain="$2" + shift 2 + ;; + --company) + company_name="$2" + shift 2 + ;; + --json) + parse_json "$2" + shift 2 + ;; + --config) + if [[ ! -f "$2" ]]; then + die "Config file not found: $2" + fi + parse_json "$(cat "$2")" + shift 2 + ;; + --help|-h) + usage + ;; + *) + log_error "Unknown option: $1" + usage + ;; + esac +done + +# ============================================================================ +# VALIDATION +# ============================================================================ + +validate_required "customer" "${customer}" +validate_required "domain" "${domain}" +validate_required "company_name" "${company_name}" + +# Validate customer format (lowercase, no spaces/hyphens/numbers) +if [[ ! "${customer}" =~ ^[a-z]+$ ]]; then + die "Customer name must be lowercase letters only, no spaces/hyphens/numbers: ${customer}" +fi + +# Validate domain format +if [[ ! "${domain}" =~ ^[a-z0-9.-]+\.[a-z]{2,}$ ]]; then + die "Invalid domain format: ${domain}" +fi + +log_info "Configuration validated" +log_info " Customer: ${customer}" +log_info " Domain: ${domain}" +log_info " Company: ${company_name}" + +# ============================================================================ +# DERIVED VARIABLES +# ============================================================================ + +# Email for Let's Encrypt +letsencrypt_email="postmaster@${domain}" + +# Subdomains per tool +domain_html="html.${domain}" +domain_wordpress="${domain}" +domain_squidex="contenthub.${domain}" +domain_chatwoot="support.${domain}" +domain_chatwoot_helpdesk="helpdesk.${domain}" +domain_gitea="code.${domain}" +domain_gitea_drone="ci.${domain}" +domain_glitchtip="debug.${domain}" +domain_listmonk="newsletters.${domain}" +domain_n8n="n8n.${domain}" +domain_nextcloud="cloud.${domain}" +domain_penpot="design.${domain}" +domain_poste="mail.${domain}" +domain_umami="analytics.${domain}" +domain_uptime_kuma="uptime.${domain}" +domain_windmill="flows.${domain}" +domain_calcom="bookings.${domain}" +domain_odoo="crm.${domain}" +domain_collabora="collabora.${domain}" +domain_whiteboard="whiteboard.${domain}" +domain_activepieces="automation.${domain}" +domain_minio="minio.${domain}" +domain_s3="s3.${domain}" +domain_librechat="ai.${domain}" +domain_bot_viewer="bots.${domain}" +domain_botlab="botlab.${domain}" +domain_nocodb="database.${domain}" +domain_redash="data.${domain}" +domain_documenso="signatures.${domain}" +domain_keycloak="auth.${domain}" +domain_pdf="pdf.${domain}" +domain_ghost="${domain}" + +# ============================================================================ +# GENERATED SECRETS +# ============================================================================ + +log_info "Generating secrets and credentials..." + +# WordPress +wordpresss_mariadb_root_password=$(generate_random_string 20) +wordpress_db_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +wordpress_db_password=$(generate_random_string 20) + +# Squidex +squidex_adminemail="postmaster@${domain}" +squidex_adminpassword=$(generate_random_string 20) + +# Listmonk +listmonk_admin_username=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +listmonk_admin_password=$(generate_random_string 20) +listmonk_db_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +listmonk_db_password=$(generate_random_string 20) + +# Gitea +gitea_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +gitea_postgres_password=$(generate_random_string 20) + +# Umami +umami_app_secret=$(generate_random_string 32) +umami_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +umami_postgres_password=$(generate_random_string 20) + +# Drone/Gitea +drone_gitea_rpc_secret=$(generate_random_string 32) + +# Windmill +windmill_database_password=$(generate_random_string 20) + +# Glitchtip +glitchtip_database_password=$(generate_random_string 20) +glitchtip_secret_key=$(generate_random_string 32) + +# Penpot +penpot_secret_key=$(generate_random_string 32) +penpot_db_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +penpot_db_password=$(generate_random_string 20) + +# Nextcloud +nextcloud_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +nextcloud_postgres_password=$(generate_random_string 20) +nextcloud_jwt_secret=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') +nextcloud_admin_password=$(generate_random_string 20) + +# Collabora +collabora_password=$(generate_random_string 20) +collabora_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') + +# Chatwoot +chatwoot_secret_key_base=$(generate_random_string 32) +chatwoot_redis_password=$(generate_random_string 20) +chatwoot_postgres_username=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +chatwoot_postgres_password=$(generate_random_string 20) +chatwoot_rails_inbound_email_password=$(generate_random_string 20) + +# N8N +n8n_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +n8n_postgres_password=$(generate_random_string 20) + +# Cal.com +calcom_nextauth_secret=$(generate_random_string 32) +calcom_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +calcom_postgres_password=$(generate_random_string 20) + +# Odoo +odoo_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +odoo_postgres_password=$(generate_random_string 20) + +# Activepieces +activepieces_api_key=$(generate_random_string 32) +activepieces_encryption_key=$(generate_random_string 32 | tr '[:upper:]' '[:lower:]') +activepieces_jwt_secret=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') +activepieces_postgres_password=$(generate_random_string 32) + +# MinIO +minio_root_user=$(generate_random_string 16) +minio_root_password=$(generate_random_string 32) + +# Typebot +typebot_encryption_secret=$(generate_random_string 32) +typebot_postgres_password=$(generate_random_string 20) + +# NocoDB +nocodb_postgres_password=$(generate_random_string 32) + +# LibreChat +librechat_postgres_password=$(generate_random_string 20) +librechat_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +librechat_jwt_secret=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') +librechat_jwt_refresh_secret=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') + +# Redash +redash_secret_key=$(generate_random_string 32) +redash_cookie_secret=$(generate_random_string 32) +redash_postgres_password=$(generate_random_string 20) +redash_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') + +# Documenso +documenso_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +documenso_postgres_password=$(generate_random_string 40) +documenso_nextauth_secret=$(generate_random_string 32) +documenso_encryption_key=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') +documenso_encryption_secondary_key=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') + +# Ghost +ghost_mysql_password=$(generate_random_string 40) +ghost_s3_access_key=$(generate_random_string 20) +ghost_s3_secret_key=$(generate_random_string 40) + +# Keycloak +keycloak_postgres_password=$(generate_random_string 40) +keycloak_admin_password=$(generate_random_string 40) +keycloak_grafana_password=$(generate_random_string 40) + +# StirlingPDF +stirlingpdf_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') +stirlingpdf_postgres_password=$(generate_random_string 40) +stirlingpdf_api_key=$(generate_random_string 40) + +# Sysadmin Agent +sysadmin_agent_token=$(generate_random_string 64) + +# ============================================================================ +# TEMPLATE REPLACEMENT +# ============================================================================ + +log_info "Replacing placeholders in template files..." + +# Process all template files +for file in "${STACKS_DIR}"/*/* "${STACKS_DIR}"/*/.* "${NGINX_DIR}"/* "${SCRIPTS_DIR}"/backups.sh; do + if [[ -f "${file}" ]]; then + # Core variables + sed -i "s/{{ customer }}/${customer}/g" "${file}" + sed -i "s/{{ domain }}/${domain}/g" "${file}" + sed -i "s/{{ company_name }}/${company_name}/g" "${file}" + sed -i "s/{{ letsencrypt_email }}/${letsencrypt_email}/g" "${file}" + + # Domain variables + sed -i "s/{{ domain_html }}/${domain_html}/g" "${file}" + sed -i "s/{{ domain_wordpress }}/${domain_wordpress}/g" "${file}" + sed -i "s/{{ domain_squidex }}/${domain_squidex}/g" "${file}" + sed -i "s/{{ domain_chatwoot }}/${domain_chatwoot}/g" "${file}" + sed -i "s/{{ domain_chatwoot_helpdesk }}/${domain_chatwoot_helpdesk}/g" "${file}" + sed -i "s/{{ domain_gitea }}/${domain_gitea}/g" "${file}" + sed -i "s/{{ domain_gitea_drone }}/${domain_gitea_drone}/g" "${file}" + sed -i "s/{{ domain_glitchtip }}/${domain_glitchtip}/g" "${file}" + sed -i "s/{{ domain_listmonk }}/${domain_listmonk}/g" "${file}" + sed -i "s/{{ domain_librechat }}/${domain_librechat}/g" "${file}" + sed -i "s/{{ domain_n8n }}/${domain_n8n}/g" "${file}" + sed -i "s/{{ domain_nextcloud }}/${domain_nextcloud}/g" "${file}" + sed -i "s/{{ domain_penpot }}/${domain_penpot}/g" "${file}" + sed -i "s/{{ domain_poste }}/${domain_poste}/g" "${file}" + sed -i "s/{{ domain_umami }}/${domain_umami}/g" "${file}" + sed -i "s/{{ domain_uptime_kuma }}/${domain_uptime_kuma}/g" "${file}" + sed -i "s/{{ domain_windmill }}/${domain_windmill}/g" "${file}" + sed -i "s/{{ domain_calcom }}/${domain_calcom}/g" "${file}" + sed -i "s/{{ domain_odoo }}/${domain_odoo}/g" "${file}" + sed -i "s/{{ domain_collabora }}/${domain_collabora}/g" "${file}" + sed -i "s/{{ domain_activepieces }}/${domain_activepieces}/g" "${file}" + sed -i "s/{{ domain_bot_viewer }}/${domain_bot_viewer}/g" "${file}" + sed -i "s/{{ domain_botlab }}/${domain_botlab}/g" "${file}" + sed -i "s/{{ domain_minio }}/${domain_minio}/g" "${file}" + sed -i "s/{{ domain_s3 }}/${domain_s3}/g" "${file}" + sed -i "s/{{ domain_nocodb }}/${domain_nocodb}/g" "${file}" + sed -i "s/{{ domain_whiteboard }}/${domain_whiteboard}/g" "${file}" + sed -i "s/{{ domain_redash }}/${domain_redash}/g" "${file}" + sed -i "s/{{ domain_documenso }}/${domain_documenso}/g" "${file}" + sed -i "s/{{ domain_keycloak }}/${domain_keycloak}/g" "${file}" + sed -i "s/{{ domain_pdf }}/${domain_pdf}/g" "${file}" + sed -i "s/{{ domain_ghost }}/${domain_ghost}/g" "${file}" + + # Credential variables + sed -i "s/{{ wordpresss_mariadb_root_password }}/${wordpresss_mariadb_root_password}/g" "${file}" + sed -i "s/{{ wordpress_db_user }}/${wordpress_db_user}/g" "${file}" + sed -i "s/{{ wordpress_db_password }}/${wordpress_db_password}/g" "${file}" + sed -i "s/{{ squidex_adminemail }}/${squidex_adminemail}/g" "${file}" + sed -i "s/{{ squidex_adminpassword }}/${squidex_adminpassword}/g" "${file}" + sed -i "s/{{ listmonk_admin_username }}/${listmonk_admin_username}/g" "${file}" + sed -i "s/{{ listmonk_admin_password }}/${listmonk_admin_password}/g" "${file}" + sed -i "s/{{ listmonk_db_user }}/${listmonk_db_user}/g" "${file}" + sed -i "s/{{ listmonk_db_password }}/${listmonk_db_password}/g" "${file}" + sed -i "s/{{ gitea_postgres_user }}/${gitea_postgres_user}/g" "${file}" + sed -i "s/{{ gitea_postgres_password }}/${gitea_postgres_password}/g" "${file}" + sed -i "s/{{ umami_app_secret }}/${umami_app_secret}/g" "${file}" + sed -i "s/{{ umami_postgres_user }}/${umami_postgres_user}/g" "${file}" + sed -i "s/{{ umami_postgres_password }}/${umami_postgres_password}/g" "${file}" + sed -i "s/{{ drone_gitea_rpc_secret }}/${drone_gitea_rpc_secret}/g" "${file}" + sed -i "s/{{ windmill_database_password }}/${windmill_database_password}/g" "${file}" + sed -i "s/{{ glitchtip_database_password }}/${glitchtip_database_password}/g" "${file}" + sed -i "s/{{ glitchtip_secret_key }}/${glitchtip_secret_key}/g" "${file}" + sed -i "s/{{ penpot_secret_key }}/${penpot_secret_key}/g" "${file}" + sed -i "s/{{ penpot_db_user }}/${penpot_db_user}/g" "${file}" + sed -i "s/{{ penpot_db_password }}/${penpot_db_password}/g" "${file}" + sed -i "s/{{ nextcloud_postgres_user }}/${nextcloud_postgres_user}/g" "${file}" + sed -i "s/{{ nextcloud_postgres_password }}/${nextcloud_postgres_password}/g" "${file}" + sed -i "s/{{ nextcloud_admin_password }}/${nextcloud_admin_password}/g" "${file}" + sed -i "s/{{ nextcloud_jwt_secret }}/${nextcloud_jwt_secret}/g" "${file}" + sed -i "s/{{ collabora_password }}/${collabora_password}/g" "${file}" + sed -i "s/{{ collabora_user }}/${collabora_user}/g" "${file}" + sed -i "s/{{ chatwoot_secret_key_base }}/${chatwoot_secret_key_base}/g" "${file}" + sed -i "s/{{ chatwoot_redis_password }}/${chatwoot_redis_password}/g" "${file}" + sed -i "s/{{ chatwoot_postgres_username }}/${chatwoot_postgres_username}/g" "${file}" + sed -i "s/{{ chatwoot_postgres_password }}/${chatwoot_postgres_password}/g" "${file}" + sed -i "s/{{ chatwoot_rails_inbound_email_password }}/${chatwoot_rails_inbound_email_password}/g" "${file}" + sed -i "s/{{ n8n_postgres_user }}/${n8n_postgres_user}/g" "${file}" + sed -i "s/{{ n8n_postgres_password }}/${n8n_postgres_password}/g" "${file}" + sed -i "s/{{ calcom_nextauth_secret }}/${calcom_nextauth_secret}/g" "${file}" + sed -i "s/{{ calcom_postgres_user }}/${calcom_postgres_user}/g" "${file}" + sed -i "s/{{ calcom_postgres_password }}/${calcom_postgres_password}/g" "${file}" + sed -i "s/{{ odoo_postgres_user }}/${odoo_postgres_user}/g" "${file}" + sed -i "s/{{ odoo_postgres_password }}/${odoo_postgres_password}/g" "${file}" + sed -i "s/{{ activepieces_api_key }}/${activepieces_api_key}/g" "${file}" + sed -i "s/{{ activepieces_encryption_key }}/${activepieces_encryption_key}/g" "${file}" + sed -i "s/{{ activepieces_jwt_secret }}/${activepieces_jwt_secret}/g" "${file}" + sed -i "s/{{ activepieces_postgres_password }}/${activepieces_postgres_password}/g" "${file}" + sed -i "s/{{ minio_root_user }}/${minio_root_user}/g" "${file}" + sed -i "s/{{ minio_root_password }}/${minio_root_password}/g" "${file}" + sed -i "s/{{ typebot_encryption_secret }}/${typebot_encryption_secret}/g" "${file}" + sed -i "s/{{ nocodb_postgres_password }}/${nocodb_postgres_password}/g" "${file}" + sed -i "s/{{ typebot_postgres_password }}/${typebot_postgres_password}/g" "${file}" + sed -i "s/{{ redash_secret_key }}/${redash_secret_key}/g" "${file}" + sed -i "s/{{ redash_cookie_secret }}/${redash_cookie_secret}/g" "${file}" + sed -i "s/{{ redash_postgres_user }}/${redash_postgres_user}/g" "${file}" + sed -i "s/{{ redash_postgres_password }}/${redash_postgres_password}/g" "${file}" + sed -i "s/{{ librechat_postgres_password }}/${librechat_postgres_password}/g" "${file}" + sed -i "s/{{ librechat_postgres_user }}/${librechat_postgres_user}/g" "${file}" + sed -i "s/{{ librechat_jwt_secret }}/${librechat_jwt_secret}/g" "${file}" + sed -i "s/{{ librechat_jwt_refresh_secret }}/${librechat_jwt_refresh_secret}/g" "${file}" + sed -i "s/{{ documenso_postgres_user }}/${documenso_postgres_user}/g" "${file}" + sed -i "s/{{ documenso_postgres_password }}/${documenso_postgres_password}/g" "${file}" + sed -i "s/{{ documenso_nextauth_secret }}/${documenso_nextauth_secret}/g" "${file}" + sed -i "s/{{ documenso_encryption_key }}/${documenso_encryption_key}/g" "${file}" + sed -i "s/{{ documenso_encryption_secondary_key }}/${documenso_encryption_secondary_key}/g" "${file}" + sed -i "s/{{ ghost_mysql_password }}/${ghost_mysql_password}/g" "${file}" + sed -i "s/{{ ghost_s3_access_key }}/${ghost_s3_access_key}/g" "${file}" + sed -i "s/{{ ghost_s3_secret_key }}/${ghost_s3_secret_key}/g" "${file}" + sed -i "s/{{ keycloak_postgres_password }}/${keycloak_postgres_password}/g" "${file}" + sed -i "s/{{ keycloak_admin_password }}/${keycloak_admin_password}/g" "${file}" + sed -i "s/{{ keycloak_grafana_password }}/${keycloak_grafana_password}/g" "${file}" + sed -i "s/{{ stirlingpdf_postgres_user }}/${stirlingpdf_postgres_user}/g" "${file}" + sed -i "s/{{ stirlingpdf_postgres_password }}/${stirlingpdf_postgres_password}/g" "${file}" + sed -i "s/{{ stirlingpdf_api_key }}/${stirlingpdf_api_key}/g" "${file}" + sed -i "s/{{ sysadmin_agent_token }}/${sysadmin_agent_token}/g" "${file}" + fi +done + +log_info "All placeholders replaced successfully." + +# ============================================================================ +# GENERATE ENV FILES +# ============================================================================ + +log_info "Generating centralized environment files..." + +mkdir -p "${ENV_DIR}" + +# Write master credentials file for reference +cat > "${ENV_DIR}/credentials.env" <e#l?v2As1+qP}nwr$&1$L@5Tbdo=P_WjS^XMgwHdryrqSFIW~ z>Zvi-eBS5fl#>JofdcsbA^-~1_{YtET~Gk90UR9l3@jYz^b8!$oQ-VFj0}t%=#-Tp z0f4Ut>CIk0my;_r03gWkU;N`D_lbt``2pe|XcU|(t&2ba0NS75q5m5+S|e-ce09l>}t);<|^P0z@JjMEtD3&l~8v!=3e2 z5ajFSRm(?$IM`hXIa}kx_WBsBWaz*RMRH|})HdfMN2dqh$r#x(O1_#ZfxVRMwT>fw-?AZ4 zolU=ofid!qaPy@`I6x12GtY&m=0O}}aJCJm`D!q@Q;}x9k#Ii{X?HtUX^6Hh!TwL1 zQQ_2+0sfD(3b!Nv3-pB=@tKc={vVwj{l)JWN)JY!ikIm|ZxIDfPOsRUk8}MPRaL5b zJlBWk%MCbZC~0yz9NBxRE+?qzIFWf~I8d}p#bL6;)r;~UFU~csQAZyqZ0+n25LWC# zAask3>yq?NYAP21(pnq%<<{_1KhT|#IiH#8@eh?(UpF0Pt-3`bgf_Iz<;Pu)SQy#U7}!|Z+Bg`|x>;HN!)A^@?dA9So6S@y z&6#h|qj%#Q%&TG>Pg^6gmoA_xDO>_KKu?fjE*!9g@;@qcauAwMLgIBg7_8ZxP+Hs>zv_!SWXY z1*?+L(Hj%O7%HrvuWN+j0hxGp@-2W*3zkzCJPUDn6NsSNo=~(_>p#PN5S~lySe4eSP{YIjVCIAFs z3~45}stySv!RM7qI)U(|^2nkYS&88N)mi{aLF}=Jg6@X2%CN`k};28Q3OBlvx%A<7i4bQ*?WFElC? z(HYK3#iTj4SSti@4(M5BKy36dk%>87_NYB~HurWy6F1gXQaCYdhU~A0~VWn|Fa?M`eE2+nSBK$g@1frBly4 zcA{y|ChTgVN%J#9SP%87OqZLGpa#rkz=@zP%m=VHq*r~qBVwY(pZufMc+B>hB|+RA z0pce^?`sE44QIty3c_}FQbbxyx2%q@NF~+eyo^7Eha&~@CFVEtGxXU)U-1giQ8nDuGLj-1~V5HDy76kG!ih_*h(I`;yfx zQJ>)x8}u_)L?CGICd#f&^#du2yC8er=F#$1!3nm&ChGNB$OX5!L2aMNT!^#3NO#do z_yu!o3unhO9(jI?ZD{Oi@hN*|-_Jh>{ht18Ou+w@I#RiaFck?qI>`NlKpaQ90_5y&q<)4wBM<-pNdFJcKZBj$ zSijfbgu6*uUUpv$#`}}#gA$wKe0tCx6%uh`830>W8I4 z3u$1%A6@B`nfB{?X5KBY{bHb$(#5~kcZ$dXJ*?#mY_9mIVH=e3EutQ%MQZBLtOm^4 z?n-%kScO=M2W~pZoqaU9J-QCj3`yCjdWqXj^zHiw<6qEjvC5AUI|t9 zhAR+DI7rwRDjt|99>q^7VoYKm8%Q(^>?$Q+^o_rGH_j3&&hQon83ENc4^y-yKRDIy z4Hh+-y4wVL5e(@z^XaOe`AFrLoCzvW!G3-(<@C31qe=#)Z=ob-E-xDUL8hQ$l|Lit z+%*;)2&)=&r80H+*|YA$>{|73;51*dK#J=)=wYF@T273QqI_>a)@v$H9lc~q3G)=h zYAflDp%REr&VjuI7wyrI??uZ2AflCqC=KLg)XY6P>|U@!s#1$-@s#PlfN-KJ+_#U6 zj2QlajQ*%);epq;4nl;qlZA{I<)FmalTYNfSO)GpUdty1dg)?;uEwke-DjD&MKyHT z4TU5H$9F8kW0L5ob`}|`kPG)*OFTTh{ZWIXVvM6w;V5W~wda7wX~Bp`nX4_=LdDSOg2Q8Z=W$8H>C#k&ukT^A6Re%E+EMr-Ej+$VP%NhJFd3A2XSlfw zfP*=fr@nC7zPgEvxBv+wCnPQB{V;p{(U|O6b~@E9$a^p$M}m``2~1@iQ=J!^BPsB; zGbYTI_|rlg%gQ5Wa~ERsu=Hg(_mds6WrAeWE|7iSm&NOJ5bjm245TnW#>ldB@RaQR z5T>1*NyB?h(fhLv#&sN*=0A(=`KhleYA#Kj zKbBe@wZOqlO`|+-50_RuMzeLJAmbszE#<2kQpME@HVGO+vY8;>>3rQ& zZCpaPamilWeN>r!k^3I+Lyw}i?~JH_VjBp<5@L>n>AbwC>Z}icGIUc53)v zXuQQ4WRMNFd&G+bw|{ItXBN{q_GIycZ>k?M8{EwR&@HJyM^=Qas3U;BJE|C|7=y>do4d3{y5Rx3^BZ1P$ z8$yP?GECWH633Iz7TjCMi3T$@2N(S4cNi(w6b~A5vq*etI9!T$KmdSq z;7|Yl|7hI5aqTCcYWI8nP4S8p`^@+05xUN)gfvDek}s*P5Kz+NXSt~x2PwGI?JCjb z)$B>uj3}NTWip57oE69G(WzfgyW9%W#`R3oRfL$gIj#2ki&Wvy?WKjYQdX z*Ly(l7jTQwSXSquWxDVEdO~g>8^@&F#9JIA;bv1&Q3%NQp|`u&EW;K_8-O^XNR#HR~S+V#syM6LC<#!`B2*TduGmOr805er&lTyQN8bP3W8t?y#& zpUEx_@?m*!6Qc_C_WiLd{YVs2Kj?^EGNz1KWS1yakO;%_)|{vZ`Z!9PWJ;!7yYHpO z4jLrKzcnqfX3hnGBBANA5#Hck!LSToDIc#b%)kSgGdPB#W5@Ndz3*|6?_KqlG%)a`r?K9TK*G$3jCy2{b(ZqOPrrq&{_EGMG7IQPjP^n5 zEI1GeDoYktRzu=I5Y_#IqNhN-;_Ob~fA1<0{<8h1dX6qOHje*+vxH@@(jJBb0NB9; z0QgI_!QlT#`rq7TmYRmc`Y4k3v8vP%{8|+w!p4nJ*+3bpSaTA9K1t}xYd)%JuR1g= zC(#-~&$}l#6H;vJG>tODyyok+P79$8x_iS@!y`I;=ID9EModrCNS~$S2m4#|>)ol{ zt<~eJyKAv_N{aCFhE|1Bzd-j?0*OhD+VkKz+4Rv~=%vsQ(ilc@I3d+?B}%12W>sJz z$5{MjvlA9rLK>T$!Nm!0(xJ+IyOy$5i$HQ#W{P*7JD`Hd}SSgRGKKNG8J4dbYD!q~nN9##~NOD0Dc#oxkBd-uw`1N9@R{7_A%PRXZ~ zXxFFU$Z~P-WT03^rS&DZd-i8q>2NwKP~(CMo*p$&%ah11l&VChv;B4Bh-VOthr|Qf zjrKB|g$<$Y$L%B`#Tb%(f7c~vG!aHfj#qQzg)sHvxLy@2M>7UA=ul*?oyS8O+CYw( z#R`MIHitPXa@N+2=C zCMGs6g|Jan$$rj~5>69k5frAngdGs{qM)8ep-?sNiE!WPODe453pM6Y^=;O(mTAqn zj{N6VpSIOncQO%8Wpjz8d{QuU=sLOM%%Eg4I=@=hzR=Q7(rMj;p=n$6NwNm^pVy?f zr`oIpks5JQDr@bi?$vl^z{$)oz*lZQ#1TKAA5Sjmyx)J_{iN9Na{sk7sja2?QLoD5 zrd>38sZi&6RNsY?Q6;x&+C_}I`0$>M>i~fQ}Ai8F;P~i zXWeca+Kc~c;7>G8Y4oj3lX7Bk`TKUVZ>+0?L6RpKIn|s~hEfS8-02*g+?ZS|0msRW z5Oae$t_7t#vvZj};wVAcgli|O1kXFlV0nDbowX9BD_1x}q1Y2UR?bJhE+}-kRSZ>( zvlBS$`mZ?ZC;6%Zp?5zuy-sc)`8TTRSV`> ztuzbfzZY2&?fOpn6{Z^?GNU!QR978jRIb;8Zr-N}SgGI#h}~%NpXk&@ehEGzYCiKg zhai3~A35u+2P0X^2WD|R4{h=pO)PIc%l6^=n4SG1)di`q|7!^PZDYh0yHy)6O(evZ z!qSEY(R#B`3N5VM&APA*Zj$(T4BA9-@T_AtLVqQVBe7`>sP3aJK(0fUW}Ry&d^1Mm ze4wvuxdFwjiTEg*S*&lA5$QUm(Yv+w!$$Q~v%bmmGUPb!i~0U$=MS5of}{}vVJ^|QgVbd&jxg+lm^6Z#_FvBf}qA*bb4GQNd-fQxY|dd5n^8h``akn3N5$UGC{@W3`Dw{?(e9Rm(?| z0pcc@VPIUkIEKdcZ6I*%0+Eu#zpdCg_v1Ac**{jzEou`jAdHI zjzC`>k*k;7rkd%{Q(`uMN{JvwLrb`M3fyR=PBRjf5=EM~MfAy`BwY}h$ zMqElY{`Xknu~Bk~U&oonV&T%|;J;Qk(h)1#;F0Jj&)NF>P~byZkG{}5*keJ5F&WLp zKd{)>bN82Mxnd8e7_!pqSO zpiwVQNP^PSVdxpiVjc0YsnXgvDW8*UbmOTu3YKT=<8mME_ z-i`{uVBKzkj+#vp{)h}x?VMg(62#|f{_L_NFTj9yQ?cmx?(@Bld1KwtPtW0Dcc!$u z+^uXUq>nf~<|bGAE@Vy}>K>%G0uSudaD;Nk%VH8yNpv;{i7+_^W^QZ`kVgw6qQTDg zduOr`ELJsk@x5BE{CD9_*>XX=TA!pXbDA!L8sTf zgpk-^t*A?iQza{ejm}qxEocGrIA+AW30XsBYCy#$tPp-dLxVFk3_03Dldh1W-0H{o zdfSXe+u}So#M1X=FQX!y$7!Bx4(6 zx|5+jJQhash_4ga-u5|}UQ9v-#jQCuY|{z3&2wDa^>LJ~9j>Z?7Y(VL)*dGXo4yK* zgzDtVj}8G^^5R5BejOs5Wi=OqdUs_u_kAl<+;_)`=M2maYc2E4VS-;p1yogx4%D#w zINzzB4Hu}WrTa@h_Uqx#mz0W%3z6KV zD-~^U*c;KZPQSTSa#29Oh7t}a^WG7l-0TA01Jy5}xpPMvZ%XkC4KH?d0@wz>)rjkW z$hJ>NNVuzIFw2iDH1pG_uxyWp0o)Vp1uNlTcjwHxwTHEmwJVZA>d5nwdPZ)I%HZpT zD4oh@Nv5&DFvFw<_mSC@?k}mntSIXbZ15HO@NZ$Vy$;Za`fp%hMQ6_D$Sc}n4?PTN zzcsy^nDXIh;X;Yb0TkB4--saWu(;+E#6iz4yy50n=MJuHmR~}&14q8xhs5T-!S#(Cg}cEklCg3Z zCD&=0fdodJG-3gyAC0p1SG390RXDC?EHmU6b8X63A90rXD*8nLn@9s5DYu=7UVR^( z2GHEJNZ+e8GGH2Gs$G9edcm0j5MSrbD4o-wIIeDfnv|QvSLkv8F714{ zVHU%kVHZUqOWK&6s5=@_cN6f}&+C1LqI*!{>BEyx``|Q z_!tKgS;aR!x~72+Vdeb;Oi>K_RqI+3CqoMV3eOXYlNf?D69aoQa8Gdqg$8b3@GZ=y zPx=`O^CUo~{u*4ZPh(5^c#fOHe*t3B9N{o|vbnRlMTHsqtxQ!^(n`V^Vsdj}6fRH8 zu)D|%$D3}midC&#zftjgJ$!9_oRC33yj#@oN#bbzR}Mq|Ihx+Y=0~%=*IperSMiKD z9fr@+^PVAv{6kAz_OjZXh*{(!?6tCNSoKG;K>0qsa>~L82pQGOpq*iV@{P>Iy`Tj2 z?J2qK<_UdiBvG+Em6xZa^2DXpW=FHkop*UZ{%)5;eJj}7)iF9zxTLV0emdUFn zl{N>eHB~nLyuGwEK#E=DRICI0yB(rzRC^R8t$4R$>PykF_?n+kWOjNeIqZ^TsS zm5!X|GX{5_w=@Ovi#N@vC1xZ(9MpK+aVaEGlgXUq59kAs#>wG~cbVC;6ED)|x|i!x ztXV=yl0;KDTtbKs9CG$YkQ|M4C(lIo8fhCI8jJ`lme7zIRh0fFQIr!8(resD<6F&Q zY{#tiqKF0zc?u@Pz|$LSYu4Un-_) zveY1jkkS;B0?%4S094f-j8)RF5K+SilGbU+v}=jSoOy5M=hvxZjPxmYJ9kpI^jRgF z-GMo?ooo0WZ0NB9>c!m|d#g>XOzCWOY#6dHT@4M}L%Ph3si7gzl($)pnvm1FM2%?s zQDS=Mv%%@d-LgpWBDgQ=@b%2$W^Xv<3m%y1J`v2woUQ$l4+2M2^acytO=>-L!^vFJ zNa{Dk&csPa^RZq1O9ru7;4YYc@v zir*QMe~b!OLg-GB7(x-gzx;k(+3mKWJquwKdEA|NX!371oqHekGj;@`ouwi(kXhTZ8KU|)uS3bt>WxUU|G^0emytn!+ zzb^D(=eU_xH~sk0Ubb%CldZ3>IKl=F|HPflOiDF5?<@FrXQqIAZHJ%=#D!4HOvzn^ zAJY#W3BVQztsD=enq?RuLL6$Jb*6vs^n)R!g2P_$AaPuX8bJt6EW@6ScrP=bK7j!$ z3`?PGEitJ0G_S=*615LRpCXeon#$aI(2zgS8WndM88=Vos%1JvO<&AB>&W8jON!u5 zH^D7m1!il8VEnh!njfPSLlDw!LW=C;&4<5qf@MRua`005PpIrmNaagb;My5P>8Xv( zL|Ww9KwBMnnAobX^cZ=WF{~`#(D}TWv1edEK$^9_oB=JJ><^m`E}YR)7AxgiH#8e; zxdkO&E}JU@b0((Yi|dib!$4tj(VQk4)=d|k7x%1<3sXWX4D{M(kXrZDY?_0i_+wWC zs5hyT;5hTb;nFB^zE%T0iD}*qulM{}^ZR>W_{*QFWN7AOP2-~HXkhAS<6>n0N9pf( zmG^(`4}bWbrqh1bkO@C~#CZQ6@b@45bK2qGl#rd|Ic@ss5w_!=;K}M6VNmY-V$049 zDkuxZwxYZUXos7xrtx3SEyw^F8Xxp`Ty_SI3y`*!79e{WX5qptT6L1C&f%o(zYSPI z-gcl)PGqB=Y=q-4{ir>#x1y=*bS`zt;kVQXkn2A+{2GP=w~R71Z|pz)^2U&Xx*;ME zX*wM!`F400TqPl6Zkn{OJ@GhSWL8s&rQF`zR)T^C7sS@Ox{8Oc3R)6dD(V*pCuZ9) z4FYlwE=D!H!A}7;E)i2f_GF_x%Z%r>)2r*e(pP1&7*@Ns_7r5eu|VSKrzo?##3Rcj zTmbU?{M8n;UC}!NUr%iKor=O^GlUJ|m3on}#diQt+Nf?GKXULcg*5 zj25&~WqrzCVJ2I?{TWaJOCiP~1!8veNg$V0>Pna52G>(5BWS?IU)p60{(YEDG8ZZV z7k(rz#2PD}_$A)I7Ja@W@t?79){E`szdqAUa5|GE#`Lb&>{@>)EVn4T_3Rk=yiw7X zYiS+SxW$=$ZMS&aE2C^6Ligc;jR^sUlkj~G)Vp{B{L^0k-cCmR%X76caI!M8cCh&m zisasdPTc&Tofi7fjxy?h2lc;I$%mB19lmp+be4P;{L?v*@u`s&4$I@L0HL;dCZ=ID zHJjx-^*bT3pYC%e;ct(_dztX)_7)L^qmF?mrC^`yS$V&>k00a_oe& zH7YmnL&h`_y$kcKU=#!t!?i$07^?Kl1hRloM$>ap$VI`6r ziZP2sn@P%w1{a;4V4%|~i$}`eO*3w}n*#iu4j6NFuXm?$zLp zy$pHyfQ0y44?aBeWd$;ConfwQo?qe(=nd0LsUQg0E?}^SV>@N(NIFx02OVCId4EnK zO5?L}_ffxF<>qpGRJ9x)ey`|uoid| zE9XrltZa%kT36j%^U7=M6Te6qytvHJCyQWzHTT)5s_~(yIq%bL$B6rg0R1YBbJ9Jk zqSam6f@EOq~ufvw(oqA*Xsl>73>N+y=fd;L} z?8DR;XgKKYZk5&pqRKlI_O;p~fPPnv(STV+s>->jQsJ{%0>xJ^BKDQbwz^5;)ML6) zGk#&0#MVmUYJU1Hi8P6*DQvnw=JW~gUnO&ME0fOg(`!9`j_VQqclP+7Md(e6(l+aS z2wg-scq*E$i#Z@9O@n^S*70Z%jD=;TlonR9lav-U4IR!x-d<=}SOH@8WL*q zlNms*_|#5vHn}=??OE2M0CZvy{in_d;kAQ+XmUIVP->J6;(o6bGQIprfs2G_N05d7 zX7X}DaUdh(84Lsl)v=X&MyekqNAvut-Mdkxk+Ep+M0rk-fy{=rQkW=(5WRBN1!cJv zstLmEl1a+VNXIS_hfT+{zg~mRqiS7)b*vRMMr!PtvZ_dVRe7r; zmMFc#{Hm|~8&0PlO;)66+4_?w_8=oGCgcUDYBF?yi|ui$2|UeHt&R_JdEaansI2Vv zWk}!XN^^#I5V36=`PaGE6NJ?R*K6N;SYX79)ZvQZ>ZRs%6F|OI?}kK$1sn)X;arM+ ziKJwn!(6=ie#U|YdIUz|Cx3mFhN*Gfkp>8t^HYjFSc=i$vD9BNmM)SvEP&vG39G@J zifY;uTev*)yJgl!yVco)>Q=P?`&e%+?X`LQ&ze%bjK zUxo9x(I?nH32kC(ouf-OQVNakM zY2563SYxRIs`v9Ra|F|(zDRVqmUwKr#J%Nl=ZP7Z;{)jW2izf4RH;QE11Ks0305Dp zv8RUU)+V*}s35IKNBiEKBUYh+?fGLi$bYpf+nxzo)8!mbntckN87Cx`?BkkF?}9V;&eQ11Wzrq zGO_P2y&+@P+d{n5`TC_CgRfqJQ)rPSbD(jM3meDuC~eD|dABr|iqOgOLmMbSVu9K* zMID?Xmuu*(+DOKqyM3!#pET`JF9G3(Y*(qREM^NlO-)dwUK!@_sS0E&ANOg zW~RX70>-B#H+%g(H^Wwpmw1vfeQ~O+EtNUl_Bp9ivRxWzJ7j6^ju5S)6VjDqaVD~l_&AO6 z!dP}G(pV2b-RwBzLC_9npt5~uZ;;k-;EJqx*BB}MpyQ2$0`{|%X{l>Q)-=y%?4cX_hx1i7ec>%fXs_jy>A zatsehYjt0^kT@H%=wHr{2MLH#*AX{6(b- zyy1OashY~R0YVGuQ=y9m{1k(I_10l30FI9-qN~=5c8MGErt>WERlooyb zSQr5W>Cr)5W?NPuK=)-A&RA`&NujC>#nR}d`(bAz{i+z!M6y+v0u_dElj0sMOL|-{ zwpvZ9YwJX@$16Qgh=7K`kSo>DMIh<&_}yoH;-Rf6KqAIB=_{v~zdpytpF_44Ap?>W zwc)1dRXr-mgOKiMFEuqreP$m$ zvjT*rl4R1fAAhfAT5FV8R?UO_GC-a00QH+2Bw6&2coTy(&Y^*H_f!h;+j7(=4Gp(0 z=}v@0TM(w?(55VQcxqj7bdGpBW0w3d;n)44UQ$Jr7pSW^r`iCsrG-qG@|e}$nxlU9`t2`ME&r1WnmHQj{SmGI zpHy%_gJ*X9le9eG|4Q0_!2D;VEmGdF{)`N~9#y~H0L!j|%@?aBNf6_Q$MXe7<@2an zc;r%Du&ilurqo0xMq3Do+-wlPvy zaHJFP8PR-+)2nS#fzs(5xy4%ZBLjZ0NBNyV6`rBJ_fP4!^IPcEj@O1BrS?=vZJY1=@t~BN6SzmSj?74Hnm~C{OqB+&dzPv=PUsiO4qt=OAds(2yc)GDZFaz5)V z$mBW*R)qXy*9@g2G`8Upfr=?}!pInfWqu>snWJdV^c|VbrJvqU8fBS8RAK`3L~`@s z3ntf9xi{Y*;EvgIy7k7_sQjTZY&EEcJXXd-HlPx8q5Bh`9`yze&3Q&(CdM3(S|{Q- zH72=T?AX%ad!Se8%RrP})6OZ^fIj4dT9g@OP7FP(7gM0@+2!xs#$|n%xA$A2SEGlW zcmv6y86(G*nWL19hW0cXrn*hq+oimu*1iT=-S@C~V{2c4D|LXLn5j!{?$-?Fm8JO$ z-gcwPGWR_EL)ZT}WAJxNL;g#=@!Ldb4DD^Kjs63BOM&0Z&-#oyj6O4DlK&p;KeIZo zcrBX@dX(VnXH*sEII7-QF=?86eaYEmv!Li&i4FlUnKWhkMIO1T9`EP`=1NJjLZWA` z>*rcGQ8{(T__*i(j%?0oFqUv6W(SBeB{G5%2UI7R+?QR{??&Ev#&1?wLu%bysr4T} zv>Ig!F7Tw?TX+ie6Sm%;aBaS)s{Uxwm?Kk90VK^ikZt#9NA*d;O`hr@?(njr*xtG& z@03iS)*IBGvWI-yEE`!-^?GHJaobPv);#JgkK|@1>Qg=mT4d_8E9RzMiO6m#kx4=# zA~)c2-4zTNsjRyQD4ki*%n#{j&X&seZgoElT*U5sav+4jHqq~$SndFk@pgCz__N6J zcOw0z$YNq?_PZcyX8Rw|h`#9C%kU?KKoR~mc>O2Te@3Fq(yS|>G$l+thQes+aM1|7oTCd|5yE-eUPA@fC6W%k{NiDrAQc&4ID@`vI9cg%-86Mv3!-6N< z;QO{`p!;hO1}luJ?LiWT!7Q-n*HK~M&BSL=5LB8>kl=sAjnrYV5J8j(` zcC4ZiU((+14(+jfRXo#(@wQ*#@$eQR6!326l*~hB55lZno~wo9OKMZr+BYl$?U0PG~C@N>?T|Fb-2WB|@p2OS=4FPuHf#CxJsyo|n!~rMEJ~lc1ZW!ax+pk)%u>d$!vw zlFll#BolRSpi;Jji`9S%TiGjdm(L7~oa$g_rdYlZHtE-mM$9WjkvE`^gh|D#q7MOU z9MvbP=yhjof(+Q%I(&&XM*raf)69w?m`FDT(iq+wx!OY5xEKDD-o~>fW-5=%J8`1ITBn~7~2Onp`Ue^8-HsWuOk`m&#KoD zt@$Y(IclZAbI@M#mO=Fz`egBM27nNjv0LF)eubmFZ6s~|P4pH!Qpoaoh~Xxog95>1 zT|oV}%mVh|;Ps>So1B?Mx3-pwkBaE6?=*zSJ;D1K;{1EoC25a2{PEZUTG=H2!>bWN zp?&4u?-q;?l25sSu_ZSpO5C9Q>}3?JlLW=64Ay(WOla#v6#$SC6wGr6VSFHg!0M|P2ZiKwtTjhp{*pYH4FABR(r(?X zfi>9a&s(7`FtZ{{POWpawZSvZK9OiY|7fUTm3Ip?e*EP~?J>KFTtDa~+;~ah2y1|6 zH{yIfQjzY#yS>Pp7wPjqs13}Yp2pPC%JL7PIs3n4$>T@IwveCx3hFaP#Qht}ztNcA zxWCum5@aJee(M2zgwES=Wm=9k-4bPm!fq1v5iD>}0}H^CH!ny#oEAjz!O2=CPUJea zfQ8zvBs>iQt{X#{gC>C1!0l}t--TTuOi@qA)r4R^9u$}MR{4GD+)74n=P<`CI~|N& zVnH~}+3g=2CX@Pt%e=#%odq*#M-mQ8YA>+p#<4}_)zt-wmT%@5+>$>0q-j-#B5tlp zXt3GUc6aK*-ifzF6T^N3Uxk#fC6i=|TqG#Gl4*^F4!8{TlDsyKw5P7OJ%c zn`$Q1ldg?u248M_VvGmhU4$l#372Q*F$!Rr(lL26uFMpDW2Dnt>*eZssJZw;{1hgr z_(=xKhj|gTsgVA0IN^`z;_sFBzf7lF7`Yi(+UQyQu~q-4qg=XCdGNQNy%D+3?fT1f z`ahBWx4OGltd@;GJ$le}%0&sf+;MvBT|OT$EJPs{Q0iE*c}`9zZ*?ewK}PJlEje1XcDwm6d^83ohfU}S_d9UMp3eM?+>2F=sU)Hx= z4Ka!V)eWBTxVkH<%+HC%HBQr%Gx^^Ge|!GCXr)^!tQE51fi_YMa!is zd>)I_5W;Yzh0lP6G@Eg`+ToyqoG}~6Z@r=lIJYj@7=xrU6S7~u1c)ta`*ynz>Pv0} zBroFHD<^=DK>VePW=+&CyqOz&S)G~R0+O})MVcnX#IMyCV2HDE#zHS`Sa==;AlGHD z@g_exw1NS4DdB)dg#qb=a{xhjgD}CFaBOkEXqlw4kjn^CFCVf))AfC@sY{nDaQ9#z zx%H>zBe<)X+xC7xjvzwJ$-=>&Ulj{1k%NLMtDUn?zV!Dry0;`-sRZu!>z6|3zR=Yo z&)2!Y1_{iv3rm%45C{V`FE~GI%l-r#YyIuel{Z~XDNFl-^)-DeD0QsG>&6n{^J-S5OmkAVR?}%wQ+t;+gLhq^ z+gv?oc2pB(nFZy6y|-E>2ZBHRzh^e0Bwu|$#Q^eOu{vFRO!2drq26(;H3EA_0cwic zSf0!Qd)UOBzMy2%#-3srgBWnbfBZ0rn8p}apk1GEE5a(yACnCMw#vovR{bywe{GV8 z6hO)u8f?^s(j$)?yQX}tFyEZ%_dm&no5OyAz<_vw@Wqe}b^LaQ_1mrW1>0G#Rn zCE@=Q>YsPCZhHT9Z2#XV`jonc4K`b{H{0$9MxCf^!C-xh3AJQ}WF)_? z*af9CTS2*g+vu5!iLsy8`mxtm4_l96kHcuWewl_0eVXnHM83H;+{~NERai?gQd?Q# z?9irCo8vl)pk5M^b=l_;-PChot(#g`)pI2rZm)W>Y@i^#Kq{_G!I&X;SgQ;&;f2VlNHBbTx(0dv*SzU1fb{zYorN6iyZ}z~e!>P1Ssoi9!LY|fVq6T?xfZO_WIBiF zXjm|%Z4v_egD#fM?9OS^&fNxHn=i8p`YEu8wj=BuNxJ}qEF~T>DL-*6OVf7^)shtn z+5ncWrIc7{a6#vEC>}@5d|BAoaNHDmz?7AtEJbYuK@B492vc-iz{raPGa^RPp3V`W zmmw64cACf(2aWy}ik>Oe%_!Nu7?MN;V}NqPb`>F~EVyC5Vg1G?(-@#h-J&1C@mpx6 z`N6b#)%DAt>XrD|UiWErP#+ zT8CmKUuS7TQS5SsCV|6jxji}KQxZ~e=o_Q^mGAm%#E}&~i6>H(m`fvw` zH<*-Ia`0-afaP~ZN0}ZJU~?r;F)FdhY&%9hw%s~^#AByCWOT0Eh|m*<{pW9<^g}0| zd1gG3_zhDt^rAh`>*qHDQgdm}fhzfw zWPKTKTEyEGrbu)f=R~C8Z{m?3qKuB3jRZo%^ zXqZ^$u6j_VU(+N>S8l%&CD6d$gdBou3eiG;V`J^3DFkB*(&Hw0Q^{(IA0V2*?-0Y@ z2&7&01u!>&N>wpl>NbLbXwCmNA+aLr38Sttkx^EpX%?Jh$kHfJZA79$mMS)5z$!XV z&@+!hrCbXWW{8a!sej@`y@K5ql(r@W$iS$>zSghcDDIi74+PL$F1aFBhffmXFgBTP zE7LSNng1X#FB}t`?Wr9-#QE?Fa?+*w*jVp0=}gfOx!D|p7|z|&;@_|EOWNfW3`6w6$uzFk|av#Kln!)^GxNsq~zF1e$ zgTQp$n`_OAWeC*&N7*}t3ASZfqhZ^&jSSn7VcWLN%&=|SMuzPS+qP}pI9=6U+4bG- zzt6*doez7>HMQm#gAjvC`AO&->rL#K+POt>yK4jt^TwuWe>tNuHJp!x6j#$CZR*7G z@Uv9jwj*r()|lvoI1(c9)(cZ0PBJ9X50+c9w--bD#v?elwOWF1G?UEIgma`A4Ph$M z*(`{>y#S@N+6qGW!!jaT*co%rvar@sFhb2lZ7PRf>jXeJc`yNbZH(a%b~3RX8lo#Z zR@{0wOn50h#0Pb);XyZ?j#y{9sWvb=El0oPwe1|}9qm-KN&*kk{B~xR&&USyvX3UUK9dTsmn#~T4*7sG8rN|y*`gbqOhTb< z`LOk4g&_XG$BjrbXRBvQ(2h9^xy42Tj@dUqUAqh&#iB&q84poG_ARmVu10jSLcpG{ zUnDKe9uGliE7Py*XUsFkn9Fn=6QM$+$Q-R625d-+-8TP!xkTtPN8sZgNcc75X z4p>WUG!JfpW^!#KE0mbGn1UPd>BPSGA!83>0OJ|kVm7tlp5+tktFgildYQ$YDBjGj z?v_)9& zh?XquT_bGMNk%=Hv^TCyBMgY=i?j$cFANy)S{!BHo0+U!#I(5oYMShW<%t-Z_b~Di zPJRp%g&*1Xsf5S5M&$g~6t;yPxaEP-gR4uQZK2dllcI|nalOaiZPv)#frSt{wPE0- zpwY3LGug(y!Bahskk2)~E{pImgCIY6M^FUK#7Y$!9$11yFsv%p6kV(<@E3+$_75Zo znT*tL_3@~bNJ)1vKvulLLQgrdk;A*@3q{ujo z-@7uE>3u0als;GQ-QOLbA70k8WnfZ887?|_Tewk5?q}fpfANLPN$d}nnq6fFBQsvY z`g!+^b5>z_!T*v*$mTWY6N(a=$BK)y;g{1E!5nsNoKRTGpA=%a9gGg&bqR;C>~FBX znOIq%Caq{ncvxrsAvs$}S-g;r(PRVbYU5SzM(XWPp&gB!my3}(>t{8X$8(*;Of*D?$j&wL;y~=g z+^s%`trVCx#i~ZVw>qvjn+29oJ?F+)RK|+rmO3(?mxr}?YVS(nikEq^AR;k)N3u}V zdMk2WZFHog1RSDx<$e5>9GhojLHU$A)4qZD-MFr<7YD3e?pm9*A$Sp8g^)$hjLkQEXZzJQ5kNPfoYyWV}G7!i-#qfssT;HGQ5;n9KCouo^MBVKk~Kt zAN?26FY!^VK>Q8Us@Etg8oAm7UG{Z|56y%_@OSYA&rDOt_HU=lP3xym{rPY%;9V!X zK4$I+ZGErL;5Ti?4_`KvnXo%LeOa)1_`L4VPb*2G18MXE%+FsOvs>{U_vI{IJ&4(T zTBLZ)k{fo5bG6p(eWJCejBZ9Q_algdek7$UHVI5L<$6cu`M>y-sU81DSrgD1k%<3Tt-4H9NXr|<)k@)tqs$Ov zG+W%r#|?Ak>}YRg_xZGRrhn{@Qa^vl@6<=u!CRB#%S-g!;YlUBPw^+V5`<>E8roTc zJi+0dpj4Tw$kixm$;Y=nZF;ij+hP?wXvn=#x?MfE!no$IQqVqp%{K7bh;s?luswCj zR<0I^y{zi4x=ymVx#`Fn(zlL$Er%%Fw)r(giA}#fYHHEzg6b&_fj{f{d%%TPd@HbH z$M-cyaX$$R^XXC2kmf#iJxyZ-BB0>B!IU3!FsB8u&R_hvy$}?lgBG5BI(_!GCUaoe2R|^smctAz-IMo}{X}oFK z4?UC9Fm^0zVE(Mz)_id*hbo)j-JyEDUh~|GP^73VqH6R`f~KwlMa5q(0TjKe4`mkqm_Q3E3+s7g@B3+5r)7U&J~j?#)x8 z?uA2=6^OI4C7>Bc83O3`qJ&xu@jAz5hNVHHw6a^f8eO8DJx1()%HHvE@jctwyLdZ0 zTub((I}C)Gn>&$OJn*HlC@3k*M6nN~Uww-4%FlTjr#HW!!&p-zz7Bw#qemU3I88Gi z4^y-Ak@=Ac%N*sJ%rZyUC%utH+|u$ms zT;Wa=fa~?^5dwr)czUpLa}`y28MhWaSftwyOyAJCz4Sp@h^n!5LRUI^JKKS3tcM!#3Z#zB>p$9++pD8O95sZ9JhwYXTXgSPVR{6#MwD>!gT4=8a6WQ0uET zDz~7*P=Y;JHek;sH?*5hZ@Ymyfu|ZpaNxqm5~H3K$=(vGH}pMF*z)_#0adgZ`N!94 zY1r?rx?^XW0 z5%KCEN5=37lcHNp@cmr`NOyt+r&e!(d%yTx${d{=DV3#Anj&~Khc;pGB4#sLd4}^2 z`;0kli(#9(i70Y&r`MRmt2!)qNBgG+NqtdT&9p_D7wbcZo*kApUZ-ynKB=Ba_@DWC zjf%VB@KMze3cKFiUgc6~0$fv55g*+{i-SqG>CuR7MiZUt!ea0DacR@BeY-HwAP-uC zX9bsb+x_vqUS(dRTsL8kwk7Qog6uU}W-c~wM7<229Q2?GmB896r*zq(T~_pG(t4ScSmj zDS~0S+hW(7mU%^Iw8zfTr%-vd+17h~IuOA3uh)D>X=TVu#g+y=%2^(`%rp{Y9AzW= z{K)<^9Jm|*a|8abQ4la(g0v>u7xSXfe_OBquc;0F|7*C|W!egNiedj|EMJZ2`wl6kfYEoN;{nO!8L8pRKxlL9%H7^6v*zm^ivEog zF;OyaP>Ri`0Is+Oqu+NkJtO;SaXg{mEF3ZB|kaN(&K9jUHvGugfCFYRBe7?R(h>l zvF$yWcWy1^n8KlWv&+M3V{>L&@r0;8nx{#bd!KlZgj$WaE>;){cYQE@BY&wW=5d*E zleSkF%=uMP+3#$aMsfwCaX^an2A4;YC`hRVu9#9{Mn{{EN}w~Wk~SpYon~H5y%15M z(#d8Rua;c1new46o43p?eaZ;SPmj{mEvfePgeP=4wHqodsA`4rBPK<{2Zcv5*&2o3 zsAOnG8l@{{sgREMyQG9xOlT;gXSi`#`b6<<0<&rX?DZmwv5hz&X)eZM)HP=Sq`1OX z9PAsZ#%tBLLNQXl*mllJLwo|F2%?A|C`u4ycBo?2tDFuO(;VurOY{&Vz{9nJznC%A z4|WyBs%CVG(z=!pf*>9V~5B>x%LIATLCd97WI311@1rT>UhK;G3t%Q3{0=j z2~&*PHVD_A$MGtnKfpxc>brc9?60zJ7kSZ_ zJ(<3rfOZAZcTD0ewk+i^C*~`|3k_{da`p*Th2^d;kY4$kEB@x^0R^^-KpePnhb{+zlqQb;MP}jFck1y zxs_}tr1K?FJS2ry2FIBXZ8I^AD+Wze8b>*?T$*N1xd9zm@BXYkERTtW8Ot(R%1o7A zpsQ@OIM+Z5B$T$SvF>rrz#&SDJaEsa)IFCwY$UzTS`6Wc#~wF9thaQ6X`C#XE3%)q zI+L5W$h5PoSKrbtpeJ-OV9|(l0kd7$e`EBFIc*zy%LQ|}=J53Q4%KuGo&b!f;4V)B zL-endjylusj4%T#JC9%>5VA{HJi_VuWg~tTTPlluFz&fBJfk!)?J?lCJ9$I*sYrD$ zeen88gOSBYiUZ{4eYQ)fAqgl>Hskw95+wduh)0d&#c(#ku=#UgQ2&qcy5HHoymoh#Qtr1$!w9=GGj5anq;-rU6bEX(iTWdE>d2UAz$9S`5<<7^wW}h30vtw}_Y6_x8FM?5&?ByO zF0kbEIi(sE8?c~G3KizlKhrX*@ZMgcn}9zJqD`(BVSYcCNEjD-?g^k*9)>TxsCp?m zuPn`amr0fYN+RzE6*COr<&jJF+=7y%BNQ!0i?k`Dbl1%Apfhq+Ae&xpDsYdR+S=X( zQl9RMgdZwc76R#&8M|>TYk0pt#+~zbJz)g=c~WmWk)O%EI6?{guQSZ8Z^)GD)hhB4 zwGId^bIEsj!rORTxh?1uUJP2?kUuK5HqlkjD!E@u@S8>&BfZwyA0Q?}L)V4>Y4 zI4j()6?94ge;R&h5ZkAm9k9gmL&l+Ix;tJaMNqYvY+%GE4L|0iH|XK?z);JyqzH}d zgVg{C8ekBR0&>?zg)AnV`m$L5JnT*3IK)4~86z{{ThsBED3EWby$0Nmg0$lVRLJ1V zC<2J<(2&rzUzmhL;4O0CXk9_QR|oM}L%o--hbPp;ixUT`xjfKo6d01E%m5GHN*bh$ zla27OG}@ofI#$`sB99TwTS~Be3nSsK2Aw4#MBOv^&8aO~`zSJoAgdmTU5-woI5=^> zr=sW)(|ijPMo3AMG4%3Ua1@tWrA_~$WtQZQE(-p$*By3HF2A~5BVXAvD|;_Q)cPXc zD6~XKul26iR83|EVqsztxv*FjA-!6@j!J>x% z?mE*2-6o51lN&sseK%B;FVm2#Bk*u~yVJ=UyY1YZXZ2XBaSaAWk)pTMyPJ*c#8k)S z`CO@Enc96wq55S-_j%hYMd4@ zNh$d}22sYlh&q6%`9$^V4THGgCa}m(3qyDHMqO^lR-t_m1UOnR&7Z8XY``{(R}^M| zj=SR$B+g%iv!mmbZeh(4IuKIP<6*b!^L6M{lu-9G@{)NY^w+~;8Q3fhm zPtRrsFOMtXlq+!sbnKMwh7bm|{0`bj@50>aLc8V;<&U<^-TbG=yFJ2~4`dRH?URtO zxBB2HH~~n@zazoab1QkGP=Q-#@x+3vPGn4@2lV4LLypNG#AoML9}zUetPLX`SgmC- zA6UfotZ9fiE-ms-9%FtV@vk=#jPO{=?Mt{>*smp` zvY~u?aAT08d55Q^4@^dowf)w`NSg?X>>sOmwF0CWt7u9wdqOSr4i9>pwVPN+b3R;d zH@1OO9e8a3C47~#!Lcu)0Jx^iUs7v))(4$xQPgrdKN|Ot(5*YWkd(pmD{XRMF|4Z0 zWAfWs+uVKRixKN%#liIjDh26JZuwqLkvgM8C_r_4F))r4XcxROUn00y0?9BxgK>~{ z!LvFGk^WV??*WYpqw`RBz2V)s@rA&(jA^EcNXbQNw(!%GHq;(;^8zPc1pW~4T)pm2 zL<&p^&|W0JMTHKsrw+w#iNsjxHdsc}eGK#;66OPV>nvofRr9L|N3r^p(>>%XKO_Ld z7a=y5AWe0rYoJ89YhsmO-lJ$;%w%iRGo4ZHBiG;rUN(0_Kii#I3r)vaGiB0M={lU~ z=}jH-rB`mAmqmm+TbsFyFZLVU$=Z@{$(*qx)kiaR8$(yQyP8wj>N@XU;jF)2?wFs$ zfC5(?_RCAxh?rWeCP%zjGi62gBg*mnMH2e!Nq<d;f15`<=mMXf z|NMwnVI-^x`$3(BU-P)Yo73W&(rtD|zB&z-O8gsf+%P1JTkS5Olh6@1FB-Rm0q7W6 zbYEoj+THXKA`SB_utur^l-dBbIR)%_ zN-G?IdTe-g9z44muWsxCN+o7Za*d5KNzAl{lb_q;T5)9+zbu}QLd!=u4iDZo&98I( z?y%uL|Gv{7fLajZTM%2AQ$=cTxmjWN%0rqZTRJ_WTZjU&YY_hxix18Ts}hdlzVb(h8?C<4y-pi28E+fBYf_o#dedZOqC z0zOrg__ZA|zkMUHA}i#KKK6N}u}m%DF5jD5U3!QA2Hy_VKdDANM0u2~2=(S$_v?FH z;a#*|rzc1t!0k)AV1Eh?SD4;EnC=1!X!O(4b>{Pw~*Ai577qvfctAXqr35n z?zD~BTNb7Lx0%~Qx;NBndfOysd?nDItI5c_ITM|$F*3<+or6MC#Wv>qRiq{RRt?;2 zLKe9&8i3$e%A#nY;d!ZRfX9C&cB%wo^-3->;q%|aO^Kv}{WdJl?BM2yvEg-^U~b8w z-3h<+3uv8okn)=$5A zACC?xV(?<|PCNY7H0StIWUwCk9k%wVas*;NrB6i_Pn6SXsS%>lFGm~xF-@P7EJSb4 z25i&pZC7lrxAPuE@V88rI>Lf$)qK$3iQbu!g#70@}OaH2Zv{RJ`*IGgUw93 zyW?OpJFJ*S5yB0U`MGLAnZmX?mgD6IT(I_pu_5%G{B^>MOR+D0>ebyn=|~#eCi#s3 zPv>UbO-X!I6Qv)uVM}DN6%?$m7kXx1PapB38x=PngEA)0;>cy7UH@YODWZJ_nj;oG z6>Kn;W?1x?5MH85?_<_EqEOEzIBPQZ+m37W5;=`qw!cKcrbTznB|hXSISJr6sGxOU z8je(L9{dNNFhwnW?L_W+?QS;{mV_!ZR5_2{hqe_CAu0366?zgiRCoQhLatsFc&=NT z_Xx4Drg2^^_EP{nB>buZUGC)Db{Gu>{ix@%bCIBL|6}_(|ur^?L!P z)o&=cY5tdAM-hmcf`OL^J&Ex2h^D|lYrJ;=tUMS6nKlC~BA>YvnmVm0ih$8$%Lsc` z3ysBk3@cf!b{mKbY_l-2HC7c;hnz-`Argd8gS`$Qedg7zJ^f68Rr*_ap1^42K3)K~ z5A4|o__*7BXCECodfS5w2hJeQJ+&Y-Z(PFHtGJJxQO#c9H%b z(7YT=KA)+=g|HRArcVpg6Yoe_u-nWMupVl#vveIg!F9dB3Z^>*^LC-v?K1<5CWzs1jGhzCXRltC5z7^}y=zi<@(h-|#{#N?5KU0tgI%$A1tvnvAiSa2m8E) zG4lcBSb`@|9&XvXj55u)z`h4nrq9lvKCbxMcG-3#!Tl)B|3a`J-^A z?mvDx0+Dgy*G?*Ha z5dqJo5z(#%3UHJlV0&ek;iaS55PnuWTdM0LfcbUpfY15-m=dD&3x*0r57>vqtpAp*j9xg#=J|;^d zKKho^9U(2Z!!-S5^i!+AwDJ|eg??v$TW2hJ`zZF}5EqnZSJecw#fnXF<~2yC3t)?I z)-Cel*c>-p=&ax@mvi1zNKLBr_lHw$60rsW#rXFGmHtO9vFX;@MH$j-@XX3nYZ$34 zhtpDS8<_e&O;D2ROsQ0sFhThZX7|@ObuhbcmsJMB_*DAiHMW5_oaJ;E{-~1juem}h z{Hmd_jJkT634o4eK}RCkT%B6oaGu#^c6DXdoJJv*^3|Wb9uhe zQ%El8LJas>I#dJi*_eff*oJD!i2{97>uOW)`L>?(Er}9PVCq7x5;fd}9kj&>Q!*Kf zz9*v^M@u1yNAxhRU$Vk~#qY(ME%0rLIOGVVtP+VEbUb8Mm{BL8c!ubfvPg^`T+*PdOR#Dg z5n9+Vfb#=>wU~dg-sd&QhXpi23X+&)?ys{W3_nfKg+c^}5k_d3>F>2ANJfaP0$nZ<)41S;c8etMW%)QQ%S58cAC$ClS)+M_mdKj=nzW0ulaYPGBSFpDR zOuVVWi?i}wQ4(A3mdk6$+y-!LDoCs-6G2^Z_Pop5wuUbV#I>gO*nGms`sHpyoNJ}3Jw`S2#*^7H%h zWARPPU$27joi9JsWK2a|>|faUa38avebZ&=h&-ou4Yep}nTWEpdZ#U$*{mr+shMWz zggSob_8laU!@?4hLSX{WTca*OnOgaqajy5de}D}G+OIxS)s&>jmx$2QA4CkY6i3oJ zkl88tpGLE0hc=i9E}So3L~XM*KD`{NtsYq2-|6t6jjggoikUhCY+5><<_b@kHuo%=GwDw=eI~3MjP%%vgzv#(*eKy>vjK9&5VGZs` zN_Kf0PpHiY>pTW3vZF)Ur!*r8QxfwQ#Ox#$M~%xtMd9(H!stIrszAW8bbv7%z69!% z4we{$y7`3P|74<}K666~+`W1`uyEq8cU@oI*H;!PVj@b<$Pr;|j_cPLcK)0j8F)i; z+}SrDfWu&Qy;_yg^1CLV-KN3vvYwxSu1ZY=jJw_@dM{O$&;>|?#LS_8jE~@p@Oia& zVS#@){M2-R*w3u0NMN;UM$Qz*- z+Rf=%q-v2a(QSDd8@P{ht)`<~J>gY}nM}@)VkBXRj!Lb{IN~C-rS8YWib>HoL!Cv2 z8U!hO9v*`UNK6e3Lhpr=kCT+hF^*+CL8eK?N`ub%;paU}$0b^-eO@zgh=%{gpJYKO zsO)}u(ZIa6=i++!A0g=;{e~4{=+5YSMH*Cy*;{7M9n|}+v&UnoTTAIUS*C=Btasj2ynG4DoboaS1 ztwI{CL}x>i+~*`d$hi@4JU(Tld&-uCDm5TL(0P8W7w%FJS*9Td;sA&y9t1AAxwm$K z4nyTKq6*W29Dw{Jlkh|~s4hSZ^MJLYV@bjSG|_BVgjGSW*a5G8AL2r^ zBK2WJnNtIcDC%KG+f&WrVfz{^JbqShu&s@{ zU$X0Y3ttqJl3Bg_7=m!Fg)$|%h;e|ehArFZ#tbAATlV6_tI%#r;>(7L5Kk*V zM;Zra#l}t`dgk3VL{Q)E%Z%>Z5(cn{RO;EgxT>wI-BUm2=H{nlFBuNFPnvxny;!+o z{e0Wh)0*guuMEKLCL=-Q(8y5jHN@-fuRHv42HB%#+T+FQ_eYrL97w8B3U-+#5^22`;nK;@0@2mXM(UP*jj4%OTC}QOckz8Zd zA9KjIjpzu)D)YYwLs?1b2TIjmPM2ZJ_<6^@h@(6F6CQv<>P)gKO%$}}S~}zYP%LB@0$6${goSxQ}fxQfu$7kE-P#0gyjJg3Fz6pPI098FuE zFnA9G6be$vXS8U)b}-9IZLQ-uw@UG!yMhiM7`ZFFJwsiqo5v463>|4f1pq;+(6dw} zd(JrVOIwXFr_3q2VYH<GTPuv(d~pD_6JNtq(`6t@QP4}`l>GjwLQ9+Q5CWqf zQ)&pl3VHSD3=uO$B$?E~oVA|m9JXRdN;#)_Z&9ehSfbb|C{&L6BSf-Fh8zH|7Iubv zZWyqK2J1_hkCb*7>0YpI_?oGoYuZ7pjcYau2-(x+o4Z^MDU_oOz?J~*Ug({CU19gn zdB_C^8fBu;nlYNYju>@O?B%RJ3dO3#2)UIR|HbQ7x4IM=vh46@t8cg=zi_K)6|tpT zgXisClb?Ar#yiPpi+}dJGp&XXS!cT@Rq{Ts40w_RM{m2_-QZO3hC|Bt8;KJ-BC=$J z0Zlp{V31B3>K}PA$mM$rrX6k=6rM+d_w&)N5S{CYk~F^RbyW~eUnG7|20-~&5wi8O zd#h!X=S-b}`S8Ex?1C>J*^+$l95RMiN#q_>@j($*sS}-%p1o+@N>gTolc{sAJJ8KQ zcz^=y?fS&NA{k3CQx|jJcFk6)`20a1^zS(U`d>H3hTY~LD4PG1D;$7gxBdkcTm1_E zxc}YA-*bdaCF$t&FFm1K>WVXc*`EPkgd}iA0_*b!k}=BjIx&FcwFXueFw+N%i!r2u zao-dILEH{HFikdKKzA@r=0GNi=iO8iV6K70D40LW1Efkn1o`J!qGdmaYLZg|4 zwhJ&=3zMCy$3Cm`YLd%Yk)-?8uaf+3_mDd8_woxU1m|zFTu#wjWm=)zNtP>+;u~ zjgZcM`8;MHolX72uLUFSnad|z_{rH#hR^+Z?G=@@U=XoK1si z7+!ABrv$Jvb9agzB zdJCcu9JL7O79q~{Wcrhp>2~2RKaGz4Y2b~!MT348VXN{1cs_T;Hds^9M@I#@Nm23rp&3^f&xl5RTN4>Z^Clfc3rqaqR|pI$`R`S!zCT)EUW92wA`4932x)1bfU3B~N_M$8^E# zI@)?7^O_A9g$yoG)p3@Ue+LX6vS1sTCt)(^_Bqlx8kPa~J@4VbegS(;+qwD`*TJ=^ zTp&PLV!2iP$$jj(s=~hgm~-mZC~pnmJZoc4b6ntyu=)$%Hnutc3r78XS>`@6%Zp(! zDhzS(x{WBI*}|(ZIYpjc9g3p~2{^gu+78kQu~cdb9iChsS!9azNU8?JdqkMgIM1)F z>0YOvbkjP2c7{u|OWAjkAB>2kOk}B%fR|wZgVBQ z_qv2LqcAqmn?*Ne*L#erup&Eg#&1XIr^>_nZ|j3r=GE5Bl*`$1_I>*MiAuG_B5y-k zw1cBBXrwL%0fHcGyNlTw0{uE&MQoxaPME8|L;vuI9V<>|dKAv{b`$h$?YPuX>KMtX zF$Vkq#Z5CY{ql~Lhs||8HxhoqLRH=Z>F9cUPbJioyuuLraXtk1DP{Z`8j1h=>y)4}uALtC-FKJ1p?H z?sUI&X&cmw*=)zJ@RiMstg|!!0qQ_$q$9+0N%T;G?^9zWE!B9^C=%l}v8PdLcJ{a- zwc9|-r{VFg#wP(b&VCAdyMOkuYqC8y9&1gP;XBR+c?Mc6*zJaVV}x1byxJAr{xQ4a z8^Lq3+2#*YIn5eHw4~F57omvmG8_C#qFr3h>$`VcjPgsjT$#4LDAEw)z=b)Dw*A42f%WK&@{chIS-c`BznEJWiIauJ9CI>P14{hu6h z>do?2K;75zI3X_O`(58O-d>-q`kv!8&iVd@d;RxZ{fEk_jjf@rk-^^xXSpdUmYiSd z+Vv}4|Dgc*Pdk4vmi=e1_QlPnWvu*KM}oj>8Y)fApf<1&CMSLRjW8agCXGGoJRrC9 z(;JR3hJRjqHUa&0ZTHnVDh`3rydOb}rGia$UZRmGRJ=rtq)Y`sZBOLGz!l_*sEEN> zcnlK`5=f+giCI)Mhfv3y+yCr+6`;61g5VAkIuHOHyFC#TV+#~=uM_MCWRR?(31SdB z;4vj4GI2O!k{n`f41=$+ZGi(a-EfV=Fnj{U8XE5ka zG#0tzRgQ{jLyBv)Gk)L!DaA$}r1G}3Ul+~G+=5xg2d^2JKIaAQx!sP5S+=ScL^^MyX( z(b9UOqTPa&>H>kQ%8s~n7yO;@3k(jS0S;Q>8;N{HSPX%L91nKGN4PUqsj~#f#HM0Y zpAePqQ~*RlW>>#`j)R(~^>@`kKeA!BTx9g{d8=YiBY=v#NeRz#@zUaVlzpC8@C-lj zMp@vNtoh%z|FduZ!Ovl9Wczn|l2N^z+Uc*JWb`Xp|H04kA6EXJUw0^u+N^%HB&XD9 zEqf%>ajS!5TS8$Vv~nnn6)3b0j1-oYbw%Q-1xCfXx)Mn!&#@>+xl@5y6BBeMNvIZm zi3N5%aAb>FrgExYfk7SsRLZ|M^%b%KRU|Y9NlyxmI23BkDmLY@f?Oc}>PNU9tgs>u zX&m>`#6Zg}vD9in0y!XnpX?HE6u>CdiR?_ALsiF>!$5z}(X9XAX*5@-Fl1h=7Jx21 zm!pob@r!*G^L^PkmT$hP*b{0l39K> zqzGV>N8s=+7T9z4oLPQsGUljjS&!;u_4+&^$}|RNa9#G+SQ{E`i>Ajac6E+Ik@3S> za}8B^N)~?|c@^g}q886&d6pH3;IUQnRv3X!a>Xh=K(#p8SV&9k{bkl>K-gDPwO)1*e*v(3gptNee!gfvL;HVkw!AR0VM4BWpE+r2TS+AOUWxl1BT^8x z5qw?0ZQg)nSw5MeF;Gsbmwji3GgMGUP&Nj^LI0<@I1+h3pgbaZgrrzpDgo>j*wcAN z@UB`F%(I$VBXccCr(Lgdz&rWd7*krbYQO2T55)J9^XYGAaR}93dDkSQEfzSW?PT{= zi(O|_SBr|^5?#eQl$T5!bT_j<=0HyA3$|t> z?^e&=&+FJQ{%zH=yX&=O)?*RaeXN(bhE>rfgTk7I*|CBZ_2^n@gI}5+>T17=j6jQV z(ls`sOlE5xEoB;M4}pA_i6d-!tKefbVlTozgmHU-qVQXmHi3>#z(tM|SDmLo4BC1V zffKJoZBB{TnV4>kYw)~ll%l;xVs7t8MQQpnPT7LctU4a8kPYzKDca^}Pi^#N8rCAd`;{IN4Z=RG}P!ek+I-N8i81z+CU0`1N4#1kD!(@lcF~ z#`aM6lWHm>^jiH(epw(r9Gf-_dnnN*(eF{)!q4J$m!PKKVEO#W@1*>5z^ z+_JBHG=7XKS++cB*)z^^vZ4Yz(@0~X!UeOvv>e7lyU;56aP_x`oYzUHP*RZ0uL6x}rrn|3fZEWA%+~ z;fG_14c*CQ>AOjfyJJ0pLap?meW^o5PbLQ^4IDpfvl6c_4e!0L?UvI9i!-d&t%}6(BNE!;X6rw%l+$Rm0o4u!QF4Si3nOi;@&DmQ)d$mKiU`NI(vjt;^v2f;8Gc$6x5e*1F0-5LN)> z03@{Y7A>4DibsA>teGY5hr!{}m>v;Zyc=*R=hn!Br}mR`;W#urTnFVkC*-_C*AipZ zius94=<^ysx3XT_Z0WXgiUc(&#N&uyXMY>+4BiD}%*$FOP>Ifskk1};{Ee(`oqr_o zL<#XIAEt%lNAMtrKnk+0oZv7lgujhjP>#_R?id~nT#~SRp7dnT7F&PMw0}O<07WOv!Wcy3~i7x!P z?*CkB{4p4?vvqLNH@Epu9hCo5_=kdr3MKifH;TTh=|3KI|7Gg$WdLXSRU3Z>81G3{ zwRK=wgq*}y(Um5ppl0Pc1UWjT*Vn*?@uLN*;r^k+rRi&_h0i{XN4 zz-}>X*!XWdoY5OmqxeKW5o>U*zZi{uq5Fm>v!UrAEY)}cfJ5@rF4yF23Dz$mSUkeSIj~h&LM}=eW1xAJDWsnmb48oi zxZ@J7O%$jJqdcpixVy}d8NYsik$nbH87jYpf6U_!nn!8jOCZdUclgcsiU~MIJ&MjR zotD6-U&u8TM7-iePE1cOcSXI6fTYov@-`LHR1_(@3KP3)=f!_`o%jgA2Igqn)^e1TrP4pxBj@2!) z#O#kwXY*8&&klIK)}(ODZu~GRAAbwesMIB&tY4jTxMlKOk~%~`Tb-p^l|ow`+UIOr zzcOs=dbP7zgRDhXXE{zn3(Vw%CBYi>vHJAd8_GHi(haT)ODZM_G=QZd0PUPjODDF_ z?d4j{hQ1H2a!jA0MFP7%(&QiB;05BksUo`JHs!27yg}lG&42vkrR3jHwSur3i>8-BT$+gF%C`qzB?pLYHWqWG7szrKH~3!N2KZT!BNq}i_V zBQ8;er_EX;Am%|Z_dzEf;V)w!Fc#rx3DZ3>B1s;3c3cIQWZbh zCF&>prMtXL%(>2*SG^OSBW?5wnG?LP2bC6+85^I*O;$$VeOm9F226yuZj4%tYIb25 zNyv%2BTwL3s6NBrt7MM-p-`C7CLMn2yTyi}(z-mn+ePTc%57G0nQ$rfv19e0 zT4K+ELyS)0$y>zij4$QK#e-TUJig{moZQsRo4K<8NnQU}PyuE8-X{5VR83!+J%1GS z|2(e07Y+szrK49E5P~kn-@vJZ@u+SDegulH3h~8IDW8;CP5*zCy=7FL*|Iep2*KUm z-GfVj;O_43?(XjH?(XiE;I6?TI0Se9Hr?l*&h7U*qwo3g{9R-0vF5HhXH~5#Tp}@ zffJ}s^_a0i>jT|3jbU3__U?#rjsb92)N)@~YLX`2NiHXFHJ_rCzC;Ku ze!ErwNac-zm;w~cGImE-)YJ-3rgFn9vOVIG1Cjw75b26>+Ymv+rW&UddWUk}hrwUF zsWP1%qBhKtM!fJpKrEBPB8qzy)0@=1UTvO8y41ZQ&a9NQlgn4YC9#+_bVqo#?jS{>gE6fKldD;@F_Ze?6&b-+Svf}MrPH|pfVzLY`n7(=L#U=$XJ<*#A{IvC$=)AqS84p5c8j%D4W`7t-4xWIVPL7WZ`C;nJ z5IGKye0WAt3Frfe>WEE39B?5U0X#k5QqC@nnU-WYV#m*Xz5EV%^O{W!4SH~!j_*9C zk(PBW5@{Cm%xsA8BLqe&urJ%#cwl#iQn_Ps8df}JMq)o$s7~?pofSDFCYd%mgxH`T zcE~~cM`M}f3JAKPa6aBDkkOcAyOTDgbcEYIbl`lx6J6nslGFzEVtr~}h z)#!fS^-?*nope7xL*i53H@XXid5)MmU#A&(iNLu}sL8JVh=-OIwD+URbNeO^H{w}v zUEGs?W@r&)&;qAI)MmpH7~TVg*`BZqSWgDk_6D1}+}#n~h4-7P&o=XhxmU~%>45)z zC5%kAYC=5W2p>3Y!rp2SF`20df^|W3ah0vbw!ZPuvS%d0lldMm!e$LsI1 zQSq87H6bhoJ|1i08Uq!Au1Kmv<;4oC{xkc$?WV}C4mTnnbOJH9)~ZL*A^8SNh~B`0 zP0UvbI<7?Tf7=#800{8EH$MF~!`|M()Xu`x%Gk!h=+FPp$v=_%`(aOUS+B}~|IYv* z4fNX#``-`y5C5U}hq$#hH;h zL47DfP)@NU)QI8-_9c`{EXebdQKphq<8chdZ1X>dGERL4^b!Q@U5&OE>)% zcP{hMXNKg=S46ya3jLC-B$mexcp< zI>~VMNMzv)!CBS^C2=2e+`8yw6z0iJ*1GvuaWZ+e1#!=Caxkpb>NiGr1*JJTpgmUw zducq7ZE5Cf%~qv6R3Lc%uz%Vs;8H6!ew_QPjrt?wQ5k;iQyY{* z(kxRl;*h@uW3AbDGm=)-Ye602Wdy=lHLVC2w1>MW+SuJ?&sz3=dn4H3jOir6%=W11 z_N(a|&QUoDtE&VG3jzVwh+{Y3sr=#a3QfWU$f~9A0h2j)hFEUK)(NPf=?ZOiYn3#6 zQY0zy9}5zu?GP5sPZ8Gk6fR4~d5P+szI{&vt&9VWq}p?wn=6&D+HFv0kiC( zKc^!7=~V+?$Kw|g`=W?IzsdGCxEbURK6hswtj8K=eCze2wjQ|@D=u?^nDk$ZrfX;9 z&{7D>nw+&J41uLtv+v^drT|rA zym8L9l3JzDf@hWLFqG&!tMb0ue;bst$d1-_pIPj=3%>t|wSrdt^sVcM1-`xXjeq+$ zvJFwm_DAd_%PwDjC1p}vFW@)una8hc=odIq zCb(yeDX;d$xl?C^A|QOf;N8#<*6KIRkzgcYDrN>%&QFWGPrV;gNt&~yTmTH3G^@Y9f7&}yBf{S-3~da91+Db~XJ5-g3I z>sFKm%U)+qU~m>#81(3D@J%wf1M3TCVuLtobQI*zDfYWa?Q>%d0&_4%*hhY#E-0?; zc}~UhmqRY{jPr#NsG279f{XBehfVCWsl*J7n+681A_?4MDP@TxNbBp7bFY0WJM4-PY`gikKyHVf9eipyJhFKWwHK{PhiNW<%^P%Z0>5Ttx=Tw*l-5k<8R*oesj zJP?^}e6b`9*pxzF32TQBcGFB=naaYko)~&6gF?^dtYs~O5{lm4?TA6}K{n1&jXM@M zVxla1Ctft|I$Q@n0xe#CG+OdqC)Uqiw6KL@4xFFA6hcO&?~<5Z-u#zY545`_ojMI) zK>m`9f48&0Nk#`(8$-SSse1gM{OqjQowyZ%@`M0ow%_>ke-8R5LmMft4iJgxp4AC& zI?Qw{A43zEf^I8OLsi1&L(g$UrrVkxo6zx+}Onq;@_O5 z`B)YUZ31)&vukd@oKOaHmlylB!v>#%?i(yQjd&NR-lw8?R4UV|RE5n^p<}i2!EL@B zvw}Njbx1DC?<%!shL!;{q?Q^rZHP~(^@etAwI;GEZvE85S?O|heIuW=6OV=DG{eGs zAz**qe->M%9|CXE&*&Lk%Y5=PK1gP)ZI*tusFMJ^6`S37%6`b?;K9p)mcmZR`9#Pj zt`E0Rbgjm4qmjC|?)Si`t~rV+`Or_5o|P+i9Gosbp3wBTP^rhBoheDS&28oCKh>-H zqme(y&{j9gPc>{-&C(6rOGGAZ)uC`hNpFU#4O>y7oCJ$zbnzo?_)Osb@)8`{#`e8_ z5Jz8n?m3EBCviL6wmKpKmT_z_a0+PFaXW+)bRUVy)>cHzA}?kyN+q&6W9)W2Z;W}s+DaI`sF8WsL@3t#Cz^0xim6vH8Nj2(IbIvcw^RAY=~b@-7u@8L9z0EfPd1O;aeeb@k_k>d}k|Ou#*XSakwcMNSyJ%Rd zGXCDHbHN+cfE!+W7=!PPaqck(;9RS8-K$@;=<2@@s+KR>-_vW;cCYc!8co)8O@IFq zE8UHsI>!EU*0O&C7^OwiQn!(Fly$Utr~U5ms!w}eYh%szEJWFY!HP-7iTHJ~{?mH& zC^PM}nuwrS(E!4>y9|jFFHA}6O^^hZ1Tb#Gy5^W&rCnD9+B|W7#M1EX<-I5NH(7XT z{}$M(bu&O;8!h^Ql^Hut3>HXPA*43rr%JL{0!dzbc&ilL?Lz3UGa5b6ju2Y*134dt zC5)hcG>;WZ+|BSA(@_MlguN>Y<#&7O*2wY7YbJT2qa&Q zbFhgV-cUQ2(_I_BTq(5gW@t`Z!I?`*kL*JlJe5ZWy7Z}B86kE(VrYY3tc^es8}=Ds0!E42Lrs@jAg<@u!?+^PrnJQP2jMHRA zW0iHBG?3eHXrKg^`WhoHZ1=x49{ycg{g#tD@%-#b|C|)IyuIXD&}gN*9$PMHJJ|K6G@cgiZMYC9AuBL z^2#x-fIM!RT&o^vxRE3G(u1feWUC(ml^qSC|2ee``Q(9F9u!0m7+9d$WbhbW5UA*R z3jG7hHJ_Z($BbN`pUr-WEy~4V8KJZz=<9F}59m{KH`A|$cs^|9;G^}(#(BOg@w)~b z7uep7Dg0AfFlLy=qB2E-anYMc52jWVgRr5icWr^VxLqqN&L;X+9Q?gsC&e-b%>k~Movmnoq!ZT&P?piY<`?V9@(S(z5LpG{-^@_FQ8K|4s#ii<- zSWO7!eDm6FWYI^Iuz|O|*w9BLQ~Y%!!EJr(W1i#=h_;z}O8pYpi1Tjyra`L zgtn2c18MIzNZ1Z?WFBYJRw#y;CV;4w=b#e$R>cB0NR9B>38Q#;~pJIB z=yZ5Mrd$F5{NK#-6q@^OdP~x-BSFPN}dBm4+#~;$uZd#uV%NKP6D{Tp^^G)dtGnM+K9balK z#_N$Jw{%ypjovWpcH^m>xHoLjXPUW!_hynTXE>8-^jn9zd}>NqN_#_7v4ee2(G6V; zXAMsEo@tbQqBRjG#OD>C9zZ_~Foo}UNHIkiuAjEG#RJws{MJ$U_q+Se1v;Br0ZgF9 zzcwTNpDqppQuWmtkXPvid>DUa5&wM>zpjCStC6XN;U8Or|HEaKE7{sXJ4M35T$U=9 z?8*7~6^wNt2b!S>qb1`nZf>fzxX7BRn#tOldGN}52^RW!xz=_i@d=5F`X+XHN!j`) z*82JqXjC#39o?@22L!FoWW-}{(5 zmEr{|N`(s)`AztVcnTtzp9LSoVg`u1@b#%yZ^pQlP{7*}pBlE`*=I4+(sphS-ms zH#dNMBQT@{2JNmct%X{HAmGPE+qT|Du*YH7t(qHD+1$K(Bc#nLvZO!;aMo%aWVGTY zM5bLnldcQFC@WQf%0m)K4Edf<%?a4l0~2gC5NsU0lAw=tya*YB7X6GeA;FFG_D-au zZrWZxn`zFL4tEPSTa!C0*4-UCZLc|w|M;RJN6}Ym^NYj;>K*RGCxxP8>Z=6yiI0VH zEm-0Mta8Qz!=I+nuD3cGGS}GcoxgOR&YBF1AFi~ALdkU18cS`gR(tP2(+)Nr|ENFv zrRp7;x=NP$<7(+(#1^c7E7#1ZyS<(ulJRlcX)u4-Wc@5{FI}sCWqB!sdCJAXushIk z;|I?*&(q1vw@PrY$*~*;;>$uPOY{9(+2UoOiDF7~tM;nBy>H97)--Lswt2uWXb`kQ zgn0ITnFB?um)XX%-qpjuPA&#UnwRHL_y~y#NKbAY#hUwTLY3 zFWG~1#0IP+#PJstzdqK_vL~ylLl-092HwjzcSt?br{BmAAd4tA(Db%4Tq_?dYZvW8 zAQhR6o!>T$@_v8D*exRktduAnFfdY)7qOj_z;td|2YE3LRBE+1_TG=tf8J@f>~^ zL$#*!oEdD@2n;k?co0>AMU#zLtRGG3HczQsi0!AOJ~pbY2k)#cB7XC?4|$o$PWqzC zPZ}`9@t*`L_dZ812%7p#L4JT{u2y3*Zxp-8ZgI^aA0D_KB=^$L#`Vcz=e+NRP`cmF z_zt`9`x?%73gGW9$_J3`hxZTj|0i%;>pD7^{I&1>KSXYK&;Kk6e(SMuwze~{u`{%{ z|1Tr~G7OIMAwUvP0(9_iJvM(G_1^{L!ua9$#@?VaH^{!^jEcra9AGZCT7O#6Uuv*n zdxQ}cwUR1%=y7o|+@2WU!#h6FLS`~u*#=!rP9hfSu@~=9)>Kzhi|0}=L8CN+m)fO* zhl(}UE8oO*1%jvUYJA)#CxXS0%ac941yc2=_75C3#>XK3e528Bes>fB#4I8SQGn7> z#zAUVI`0m~w_LFMpt-`FE&TD${;Pk_Ny8EKW!!Lv@z4#Z_dU*QM>J{vG!y1s|M{W` zLry{4D>q)$Hk#J(>6+&)w6|WY>$dltoWZ`!U0Za+02_&aY2QQElSp@$ zQt7qafiJ!n90gWKGfnmha^5N{vrFQ5a$e(R$T*(Pa$m5rcE^(r(nW`{%B#+|dF&-( z%b9dBi*D^v10%zIf)l2dlEIf&Fvz-f8jweO)_wv}?=wr{Pz-_?V)=wLES4~*VGk=< zwkW&Z&aVC|ttpgJPrhks%JF#}72f~|GqOD`z*rULEyLAuFm+jea4XWcqWc|?f3o^} z?u(d#U4b5eaX3Jg6Ab`!D`QhD7dl;i2U90Q8&gAlLwnly$H9N7lmF0%F%Nj0I1A|P zscv}FO*mneY&%ghA%X=D*z{>w_WK4gYD*faI0$=dFpEsnjkBNM@T;y+S&yQQ@ofCZ zS>*Noe*LRB&2xHyPbJO#UG zelg^5A)DmxX%ACgG;agl0vBp&6XxCpG-Te$n$UCseH1Bb_u@gwyi!ylZnoj$EmEGT zzgQraB_@uotR6XJ74i}Dt86gUFpdD+s*vQYz^B9`WM+1H#xd8*5LtBskO1{C%I}4+ zaofK}nAVM>` zWzEaf1Ev|)a8pDeBt|%FoLf0f0fx$d<}T`!yL1L!(Ro4~4dz&I;m$JLuGJA#f`sHz zuhKN64x~a}H#H+TzYOq*tlqCre_)8(1}Q)-o3K$VK6}&0Wz{GpaG&8cw)}h>=N?ND(>mF?H%q;L zQilPb5? z{sf?&uDzk1we$Z0d*b1MsMU2=jUK0gvp+AhaNSVY5ZqU1np=z#_9aTtU^sE$xPaj0 z1cyg%rUuKd5?bieW8+H~278V-FI+I^W?_VocwP)=t<#|v$P1Ki>wVg4qf!3H+`zl| z0`6jhFzo+tqOr9Q$KoN`|FQj>d9i<_-qubuid@Xh7%88)A zW#L|FM#Rc~7MWv`#^#rz<$OXLRVPy@mm?~Vz1AS7wrU?Q0cmo{>s^FAO z06^M>AvdEjBdQ52ceH#?dSY>=IE951-7%ynm@_t0V*FHyEP6fGUsi@4)g|z%bJb=2 z8bf*Q5b`ybE~K65VA|N(=Es)wqETDG_gffq=NF@uw_nMZe^mGW0$V}G5XvrqGdlo2 z=AC~31vYC33thc`#9E@lwAp)BlkOUiR#nTA-SUUnRa4&&Tu?*$AG6;Y9dN=&;$p3i zTJ)c8MMc%>0tzy511`AkGRn901tFu6Hy3w56%zmXXNXyr`&1 zvx(y&aPw7)fBWP44|zn2qr}>@J_32tSkttWM5A2`TX2~YwYZy>*bJ-9axV;-Sp=pS z7GGq4>$H45;v|~FrVrVkI|7Nf_9b}VS^3mGT7yk&(?Kg)6upodz9Jjg%t{ zrPnC(M#BxvJX)n-p}R`gU4jiH@U;T3Wy>B+Yl-jB{x>_S@K*9o0`OuDz{`7O=-+tx zXI4y9khET*hwel>@W!vxuxz!gsT{Zb6m&wLgHEqg3=t+U$ak$PB4I%{fQjI#`S zp@svp?BPL-XDSSh1zpqG;p0#+eGsBPeHEzuG5V3U?`f&|p%>>)=QKd@hZV0YRl_z@ zL(4fxyQUwG&wx1ZGN(@T*h53_-A8M|h$;9+=Lk*D7TW6a%d(En_R=yl*t5;}7%B%y zGd%oSRIEsxpk!j8L|AeN56ww!4kAQrnUc9hG(3nR$+kfmp71kriHjScBA`mL9lla} z+>51PDUd))b$Tv*IrKBvmxGZ7b5&eFGPqfCtOhpJ>wL{1gMzBKAP-L$OsJN-36j!F zHd5jHosf-8=wFL?I)=dm$W;5Bg8>aHLqh2OR-uyv6cMNhKZHOQ9C6Ht(rTy3NGL15 zFHd>d>+ta}qC_&KeEL5Zza;8Qrz>1UbcwCy&benYoh z^UOAW9g(=!LMKltvf2yML-~=f);e2M*b|>g7SZFKW-8`uVf~S(hv^%>d89W+3yLZJ zQWZbceCf_pm<_}KMWI>5&Y{1Q$0h9G8Y>X_=JjE6=S8LSHGQ(irvcWKa{iY`Q^MJA zq(uZ!O@e}C2=Ano4j`?+;)uSkg}$}rUvcCDkam0j{+44@Ic?~}LYlSw(mU!|pd+&BPJAOWlItl1#Z%ow_b!&0 zH|pmMxa==K={CQ_X+#>4;N%9SvtdMA6)=pO2h6329raZr$BEk}jwF8^2A7&K3E9K% zuM?@9uL)h0L1aE<2}>%pZD_d97Zk2&w1d1n{x&*m9ioIoiK4XZ!0|J0Kqf(hV(t^} z;HH=z*fe9a?4qNrXd}lUfU^KvsXRn>10N*7Jq+_*D-mssIP|~86iCl%7VYS0l3PI; zFqQ~px#G;SOipwB=yOwkDZh7W2il)2nZL1UlklAUQzrrap_BNIUTnyG3uU=)#Z&;C zRS)^p%(l)1rg~1DZVeP|P?&RGzPrpkAa@_qhZgS!-hoa)cm87ZVpSi$%?4B&XvXMa zF$-683)$XdzI?GRIXpI@PO18?lfK-qqd)6|It#g*n$V6Qgf(hmCW!~q4`I*tvDZUW{w#51ij|+_*&IB zdACWH!=mLvSK1jtUoavt5iq@6y#3@eyhnYLUyIX>Vbl+sgrrxpm9+~tThl%@+{tX9 zb0`qXoVDd)4tdOKKXBr};oqH3w^NQhc94&tA#vY7SK(Y!xn3hR>#X@=OgHjdW$qno z!S1HVf&i@1L;YD){xb%>@AGxGws!bOw4Er3zpL|ih1A$tB-&iahh?kwhc^Edij;IL zfk+jF^J}%N#7l#Z zeqXsSW28C+XZ{lX^YoxirpPsP^Qep=sR(ko(xxp=dIg<`O*EeX+pJ-fa9*h&un|d0 zp*5X8V`pIc+s|T5oZ97)u7PBKYP@bfHcOe(oT;K|0yGj(!hNya37c+E zA_#lR`O+a5v=p5YbHNA}l)exfN!`MhxqKJ8W(LWp4ZmypHCKVAh1#Di{48v{LrBuw zWm57Du&DIvs2fOKDr3Fu^R|v$DHDVnV4r=H3@Ew0!3pBP9Z7H6I^%j)$+g`mslW4F z8h^5i`tC9-V=!huvJ}Qiq2qAYZd`~jvAJLZmj$|f+o5vOTdzPFfNL8g|2@+@PIG`5 z!^Ah3R`Yzq?qpdU6psAaVG2|zYt6^_UXRRjX%ete)1Ka2Sc?xyz@ z@_!-{kZy3aG_W%*)L_Y{T3^oC-!DE6MP+EmYkDbuH4j37nSX0lpfhzxCSFP_h*5&TaP zs9svW-2rhWp^DEvPNc^v@pRxTJK}E40f`O)nCssU&yRu>id+M8Vpd_5IUg8IBR7d7 zgG{bZ>B8q!VA5S+NMui}@=a7Rg~h{pFde>OQbn(KPiSO$@i&;smfBQHmJ`uZLJJNj z@@Z}!;%bPV;=Ap!n60o-mde?u9&UVtwqGaLo0|s*+Vag)vn}<%{-&PV<7k!*vVsLW86T-r%xxPP;nuk zLOUNoL|wW!(YO?PT;8vuZhxRsjq~A2HA1=Hvpv*0?pnh5b37n+3}0QP^p~ce4@)yb zGjb6<;&S6FZ|%FE`)gystrImSlUx@v#4_qO-0q5K(yLAuc`-HZvh-JI_gvqKa0cvw@>9P?`<1Q%}Y z=9T9u_W8FlbMF}Y@hIY<3kVDcfWYt<#*9q>ug?A%V{($_fS}L`_lPGj-00YSAZCxZ zqATOHY&zgLH!xSrsBRh;b<#X3s@^s!=add3By`5bMeORJ?r=WS>St4+WD;VW_ftH5 zZnYez&%qAQx$5>q<(FLFCH(a_Pw{;r18~e?y4r4qcBEA?-VsclLCA4ikw8XzTxbH4 zk(Z2G^2RK};K@&JKLh2#UKXvzq^)}%NzB$((A6QzLicOv<0BlYCsvbT?ji`j_z@(TCP3vI65$3_}{fz4jt6(fwCnZ zh*T;ZzyR~dja4L;V%8(dmm@iN9+JPt_OPnF2|<>AJY2=MH!e@x?|WKsPT96`Si=k8 z%!nSCReeS|iggJiRPb4$w=o&?g1!0S{x#R#847$fc1cH#IFxg;rM3#0wznV@f0+n4 zQrW{%f_b`-63Td(Z^tkU<8`sq<2NV#epO=My}I!L;UNBhg#%+#2SeR|zN&vE`~Z@o z@DE9m6Uj!yXY*C<%u zrXI>h{pv`R>rFt7s`p40s0x*|$);o)j5?1<>K2M$4I&!m@P5nC{4B4|Co)4AVtKz; zTaie{8nI*<#Q9fJ(Y?YOCg=N3Ug-*3Gl}yn;*n@Q8zIgLI`K>MO7k? zq+dozR+$CJK9qkNX)LqwmuT$DCF_kO%^nvg%1RQgzP-RVfH_b_ze-eUY6 z2oMuMTkVsm$2!St`xw*cZWaQ+E7&rgPMlj8e+;A*M0z@S92a`s8lYyaS?~0TVm))a z%YKpc`r}Q(Ut)na6^0uN0JLxb&`|%)JAVL;#=s8nuJ|vwT>`w)`}cRhl<-G>Iq2+( zO31X@bjGq&he#?M9y>-!J%&w}eS*05=tpmsMVjFGiY<2=B??XFPm$P4SK}{UMi#-h z5H!y%Ksa4~wG; zZ0p(PYKdFrz5Zr7*JP<6S4ithN*9H(*CG3@q#Oabk4v!erySU@tcBt7tGCEZ@nMzz z4|U_P72qXe%2?wpo#h=sUfEp5%E`Ms{+&g~BZ5)S^8fX<#~!KouJ z+i3#|cdXr-`cE^WStZlQT#^7Sl}Q!I$X-nv?aL~V&Ey^!rT#to;I6*b%A?BVlGuX~ zA~Xu|TTP-HsU!7{zQs$I6P+MdB?c7Ea$nC6IP;<-F;=loe5Vw`h)~SGz`r}CJaF-{ zR(nepvB^6<%keJ#PN&Ud_0L++8*7wN8^mNB|4J!mFJt3|LEw2X_D{{A3265V?@&J9 zf9N`++Rk;)xaA1uRwT6pNLOy0QYg~n@T#*&%hqxFV`F+e6wjAiYMzwGr9@s_KziAp zOswMEZ!#IcKMaPe5#>;iPlsQ-#Ci+3+g$2py<4b8VJe)BE4DM+*ItazhW@5Yxw3tf z@BvUu1VHUCU21G$>Y#7pU~2QvK>J_DJf3tk=r3cQMLL9N2^ITyW6tuYF~2v6K83(} z?(jPYubhG}zt1vMavCc)4mjC+mpP4>0if$Nv%1G%l#CsGF)7U7`G_8%(Nf*G=_@u1 zDtryfrg&gWZ9x}CQ4$0iyG7$%O}WN#UQMQu%rh{lo;ijuUs#F5Q5&P22bxH?^7>Q~ zG|ZVk;Vy2f*}@GYIg73|iHat24@$ftlI(R%zeA0%kBg*nZ$#iw*2;3_)!l@l;gBxA zHhO}|`BNQ)0%0=mw~AqV2V9n#<?Gu`M=ckPj>tSY;PC~pc{u`O0Ij3d zscM}(Y6v+3z9!;fQU})rdHF~VzI-_mJuWefMX5T_i3q<2rhok)+gHiVias;i=$*CS3h!O9S+JzDxH+)VF=Gb6Y zb*?3yO-o^5y6@*5Do*XfNNvw#JvCf6zLryoLc?Ow>}ND)p}e1BxwIDz?|wW4uQjs0 zD%`(KeC!(T^PpdI82SxNU&Q%W+x(Wf`fK852F-h1LATUXN7|qR4O?}>^sgwFtL`a{ zg1!sAph_z*8&=jV<~C9=TAI?i-kfI`5;-KQBG$Nk5yX0$VxHjp2r3x6$FzZk*X&d7zf|aaCgg*It6&T2J4zINub4rRQC3_4 z@0-i3b*CUVZ=V9cI&s9D%~^*JjF_hg}R% z)bE44-kS*jbZzE_u7FXUy5|23w0KGL)%T*l?{lzj{bJ+7@J!IX3i)s@yqcJM+0;nA zd2QW}$v`b@;mb*4M>@v~gQ<05PsG&I81>z!vUD4{k2smImenm4&iobL~@-RImWF za@OyM!@N2$pE+7IP=gs^S{%xRWN4IA1&f%C%&+Jfw5V9teO;*ImA+c}`-dCCqS*5* z^NmqSe}QvZB&@aYXGeXw6fct2<~6`kYyZK%!Jg|738hJ4K5cms%rSx~h<~aGuelf+ zX3iOiN~HQzs4s9LJ-uhtnLr8NEmYt8q!`#!+(-B(KQ3l?(W&5+oO;=UA?1fi zvddf`DU7e}*Y77%EBxNnp9&f0ysEww(anc*Ie7QWn3>s8U3qYTujDO4ZqsQk%Wy_t zpdvqn;)IJttDJ70*j>1_O2rWyE>q6#)L7QtRT0MW3)D5$L2Vn6UV@6p4(FhRpPGJg z^BP*>G#16l@L(@RgefJ0wDgGt2$cnf02UryPg{EPGuX@nbFf-d+V}?>%Oue{sqFbr zY;D5X+%wO0SLjA=jOi6ejV(jxG+kh(Kc$_?pm8aMNn<)*@qWNfG|%kE+?4}yCf!V6 z3)qg`mY1iVcJK>J@FvRi3jSDh+c$Of@`#gR(*H@AlF^U@${K|r*>b@11osU37p8~- zYNX4w-0K*Sb`ga8J@xX((D}arbp{A?C*9<4VLz|a*j*)y7-FI7>_W8$F+@-xCn`74WlWB>*_ z#Ds(t`T4gu>hmDpXxJSzEXSB-mP0Lai3ZM)A$n)$&unSTL?%{8GrioBwTzQ6?5*0? z?$CxZIOHYJ2YCd+{el+?1$O2wXxE@!Q!uW8Jvxi|OlmOUP_oG07dw#XWG_U{b6LKe zs}Ro3`NU9l9%LFMOn$_{I^2u;7;!@Vh!dQQ z1Ttu9jlf|ggEOrXl8#tDC^lKyFpLFX?tMm6D+vWs<*g2nvX9rF_DL04PK499=ST38 zeGb2f%21kQ^WL3O*s_bPuX*x@BiWe8HGS<#G=&>6p2Ay;%5A8^k0tac*i|N?2iikK zGh!mh+K)&x`r3HrDyF~MwjNHXF!C9}gd%0OUh*h{I8(N3pHJ5=VtKi`q~X)jf!Q3n z`Pc2>pu<-ZRjN0X1A-%kZzDZCpW@|PFd|QG>RUBH7ki1>0;k7!%&tf8&&cr-AUry) zdnwT%sqS_7j8IVKJ7B=0F_I1$8%5DG3o%*d6h(<8H`loG4F!LkBVewauPg7nZg3qZ zmjWzfh-fj4$-`uJCVCY^*4fYqSij{=!_8o;RL0dnRul}Bd!?1&`OTY1I~Io`^%xC_ zfFDJQMl{+g`0P^h+)y?sW&8F^7urTa8|Yc-LSAPh%%YpwxNw4oYvb>NTJlMkVQtnDn4UwT|te> zdDeap%Y{i`0FvqJp+?U~EV?>?9cLKBt37OP+|78QQEnC_)aBTHLVYl562aJ^P@r(T zW9oRWac?OY@hQ!WlfUos97)$dxCsWjZf*yzY8JShh`L}t3i9(2w#z)edwb|B1KDsH z?sL00+e|lM>>GyGPpQhaMt7&YE@$^%q@Yu{UF9xOc9L-Yws$c=BL1S1)Q{3i<8K$i zUOcrL0V4hEX1UPJHXcg6q#vm?D^!xkZsaAIBi$W}3ku6{`2RN~lQA>vAOs*&2yMuvyuVJI`)XW*+*k5Akwm)QO z!07&~hMi=U!!S+-;+c;I==Ez6*}X!r1|c(L5{UQWp_bEUuGl6480GfJJXcffaT+h{ zDFpKh>>KZ_(l4)n=47;f&&kaGEhqCip6CCm3G4^}Y65R~@zr0sMDM~Dk?Ah^lpiRzgl?>^xJ6tMg1 zxtywPJxrDj2I?(1KKvRIzuB)4E2p!13J7trT+1NInx-O9y9g9)RAXOg!hmZqvKw04P+~hjBoiz+b3a6}I zAR3$Yao^)`f-Pee?dDn0#|O$!NRK?_Leg7)8A0{#(f@BFmF0{@z&ZEFL49lvftHu}r*YmCvT4=T)0)RiuYoYH!d z0959y6kr993D3>Tw^Ga*O-GJHE@UHTyvls+??0`EQ1`GSs{W_Nn_rlB8EB zKeC0tO8eO>L4LeB_y*JE0;C}#`NoZnW2)d>V&+K8FeceyaCad=daPo6h9D(eO(rZw z&0t9%Twil3pE|&ti z@{L?Jy+uKlxj#55dE?EmM>`jO-I7+>wdWp@D~o}EUR%c=mXx80phcfjrBaF4EJ-hW z*T7tb2sPc&cZv~^{ot#iK9j`k3e;Gd94CbHyC8;EXNwZzpHgFk2W>p~ATj*^wRV+Z zSuRZ&L=b6dK|mw~36(}lK)OS^yFnT$Q9>H&MmnS$DQN{MDJd0D0g>*K?|I4hMf~KP z>*$5@>)v^0XJ>b3XYL{Wo*~NA36Jd#g^aqy7yfxT%u+N}P;yfxN+`m`$#*8*L{L`3 z@_jI=_zNxmy50;w{P`TM3fdI9PnfI(FBEwLOQ#B8UAX+jJ<~OD#jm$jP+-J{7e!bK zl}{5AmuMNQ(xlm{MKi?Tje9gT+U2dh7x~8H{bd{(-q9`}k$SRt{R@dt8$`tFiq*)F z^WqBRC>&}7a&n(V@zAV{8nYu^Y7i2UNFFrVlAjmIloKV3{HWfQ$3RWJzUV|!Sc_t9 zDfX7TmQx*Z!p&AD{st_sdgVOtO_>h>zlB-Q<@rhDmtU2IeNcupv;|tWsjppHbg0ry zaJr)36#uS+wWZ!Zf;HkYC2wAtz@`s8^felWfbthAv;}(KoWu>}HnEV+hJRsh`iPZ2 zP{d8&%6CJKzW_3@%w1H+RnPc0&Y;1~Ld%nB&qqGA~ z&V1MSB5z~fhxcDZKa80_-P=%1dOa+%Y%*jtnu(1Vc8`DzB`Gm{I$+s&%F z&mQd(v>u*id1-bpxH4+JkXt)^Lc84Bd?Vx|2%W_{tC!mh8CaHGUfa)#<#k-TnLZ)> zxJaP1Bqh<`+Ma2sMI(kz*uqEn&%#j;Y*BX_O-7Cj}d58aK2BDI*Tn6w%M zK>^`U!wVTRfAKJEG?U5ez}yu`$%buJ3W~-V9u0ur^+1(n9~*u_*SVhCzlK>PINHDc8Ppc)z(}yTq(6#9knjweyFi~gXTgZo6dzrq+baI zO%Xh3-ze8g+V^lnX%#7bv2h24AF5{IV_6-BxIK#%Q#2 zcHgk!-Pr&e<}$A>&|zS zAHas}4sgbtxGiZn1&Vjd4Qw_UfK`B{rN4x0Gheh_{ap*CN zJ)`{U5-34Uf?F&E>OTE%B^%yN+8=iFXs7J0Q`cOq|zU@@r!QZjwXwHN$l?zSnXp`d4+~X|b>$T(DBT z|1OINPm*lN!rfN`o%$+uKm(?ts4`OQl@SMwhS2tsM`}-E#k<-Q`L9-5WPI26PJl-=x)&W?@ha@gD_i~Bvb*d24mQlx z#p0d~-N?@2H~Zei8}Xs!6)ra`A&3k0e;|~J&5dWm{)DRJuOlv?4pW{%_A^><+a06r zaad{hhgbCL{QROGM!eh3jvW4zl=4&31j^%1oC!bc?J>LT7~)j|YA!NVrwtBnE;w2K zGHPlG6vvilh?a^nb&$!&+v^9g+ zfW(GUgKir7nwMH$ufH-UnpNk$O|fHzGeU~8#r=@huGf6iR9H{T@70x`)M#PyMRycw zA0`iE`T3_^_*lT+gp~09adz>Iyj->s4NLBGhH)!l^}*Tp&Ss zc#1s<#DjVpoMXuxl&&1VWIsK8$u{L4cmKu`(wL=v$+6H7yUx62lj2sNS=oS1%G>^` zb#@o}Qr_(+*e<8k}+;gxGjA|JA!BC)raOr*wLXq1C!jpz4knpAf{j~?qXk*TvnjXX4AbF^6yZ2+ z^1bK3JuSW{r8C{*BAxEBjyU5O+n+!3^B1#YwVE4nj0N6%strybV{1;1vGMgq1OU<4 z@iErq9dL}5KRCwvo*ZL$jW+d4ybtm)=|-5QFUdGqseYL?ahPn@Az$)B@tJ)WTW0j-BH@MPN9v= z?v&=j8@7zm?C%6U_~7k7_!J@XfkN^7uZu{N%Bl)nQFLa_eOGs8t*@)6YVZ(^)(k%9 zQi=Qq*I%ihG|^P~sk*S`D_G7i=oG78MUDcxEJXm9r4!$5vELctJcb6UQ>i|vQ(+@Q z))kXb7IvhduB2I!B^J?Eeh{u=SQSuKTwA>)AdF|rn_E&RShcRlZ_d9q72j3Yi4Pyx zg@9b(L!_Xm+fwzI|D!2aIx|dm*iD87CLymJqLlXw2;52X9*Rb!31f6JodVuiv7wA; z5|mpl#Eczp$Q8MTWVeS|)I$4v>c;}%I0~9G?>6NXQG9mgtum3CF)%kV)}p=T2c+Kc z%X~3J$;Dbn<9dWsW{MvnJ1x?>k|(LvfiLFw8c&no{$+W;N|fSbbmigFJwjh3-K`m? zpWPMTbMG^~5dKP69tmVobu?hhRqochn3v%%sG+Lm+?>Ljf946~Oju(5d9 z@%`LniFLH^hdC4Cpqz=0{hSHWI$39_t-9CYY~F?Hby_+r9PIf!z|$htvk^8CY;4fe zB5eBr=xOmG@U)1yQ#pMu>-| z+X8CxPSU9MNw-f7Z3T$qfqqWM@($myC-;-fN1LfU;n+;~lgkGWlgnR_@g_}{^R5HQ zjv(Sx}hsTOA$^;T7+GIb?6YRe(RuZAnD9hd&G3xp8+!ujY$}&|F2Uo0RJ-JkG zpdgy<5}A}U0pFtKMy~v$DkY-hE2@c=VD^Lf&0*&|0UA|gKNXcbH6+}Lr&!f%ya$s z#k=gk9!GM#*PCO%>^K9wMP84&uPse&x^yk)*+tsiQi)XYU@Vq&I7-@^Jqfz&guqK= zlt>;Ik2~oNRl(|+d6ImF)`Ut-+u#ZK&eD$@lM`Y+#8&aTj!MirKq7k4x1qjf>2mIF z1tB20?|Bf;hMvu$kquVMA|6n$_s*gxW!|q-z4Fa6kX+&aPP7hq@ZK-jOTr)5?w^@7 zOcV-&!8B&40hQ~e0Lgu~$C=okZ+%3$iSUUdXX9&mB!58)t&9AUYZ-wVs2mTOnp&>8 zZTxcCBrDSBbxZOz%K_Ms)u^0F`Fz|f)%9%Q^J2R{66QW{CO+t}!yp~j&(>B~Gg~*^ z*{g739lh;JyS=z!_G_Q;>>wuxF5JvR&3!B+NIb~Pco zPj^3=H?EODG1e6*a_W*v`47ZO2nRA;UPy%|?Y>y_`Rj19XhJQ0e2IqX<4LEPa#^gD ztkC|a!*6bH3mTKsx!i^^9}bU@9lUG&$kIrmtvUE*E?hX8iT8jUUIZqWep$`zUb?iG z(IWhsdVa?wy{EyY>zxS?>?L1fAzX+m@wR{R=z7y=bGfN{-qR1t$iy;k2)6yib1%0; zs5~DJbgYXS(Fh0KeG`u9m$afCgknrYq59NEo}WNuoK>d6Euv`Jcko8%^3$v7CdrzE z*=pOjZZwk88S8qv8V29nFuiHy&2q<^BUrz|j-w4{dE8DD%Jc$-mP@q@=7X2Kv*y|f z1&PDWvSrhkf|#7`8nh(jI4ylzP$vSIIg9BGx_14~l6AN_n0XG|5E@0(YV zpY|jJttz$Cwbi>BVtz%KbJhGJg7i*5j4qFZ?kKFl9R)<=bPzPOqW`Pm#62_x-A{gT z8t#pe-oi>tn||4Yi$x^)4o?8Cos5=Ni6WTcvMmE<10)@xpfP@ zu~9?&@jxre?MpeLX?N4rLU(%=!AAK$o5+)})&#o4Su<(CRcx z*f?K@Om|fUHBs-*0E%`vX?XU#)|W3siSoac5Ro<>G@CvU&rt$VNF>n}1q zW(^kH%|(3V2&vn60?4>XEd$T;lJ}r%l@K?!Mv`)*({yC6;dJ zt)(nSN@Z~=>#C%fs+_CAg>4fKMy;O~)Ac-bSr}ig(#!Tv(nr9huM!|VGtcX?HtxNh%d(V9+oG^F|AB6ySKv%UmdwsElE7V z3g_gzA|Ik9eFt$YHp-$v!9A+&8|Q#<{pB!2usr!>N2GAb zYuiL%yeC3MJ1omY=HVRTDp;y5$lK!VJY)x@r6mB~_N2o3kdXYZtENx(xWYMBg@7&4 zvrzBg#`3gTnbQysgcUDZ6X_|+)0uv|GiSxt$hHik+ z!-bt$fY-l>E}S$3l-5@<1boMKWtw`NmVlHjnpcqxg`SJ=bRR0$}p*Q5{zO6xC3 zXzyQBybrG_s#*}&6qthyNN;l>1JY3xlmXdOSk{4Awx5>PSru#;GHF0yqERurm$#`` z1UK3Hvd@T4^;PX8sC3@Ha2E*dRFtJ9LV(K^}wqmfe$52 zS-A_EeH%cR6xM@FN}Rp(BU)o)6oQ$8XZ;PD{^_8|;Bt zJ1P;ddoROm5M=n_d=fFrBU#A=Qt-LphSEuF>U5l6Y<2G+a(-Q0zS6zE@MbAFlS_5_ zYaJfl9XsI2bK9b(&tuh#azd|4_04AqSRe%-V@v0||#T=jz8V26pyVAl!bEZ41V^{dao{sWUoA8LDc?ADvOMEld${q7Ge?g!kZ)z*2Ik z!ln|*)w|{>Go+vQbF|U0Co)Kx><-C_D}4hOoo0Cx6jw**by`#8d*LNNzV+i=68erZ zX0%Jm#l?n@P0W8OhJh>IurXDJkw+%fn=g%qT-l15ct2yPJdZB>$q3w~2tzm^V`zYf zno6PyX``A?YY@&VYjIg9&Aj$LDUdNV#I~O?LOjF6i$jW|+hUe?bwkj6RnL zuV2AISwqnJ%TB_Rk%=)@3bnYG6`D+S>z4*8k_y&Y+J26k1~=u5rGR)ozK>Udl_41Ce@}{85q8E_&fw?Wh`Bh%|40&@c?wvC45Wv?Ye?eg$n5 zn9lko&JJajC8tTK#y$yEifXWTO4+l$@1EG+KiUIE}jHwE6{}1nf&q`69XAU1#oshmv9ak%Y!|Y4lCT9UlboKK~Y)M?i znw-0`LQ6Ta7iML$-tzFW(S}E-7WK(M=~q37F7t}+fpJpKqrqT#qJ;V3#=3@&7Mk^x zp*L}Rz5ayLON4FQ9|B9rQNlGNb-h};?9$}!1f3f?CJov5uSndPMI-i~Gh9UTNslNT zQJg>)RSkNHSF=tr?RfJY*Ts9n<%~*~GLwFU5OPj0)?Ol6SVj#h=1vXvXYwcalaE{o zru2C19|-48`R-Z}K~ah!p7fTme=1=KMZSjeH^XbVk@h2w(p2QDIyUcfMKRX7Uwv@s z%~zp)!-}P!uN9pmb^lX6jS>-meu7In1u9Nq(3lPUMUI<~X;wMIClsPt(kRP*Az{C+ zTVqW_!_$5p9Q`V13g4UShLPFu5QdDvqAI!(*`=XSd{sZoCg7QazAcMb{p<8I+MEKV zCchAsONyf;64S81f~vb7+-OevJhUKH_{~ZzfxS(&Seiw@f>n@@Dxd=nUXOuB>w*Ynl7h5N ze7Dq5srO4#qmx$JZZOI<+*0EalC2!3h>mFP9vKgWBPeKYz1y0Hal5mYSgANew!9~; z?+OlKr{sN9i~tx_|Eks>%?fn1HIXI=t|h(c0j=0V*H|R!LPN4e{GL!+uu3JQ^_%`U~2W zPwNrrKv~(d+dx)74g0X>I$=vp+I9MP-5y2_5r8PBRw-B2l-ffXvnINi`%52?)lVef zh6R33T-gJ*+Ex zF~B@rSgQBAfRIgawvrA=?4P|AU>=MS+a#)ld6B$R!W0G7#zUeePjhOH{^k4_h9#DZ z%SA4rcXhp^(5$XIL}GtY?+TGeZa+au&aS=?S~-ueb5OC z6nZ<^)xgLv|FOj(d*X4FoC=GavN#~M-+ul@FNv<#8Z6CD)rCEHGARrowLcY9!I%dh zvl{W$=n1HXgtPHozRj~xLp5Rs#&-yZ84EMqTKgFbGCscw1pWK+eLzJ^K*|00)JF_u z8_C=P*#(xOi!-FH%jVCqX>MW76&u<#@_8guR7htavUk*leML#aF-tBrO%A?gBObMb zr5Cx(e#7-RPL7xlc{BjRvA7;`!n1dIMB)zYJt7d>RKrz4M0b3}jJ@)G-}U z$|q%vBS{N6DJU1r`Q9Q{DbtwC#jpfm06Ev>4W=QSk*(V^7v>FsGZZ#G?9mzO>rEj3 z2C5_RPs}6}pzgQVR-nbIHxN*eZjUoAO3pCv&;b>aaM&#j2I^Gdej0vw4JagGKPn{A zLO~VtkZ{S+#GR#2IkvaGu*GIaky+iRHM0AKBsJGSg(O8E0-H+#>Y`Pu$UwguFUN4A zmo7G%LC5x%46?V|X+ANJ>^xn$2;SOJMt&5)v0z$4wbQF`Tb(1Yu5uix-23SYP7?#N zH$KfV!Uur8NdW;ktm7-LodHlCr)92bsdElR4Q@xVbjfKlNpM^J-(_E&ID! zIXBBabLHGUuyU^%ld69ym$bXR{+8jIv;~}A2xaesoLViD>w(u?5U0E^rDH2^pq4-3 z$1P|?_aQXvlG2>sF=3-B;o+^ig0J?ma8hlQSf%6h?KLRV)a$y)`IEV>DJ!ct^B;LB zO;TyIHMpDHi74s9Yey%_CMiZI{HVkN&~v=X>`u?{b=30?gO6rerQzW(C+;^&6fI?u z9Viu9-v(kPyU#je^C`sC4aUOX+GXJJc?aMmW_3A4Hmq3M%%=j?FW;B1+UH%GRNIXo zp>6dySeStkNr&1wy}dV3(7nlYUExy;D;Xu%sM%m$H`^d$wJhKTp4Bps4MgP9ck?N!=TCDGhq!D%(geXJmk^Rie=56Kf>PYY?}q5DBhKLf85 zu0ML(Is2gr2N(IZ=C+Lw8o?b3FH;>hOIw6JS_1Y<+8a?4S8BeR=gerq=Zl)7x$ZPK zk2LC4)_!xg2&eX|$}V6XY3J6sF@oGqy`0$CVzcXOvg_(4_qZ{628@XYWRh*!B_g0l zE>LaLNzU*Q`RX|g^59elygz_$k@(D^j2xzvvx)g8N>iCU;Vq@lsVhF%6PO#iI(`cb zW%%i-LiOxL9p2{Tk9 z!ECiQOAS)-d?m_BboHQ!P5?KY&nU-Jtjg0TBno@tPHt`$Z2)009bRZyFc*VpDf&$0!mJ} zACC{?u$^`lQqDWZ zCd6@3%{wS!!>q`qXb{NPdfLlV!a6tfvpTc7igw|tZIzu2UD0y*t?);{Lsy{79k{*7 zhU#uLoM<-7r&>7!9$3=EqL%Jhj3X7aT#H-df=vC_9y4hOMD0FkezjP+EEAog&*EDF zVrG+y9ll2Bh(NR}+Q2f1bN_QasElp}%4NrR!ec#nXa=4)OB7QSH^=#&bc84qP9~Ax zY=owq3Z6m;z4f4~}^y|88qQy=Dxk=meojBQHa&_T7?tZ;y6|^hJ*r+zW$;eGX#K9d**wrp0#`wep~pR0LT zca5mrVaL4dg&$?&EJguTBkQp~E4r^|plrKS$C(6}d=S27wnX$QdiK|$Fl|rNzO<{P zVM%{1SEg-%{ThcJ2vv}CFuS7(x!;S}>UH*cQ%_O*O-aE={X-E+Y2LSuuJU)m>C+1A z*MP*pI#6PuU+R}9_3J@O3ifc&or|Nk%X*c8NfdCI9$7N%z3&`l9_O2lNikK~@ZL>! z5Q7x~*aENvT}bb_ijI9UkZ^BSi_KJ+_0AW~x-1|m2@sTgpdY~TDk%KkTPQvZvsQ$g zv%PJG&lj`7qklIH#1>H4w$B#OEkVBmVhg}#zad|jehFX;zyq-b43Jp^Yyo%o*#hF| z72DH*(xx${reftfBS~ux_5u?Gn^#m~ATy%^$N(RaPa20Q*Wyy;dX-(>-QD1>5%HCB zwp|K;=5miZhT~^I-UBaQJI9?)7gF#7I*>~<(~sX#0OAySK+5`l_fKt0LknxG+gh61 z#x@pKv{nZDtdncN{Q>lI>JCwTn6w_y=o+~t>K5ct$O~U3nvd}9RO1d?k z?2nZ-qc(H!3!m9$O8muM3inw!-LIstTwZ!_uea82w|3*@T0PeK>Ja`^^>>NO*3U6v zF2D>p3Tj0tYTQ zCskbUzriPiyE>HN$~G58V;%HnxLMslW2_UumMIsn$hO*+-a0L0aYxUIu}Q|$wg-x0 zn{plo8k%F}4(u}wGC3+3t}l%tet9z`Q_##BO&XDoh{!hH_UlMU*3;MLup?WgVPg_K z_JSoP;QX?l>)@$8j|!p+&}@Z~%QO7MK3P1JpE&^KO1wNGIY>471pO1eD|OFqQ|#+2 z(^2y%8Qzlm%v?4(DqqE~rgU5sS+JRFyg+UI&i<>gff-ZWWgqlPRgBUn%w2Jv8at+D z2;bOxHl7HgXTfGB@bgp z#`O?*({kR&--8k(g#8jtoDj6ivmdn3Dzh;&-@>a5*tljZClS@JE15U@i;SfkFOVxx zRM@2{-=g{J+!Opt9w67dR_tYN{~jAZCHXyJAxc5HShG;(&)2nFQcO&?x%_^MbUYhJ;KjGge5vdgX08;x^RP1wbN3{lgPs{0;)NNx!{2-V z^|h*WGTf^Tw4|68<5ysBD)0(=@JwDU%u|^zkV?gd!RsPg(VrMp9J(ww?e4;a6Urp|^4#E@&k57P+7qvaUZdzvt?WO>mAZu%Bt zi`6y7+e$A$%^>Nk2MDVf#D}%EGJWe4N?dtL$h`Vg?q}7@%{{}GckEh4iRdL*y&O64Q@qqi#R#WAy}3USt98M3n3 zeB~_AuLFzvqF&Zk+NSMZ84>61bkMKEt8{Bg5^VpVU{j>HxGh4VLKET09vcS_L%iBc ziL&)T7ubd+ihV0Ft^4j2ddrtE7g?4KG5b(5smXiE^l_qC%jp}k8DC~%f=wIxx{%BVy>_#^3rorT%8?->^+o%+q zE=Ua{I%q_mVuKN`!AJL|b8Xf!-zllyAxmwJh;h(ZFS3ctvBRJyq(e%PX-q-J;p#5H zUI2Z85C#EYeFhrg0A6bUm&1QBp>Pj>LxY;-2K{Pf4LB<+2)aHLZMd?4;2G7!3jnspQZ1M)!wslTIN1JE0KixjB=^km?`NDhH+4U`F; zCHGl;(ZE=fzx!Tr9f8IIY2xS)Dx{8p8`R-ghm@`d>iHw+fqFoo`JK}*9s(b59e^N) z_yD2bLI0iXr<;GFK*+ucv^hSM{eao;K=k=P>El3j5az^Mw{Rpdt1SQmI)v!lKui`A z^LS4Gf(uefVR;ZoDqtd@La*x(xMV1&asQx^sXs}7VDYClf(D@xQnZs%4*BQKtrKLH z9JOXZ%FjfgB7lQ+bYSgfXQ2KQtt`R<;YmQ-bx5Gb6yV5Dfw7LXeI)O{)5~cq$AQob zF6r?&I)*l8GRxN}>QOQQ`3H?RWk0ILfN z;wwVVg8e6^G%^}{+ZvdGb--zoKtPjFogwkRQw_l>kBJPSniZPkf%b{B4%9OJr)7I! zK_E!D}Dyu#W-Y?awg~eNUZbVKz~rs9|t*bLjPYuTXZLbfat2ut)K>F&aPHKZcZR^1yXKq`cshqjocUz z6RX1nIS9ahWNxN=Zs{XX$CBcJ>D2`+pA5n@3(f}qZ@vL?-R7{Ie8Ul~&bhzfPO8FI zGax-I;O`*~bmJ`8|3-c!h&jOLIF=to)N&yDkH4?2*6))RmjUG?0Omsop?u9#h<}j! zk7Jx?+pXt3mimbK<3RGi92r19Si?UNbs2E^l7RG(+b$B<@2KbXm7o=xgAn$d6VQhN z;07SKU9tPWBmXygfwaa+p5t+j*)h(g7?Ak6vA_EDfmT+wfX{;T26Vh!5kK z;r?+>PY!}GD4}A?*uViCfP~ztmSoQus`aUk4O* zbg&2g4|yECZ2>lrm9?q4nel=8Xa4_+0-^Ho>r#NOC)~eTkHZ&Mi3)3YqpTU@65SpI%A|XTknA@)G)b2NgUA za{v6hEC$pQ>(t8xc$^P}$@G!LR2vZ79#A@DG$`^n%;OWlUvNQ_ z3Ck>0DFR@Y0ry}?OB4t^E$hG2$m#tJGPd{g#qlVI$;{_oM2qN~U6Fw36Tijw7=wQU z{dcmTj@?3D#wfy0hOsuZG%+;Ox6si$kbn07Pbmm^J2?KAJRpBF;0PcuV>V|4|KS_{ zIMjhBIQAk-2e=6mA6Lb?Xrg&_~`HfNyzJLyla zCrIldianP8sLA|(80#;F4+8BGUKcR~w9gN09Y@m&tS=yNZty$kc~=xjD^s_TgW&;S zU0_-vL(U`#ze)S=lyZ7XA)TXV;_)y?ZP9_4%sJ*0vMD+)pbRLe1C_L&hWu~jKGHkr zPB6=DlX4XfT>AR6f&cIge|&T~&vLq(cKi)TX*CD_;E$*$%L%f} zGx!HMo`DM1^Z9)Gfo#1D4$qhVyU_n$IgmY1!N+09{(T(q4yga{JI+=<i>g1(LQ7+RB*BT&Vv1?`Z*W~? zwnaQ2E@aO;aNNtq$GG4f@XiMd*%}QTmbDZN_NY1Yyq%m zbl3tvfq`HlTPT6UR=+-mg>0yFK48dBIN-n%)kna9xk2>BIUh1)yB2Wdi?v6{;LTgk zhYH!C102<$?g$mUUB~%QA$v)Hqhd51qk?soI3Frx;{b3}rzQwgh&BQM_8Bb%P8$Ry zQ#?5KWXm!3QGoj};q83LkgVX~$PZhOkij^@&j$+00u2uO`u!2;QDpcZNef9e3=W*z zaRhu4`2Jb& z2ORd(*byu!#`BLrA!$>&Y=Ow0#b0? z9}CC0CpYGQgbi7k2#!6sbc79ArucVVfD%a|OVPk_Wr1vm178DPme~#I3F-% zS~xf`?cOo)(b@eUX#+A57#ukq8XC&#jI>}t;xkf$B}HI?coHBB@VD|N6x2O9&=vT9 DtFlnc literal 0 HcmV?d00001 diff --git a/script/network_plan/plan-netzwerk-docker.txt b/script/network_plan/plan-netzwerk-docker.txt new file mode 100644 index 0000000..6ed44a4 --- /dev/null +++ b/script/network_plan/plan-netzwerk-docker.txt @@ -0,0 +1,220 @@ +### Default Netzwerk 172.20.XXX.XXX ### + +Docker Netzwerk: + +Netzwerk appwrite: + - gateway: + - appwrite: + - runtimes: + +Netzwerk = chatwoot +--> 172.20.1.0/28 +Gateway --> 172.20.1.1 +chatwoot-rails --> 172.20.1.2 +chatwoot-sidekiq --> 172.20.1.3 +chatwoot-postgres --> 172.20.1.4 +chatwoot-redis --> 172.20.1.5 +chatwoot-backup --> 172.20.1.6 + +Netzwerk = drone-gitea +--> 172.20.2.0/28 +Gateway --> 172.20.2.1 +drone-gitea --> 172.20.2.2 + +Netzwerk = gitea +--> 172.20.3.0/28 +Gateway --> 172.20.3.1 +gitea --> 172.20.3.2 +gitea-postgres --> 172.20.3.3 +gitea-backup --> 172.20.3.4 + +Netzwerk = glitchtip +--> 172.20.4.0/28 +Gateway --> 172.20.4.1 +glitchtip-postgres --> 172.20.4.2 +glitchtip-redis --> 172.20.4.3 +glitchtip-web --> 172.20.4.4 +glitchtip-worker --> 172.20.4.5 +glitchtip-migrate --> 172.20.4.6 +glitchtip-backup --> 172.20.4.7 + +Netzwerk = html +--> 172.20.5.0/28 +Gateway --> 172.20.5.1 +html --> 172.20.5.2 + +Netzwerk = listmonk +--> 172.20.6.0/28 +Gateway --> 172.20.6.1 +listmonk-postgres --> 172.20.6.2 +listmonk-web --> 172.20.6.3 +listmonk-backup --> 172.20.6.4 + +Netzwerk = mixpost [REMOVED] +--> 172.20.7.0/28 +Gateway --> 172.20.7.1 +mixpost --> 172.20.7.2 [REMOVED] +mixpost-mysql --> 172.20.7.3 [REMOVED] +mixpost-redis --> 172.20.7.4 [REMOVED] +mixpost-backup --> 172.20.7.5 [REMOVED] + +Netzwerk = n8n +--> 172.20.8.0/28 +Gateway --> 172.20.8.1 +n8n-postgres --> 172.20.8.2 +n8n --> 172.20.8.3 +n8n-backup --> 172.20.8.4 + +Netzwerk = nextcloud +--> 172.20.9.0/28 +Gateway --> 172.20.9.1 +nextcloud-postgres --> 172.20.9.2 +nextcloud-redis --> 172.20.9.3 +nextcloud-app --> 172.20.9.4 +nextcloud-cron --> 172.20.9.5 +nextcloud-backup --> 172.20.9.6 +nextcloud-collabora --> 172.20.9.7 +nextcloud-whiteboard --> 172.20.9.8 + +Netzwerk = penpot +--> 172.20.10.0/28 +Gateway --> 172.20.10.1 +penpot-frontend --> 172.20.10.2 +penpot-backend --> 172.20.10.3 +penpot-exporter --> 172.20.10.4 +penpot-postgres --> 172.20.10.5 +penpot-redis --> 172.20.10.6 +penpot-mailcatch --> 172.20.10.7 +penpot-backup --> 172.20.10.8 + +Netzwerk = poste +--> 172.20.11.0/28 +Gateway --> 172.20.11.1 +poste --> 172.20.11.2 + +Netzwerk = squidex +--> 172.20.12.0/28 +Gateway --> 172.20.12.1 +squidex-mongodb --> 172.20.12.2 +squidex --> 172.20.12.3 +squidex-backup --> 172.20.12.4 + +Netzwerk = umami +--> 172.20.13.0/28 +Gateway --> 172.20.13.1 +umami --> 172.20.13.2 +umami-postgres --> 172.20.13.3 +umami-backup --> 172.20.13.4 + +Netzwerk = uptime-kuma +--> 172.20.14.0/28 +Gateway --> 172.20.14.1 +uptime-kuma --> 172.20.14.2 + +Netzwerk = windmill +--> 172.20.15.0/28 +Gateway --> 172.20.15.1 +windmill-postgres --> 172.20.15.2 +windmill-server --> 172.20.15.3 +windmill-worker --> 172.20.15.4 +windmill-worker-native --> 172.20.15.5 +windmill-lsp --> 172.20.15.6 +windmill-multiplayer --> 172.20.15.7 +windmill-caddy --> 172.20.15.8 +windmill-backup --> 172.20.15.9 + +Netzwerk = wordpress +--> 172.20.16.0/28 +Gateway --> 172.20.16.1 +wordpress-mysql --> 172.20.16.2 +wordpress --> 172.20.16.3 +wordpress-backup --> 172.20.16.4 + +Netzwerk = baserow +--> 172.20.17.0/28 +Gateway --> 172.20.17.1 +Baserow --> 172.20.17.2 +baserow-db --> 172.20.17.3 +baserow-backup --> 172.20.17.4 + +Netzwerk = calcom +--> 172.20.18.0/28 +Gateway --> 172.20.18.1 +calcom-postgres --> 172.20.18.2 +calcom --> 172.20.18.3 +calcom-backup --> 172.20.18.4 + +Netzwerk = odoo +--> 172.20.19.0/28 +Gateway --> 172.20.19.1 +odoo-web --> 172.20.19.2 +odoo-postgres --> 172.20.19.3 +odoo-backup --> 172.20.19.4 + +Netzwerk = portainer +--> 172.20.20.0/28 +Gateway --> 172.20.20.1 +portainer-web --> 172.20.20.2 + +Netzwerk = flame +--> 172.20.21.0/28 +Gateway --> 172.20.21.1 +flame-web --> 172.20.21.2 + +Netzwerk = mixpost-ee [REMOVED] +--> 172.20.22.0/28 +Gateway --> 172.20.22.1 +mixpost --> 172.20.22.2 [REMOVED] +mixpost-mysql --> 172.20.22.3 [REMOVED] +mixpost-redis --> 172.20.22.4 [REMOVED] +mixpost-backup --> 172.20.22.5 [REMOVED] + +Netzwerk = letsbe-application +--> 172.20.23.0/28 +Gateway --> 172.20.23.1 +letsbe-application --> 172.20.23.2 + +Network = nocodb +--> 172.20.24.0/28 +Gatweay --> 172.20.24.1 +nocodb --> 172.20.24.2 +nocodb-database --> 172.20.24.3 + +Network = typebot +--> 172.20.25.0/28 +Gateway --> 172.20.25.1 +Typebot-db --> 172.20.25.2 +Typebot-builder --> 172.20.25.3 +Typebot-viewer --> 172.20.25.4 + + +Network = MinIO +--> 172.20.26.0/28 +Gateway --> 172.20.26.1 +MinIO --> 172.20.26.2 + +Network = Activepieces +--> 172.20.27.0/28 +Gateway --> 172.20.27.1 +Activepieces --> 172.20.27.2 +Activepieces-postgres --> 172.20.27.3 +Activepieces-redis --> 172.20.27.4 + + +Network = Redash +--> 172.20.28.0/28 +Gateway --> 172.20.28.1 +X-Redash-Service --> 172.20.28.2 +Server --> 172.20.28.3 +Scheduler --> 172.20.28.4 +Scheduled_Worker --> 172.20.28.5 +Adhoc_Worker --> 172.20.28.6 +redis --> 172.20.28.7 +postgres --> 172.20.28.8 +worker --> 172.20.28.9 + + + + + + diff --git a/script/network_plan/plan-ports-docker.txt b/script/network_plan/plan-ports-docker.txt new file mode 100644 index 0000000..22e5e18 --- /dev/null +++ b/script/network_plan/plan-ports-docker.txt @@ -0,0 +1,71 @@ +### Default Ports 3XXX ### ("E" = "External" (???)) + +Ports: + +443 E --> Nginx https +80 E --> Nginx http +22/22022 E --> SSH + +25 E --> Poste SMTP +110 E --> Poste POP3 +143 E --> Poste IMAP +465 E --> Poste SMTP over SSL +587 E --> Poste Submission +993 E --> Poste IMAPS +995 E --> Poste POP3S + +3000 --> Static html Website +3001 --> WordPress +3002 --> Squidex +3003 --> Poste.io http +3004 --> Poste.io https +3005 --> Uptime Kuma +3006 --> Listmonk-Web +3007 --> Gitea +3008 --> Umami +3009 --> Drone http +3010 --> Drone https +3011 --> Chatwoot-rails +3012 --> Baserow http +3013 --> Baserow https +3014 --> Windmill-caddy +3015 --> Appwrite http +3016 --> Appwrite https +3017 --> Glitchtip http +3018 --> calcom web +3019 --> odoo web +3020 --> [REMOVED] Mixpost http +3021 --> Penpot http +3022 --> Portainer http +3023 --> Nextcloud +3024 --> Portainer https +3025 --> n8n +3036 E --> Gitea SSH +3037 --> Listmonk-db +3038 --> Windmill-db +3039 --> Windmill-Server +3040 --> [REMOVED] Mixpost-EE http +3041 --> Windmill-lsp +3042 --> [REMOVED] Mixpost-EE mysql +3043 --> [REMOVED] Mixpost-EE redis +3044 --> +3045 --> +3046 --> Glitchtip Postgres +3047 --> Windmill-multiplayer +3048 --> Penpot mailcatch +3049 --> Chatwoot-postgres +3050 --> Chatwoot-redis +3051 --> [REMOVED] Mixpost-mysql +3052 --> [REMOVED] Mixpost-redis +3053 --> WordPress-mysql +3054 --> Flame +3055 --> Letsbe-Application +3056 --> Activepieces +3057 --> nocodb-app +5433 (INTERNAL) --> nocodb-postgres +3058 --> MinIO-s3 +3059 --> MinIO-console +3060 --> nextcloud-whiteboard +3061 --> typebot-builder (different IP) +3062 --> typebot-viewer (different IP) +3063 --> redash-server \ No newline at end of file diff --git a/script/nginx/activepieces.conf b/script/nginx/activepieces.conf new file mode 100644 index 0000000..97af483 --- /dev/null +++ b/script/nginx/activepieces.conf @@ -0,0 +1,60 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_activepieces }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_activepieces }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3056; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Authorization $http_authorization; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + + + + + +} \ No newline at end of file diff --git a/script/nginx/baserow.conf b/script/nginx/baserow.conf new file mode 100644 index 0000000..e85fba6 --- /dev/null +++ b/script/nginx/baserow.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_baserow }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_baserow }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3012; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/botlab.conf b/script/nginx/botlab.conf new file mode 100644 index 0000000..a133598 --- /dev/null +++ b/script/nginx/botlab.conf @@ -0,0 +1,39 @@ +server { + client_max_body_size 64M; + server_name {{ domain_botlab }}; + + location / { + proxy_pass http://172.20.1.8:3000; # Backend for typebot-builder + proxy_http_version 1.1; + proxy_cache_bypass $http_upgrade; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/nginx/placeholder.crt; # managed by Certbot + ssl_certificate_key /etc/nginx/placeholder.key; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + + + listen 80; + server_name {{ domain_botlab }}; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/script/nginx/bots.conf b/script/nginx/bots.conf new file mode 100644 index 0000000..51ab83e --- /dev/null +++ b/script/nginx/bots.conf @@ -0,0 +1,36 @@ +server { + client_max_body_size 64M; + server_name {{ domain_typebot }}; + + location / { + proxy_pass http://172.20.1.9:3000; # Backend for bot-viewer + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/nginx/placeholder.crt; # managed by Certbot + ssl_certificate_key /etc/nginx/placeholder.key; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + + listen 80; + server_name {{ domain_typebot }}; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/script/nginx/calcom.conf b/script/nginx/calcom.conf new file mode 100644 index 0000000..d4b6440 --- /dev/null +++ b/script/nginx/calcom.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_calcom }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_calcom }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3018; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/chatwoot.conf b/script/nginx/chatwoot.conf new file mode 100644 index 0000000..646ce04 --- /dev/null +++ b/script/nginx/chatwoot.conf @@ -0,0 +1,107 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_chatwoot }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_chatwoot }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + location / { + proxy_pass http://0.0.0.0:3011; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_chatwoot_helpdesk }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_chatwoot_helpdesk }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3011; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} \ No newline at end of file diff --git a/script/nginx/documenso.conf b/script/nginx/documenso.conf new file mode 100644 index 0000000..0bdc449 --- /dev/null +++ b/script/nginx/documenso.conf @@ -0,0 +1,40 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_documenso }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + + listen 443 ssl http2; + server_name {{ domain_documenso }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + location / { + proxy_pass http://127.0.0.1:3020; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/flame.conf b/script/nginx/flame.conf new file mode 100644 index 0000000..93c5dba --- /dev/null +++ b/script/nginx/flame.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3054; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/ghost.conf b/script/nginx/ghost.conf new file mode 100644 index 0000000..acc5e7f --- /dev/null +++ b/script/nginx/ghost.conf @@ -0,0 +1,40 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_ghost }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + + listen 443 ssl http2; + server_name {{ domain_ghost }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + location / { + proxy_pass http://127.0.0.1:2368; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/gitea-drine.conf b/script/nginx/gitea-drine.conf new file mode 100644 index 0000000..0e1e493 --- /dev/null +++ b/script/nginx/gitea-drine.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_gitea_drone }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_gitea_drone }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3009; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/gitea.conf b/script/nginx/gitea.conf new file mode 100644 index 0000000..8237a46 --- /dev/null +++ b/script/nginx/gitea.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_gitea }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_gitea }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3007; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/glitchtip.conf b/script/nginx/glitchtip.conf new file mode 100644 index 0000000..ae8d07e --- /dev/null +++ b/script/nginx/glitchtip.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_glitchtip }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_glitchtip }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3017; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/html.conf b/script/nginx/html.conf new file mode 100644 index 0000000..2d964a7 --- /dev/null +++ b/script/nginx/html.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_html }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_html }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3000; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/keycloak.conf b/script/nginx/keycloak.conf new file mode 100644 index 0000000..493b8ee --- /dev/null +++ b/script/nginx/keycloak.conf @@ -0,0 +1,46 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_keycloak }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + + listen 443 ssl http2; + server_name {{ domain_keycloak }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port 443; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/librechat.conf b/script/nginx/librechat.conf new file mode 100644 index 0000000..eba036e --- /dev/null +++ b/script/nginx/librechat.conf @@ -0,0 +1,44 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_librechat }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + + listen 443 ssl http2; + server_name {{ domain_librechat }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + location / { + proxy_pass http://0.0.0.0:3080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/listmonk.conf b/script/nginx/listmonk.conf new file mode 100644 index 0000000..efc9ace --- /dev/null +++ b/script/nginx/listmonk.conf @@ -0,0 +1,49 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_listmonk }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_listmonk }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3006; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/minio.conf b/script/nginx/minio.conf new file mode 100644 index 0000000..2e0ae74 --- /dev/null +++ b/script/nginx/minio.conf @@ -0,0 +1,110 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_minio }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + listen 443 ssl http2; + server_name {{ domain_minio }}; + + location / { + proxy_pass http://172.20.26.2:9001; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded_Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + + # Remove existing CORS headers from MinIO to prevent duplicates + proxy_hide_header Access-Control-Allow-Origin; + + # CORS Settings + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' '*' always; + add_header 'Access-Control-Expose-Headers' 'ETag' always; + + # Handle CORS preflight requests + if ($request_method = 'OPTIONS') { + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + return 204; + } + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-PO"; + ssl_prefer_server_ciphers on; + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; +} + +server { + client_max_body_size 0; + server_name {{ domain_s3 }}; + + location / { + proxy_pass http://172.20.26.2:9000; # S3-compatible service + proxy_set_header Host $http_host; # Essential for S3 bucket ops + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Remove existing cors headers from MinIO to prevent duplicates + proxy_hide_header Access-Control-Allow-Origin; + + # CORS Settings + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' '*' always; + add_header 'Access-Control-Expose-Headers' 'Origin, Content-Type, Content-MD5, Content-Disposition, ETag' always; + + # Handle CORS preflight requests + if ($request_method = 'OPTIONS') { + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + return 204; + } + } + + # ACME Challenge Location (for Let's Encrypt) + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type 'text/plain'; + allow all; + } + + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/nginx/placeholder.crt; # managed by Certbot + ssl_certificate_key /etc/nginx/placeholder.key; # managed by Certbot + #include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + #ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot +} + +server { + listen 80; + server_name {{ domain_s3 }}; + return 404; # managed by Certbot +} diff --git a/script/nginx/n8n.conf b/script/nginx/n8n.conf new file mode 100644 index 0000000..0be1066 --- /dev/null +++ b/script/nginx/n8n.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_n8n }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_n8n }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3025; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/nextcloud.conf b/script/nginx/nextcloud.conf new file mode 100644 index 0000000..127028d --- /dev/null +++ b/script/nginx/nextcloud.conf @@ -0,0 +1,185 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_nextcloud }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 500M; + + listen 443 ssl http2; + + server_name {{ domain_nextcloud }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3023; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + #proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header Accept-Encoding ""; + proxy_set_header Host $host; + + client_body_buffer_size 512k; + proxy_read_timeout 86400s; + client_max_body_size 0; + + # Websocket + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + #location /whiteboard/ { + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header Host $host; + + # proxy_pass http://0.0.0.0:3002 + + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + #} + + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers on; + + # Optional settings: + + # OCSP stapling + # ssl_stapling on; + # ssl_stapling_verify on; + # ssl_trusted_certificate /etc/letsencrypt/live//chain.pem; + + # replace with the IP address of your resolver + # resolver 127.0.0.1; # needed for oscp stapling: e.g. use 94.140.15.15 for adguard / 1.1.1.1 for cloudflared or 8.8.8.8 for google - you can use the same nameserver as listed in your /etc/resolv.conf file + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + location /.well-known/carddav { + return 301 $scheme://$host/remote.php/dav; + } + location /.well-known/caldav { + return 301 $scheme://$host/remote.php/dav; + } +} + +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_collabora }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_collabora }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass https://0.0.0.0:3044; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + add_header X-Frontend-Host $host; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + listen 443 ssl http2; + server_name {{ domain_whiteboard }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + #auth_basic "Restricted Content"; + #auth_basic_user_file ; + + location / { + proxy_pass http://0.0.0.0:3060; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + add_header X-Frontend-Host $host; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/nocodb.conf b/script/nginx/nocodb.conf new file mode 100644 index 0000000..a7a3b0f --- /dev/null +++ b/script/nginx/nocodb.conf @@ -0,0 +1,67 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_nocodb }}; + + location / { + return 301 https://$host$request_uri; + } + + location ~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + + + +} + +server { + client_max_body_size 64M; + + listen 443 ssl http2; + server_name {{ domain_nocodb }}; + + # SSL Certificates (to be updated by Certbot) + + # Uncomment this if you want to enforce HSTS + # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Allow embedding in iframe + add_header X-Frame-Options "ALLOWALL"; + add_header Content-Security-Policy "frame-ancestors *;"; + + # CORS Headers + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type'; + + location / { + proxy_pass http://0.0.0.0:3057; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Support WebSocket + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + } + + location ~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + ssl_certificate /etc/nginx/placeholder.crt; # managed by Certbot + ssl_certificate_key /etc/nginx/placeholder.key; # managed by Certbot +} \ No newline at end of file diff --git a/script/nginx/odoo.conf b/script/nginx/odoo.conf new file mode 100644 index 0000000..20ae8f4 --- /dev/null +++ b/script/nginx/odoo.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_odoo }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_odoo }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3019; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/penpot.conf b/script/nginx/penpot.conf new file mode 100644 index 0000000..699d05e --- /dev/null +++ b/script/nginx/penpot.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_penpot }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_penpot }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3021; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/poste.conf b/script/nginx/poste.conf new file mode 100644 index 0000000..da0e70f --- /dev/null +++ b/script/nginx/poste.conf @@ -0,0 +1,61 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_poste }}; + + location ^~ /.well-known/acme-challenge/ { + proxy_pass http://0.0.0.0:3003; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_poste }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass https://0.0.0.0:3004; + proxy_buffering off; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + proxy_pass http://0.0.0.0:3003; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + default_type "text/plain"; + allow all; + + } +} diff --git a/script/nginx/redash.conf b/script/nginx/redash.conf new file mode 100644 index 0000000..8d53362 --- /dev/null +++ b/script/nginx/redash.conf @@ -0,0 +1,51 @@ +server { + + client_max_body_size 64M; + + listen 80; + server_name {{ domain_redash }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + + listen 443 ssl http2; + server_name {{ domain_redash }}; + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + location / { + proxy_pass http://0.0.0.0:3064; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_connect_timeout 30s; + proxy_read_timeout 86400s; + proxy_send_timeout 30s; + proxy_http_version 1.1; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + +} \ No newline at end of file diff --git a/script/nginx/s3.conf b/script/nginx/s3.conf new file mode 100644 index 0000000..a37828e --- /dev/null +++ b/script/nginx/s3.conf @@ -0,0 +1,52 @@ +server { + client_max_body_size 0; + server_name {{ domain_s3 }}; + + location / { + proxy_pass http://127.0.0.1:9000; # Proxy to MinIO or your S3-compatible service + proxy_set_header Host $http_host; # Essential for S3 bucket operations + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Remove existing cors headers from MinIO to prevent duplicates + proxy_hide_header Access-Control-Allow-Origin; + + # CORS Settings + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' '*' always; + add_header 'Access-Control-Expose-Headers' 'Origin, Content-Type, Content-MD5, Content-Disposition, ETag' always; + + # Handle CORS preflight requests + if ($request_method = 'OPTIONS') { + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + return 204; + } + } + + # ACME Challenge Location (for Let's Encrypt) + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type 'text/plain'; + allow all; + } + + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/nginx/placeholder.crt; # managed by Certbot + ssl_certificate_key /etc/nginx/placeholder.key; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + + +} +server { + + + listen 80; + server_name {{ domain_s3 }}; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/script/nginx/squidex.conf b/script/nginx/squidex.conf new file mode 100644 index 0000000..32f6a4d --- /dev/null +++ b/script/nginx/squidex.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_squidex }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_squidex }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3002; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/stirlingpdf.conf b/script/nginx/stirlingpdf.conf new file mode 100644 index 0000000..eaef2ba --- /dev/null +++ b/script/nginx/stirlingpdf.conf @@ -0,0 +1,46 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_pdf }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + + listen 443 ssl http2; + server_name {{ domain_pdf }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # For websocket support if needed + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/typebot.conf b/script/nginx/typebot.conf new file mode 100644 index 0000000..950de3e --- /dev/null +++ b/script/nginx/typebot.conf @@ -0,0 +1,69 @@ +server { + client_max_body_size 64M; + server_name {{ domain_botlab }}; + + location / { + proxy_pass http://172.20.25.3:3000; # Backend for typebot-builder + proxy_http_version 1.1; + proxy_cache_bypass $http_upgrade; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/nginx/placeholder.crt; # managed by Certbot + ssl_certificate_key /etc/nginx/placeholder.key; # managed by Certbot + #include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + #ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot +} + +server { + listen 80; + server_name {{ domain_botlab }}; + return 404; # managed by Certbot +} + +server { + client_max_body_size 64M; + server_name {{ domain_bot_viewer }}; + + location / { + proxy_pass http://172.20.25.4:3000; # Backend for bot-viewer + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/nginx/placeholder.crt; # managed by Certbot + ssl_certificate_key /etc/nginx/placeholder.key; # managed by Certbot + #include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + #ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot +} + +server { + listen 80; + server_name {{ domain_bot_viewer }}; + return 404; # managed by Certbot +} diff --git a/script/nginx/umami.conf b/script/nginx/umami.conf new file mode 100644 index 0000000..ee3ee8c --- /dev/null +++ b/script/nginx/umami.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_umami }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_umami }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3008; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/uptime-kuma.conf b/script/nginx/uptime-kuma.conf new file mode 100644 index 0000000..c0f2a4d --- /dev/null +++ b/script/nginx/uptime-kuma.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_uptime_kuma }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_uptime_kuma }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3005; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/whiteboard.conf b/script/nginx/whiteboard.conf new file mode 100644 index 0000000..e188693 --- /dev/null +++ b/script/nginx/whiteboard.conf @@ -0,0 +1,53 @@ +server { + if ($host = {{ domain_whiteboard }}) { + return 301 https://$host$request_uri; + } # managed by Certbot + + client_max_body_size 64M; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_whiteboard }}; + ssl_certificate /etc/letsencrypt/live/whiteboard.letsbe.solutions/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/whiteboard.letsbe.solutions/privkey.pem; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:4014; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + add_header X-Frontend-Host $host; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } + +} \ No newline at end of file diff --git a/script/nginx/windmill.conf b/script/nginx/windmill.conf new file mode 100644 index 0000000..753b9bb --- /dev/null +++ b/script/nginx/windmill.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_windmill }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_windmill }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3014; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/nginx/wordpress.conf b/script/nginx/wordpress.conf new file mode 100644 index 0000000..cf48e02 --- /dev/null +++ b/script/nginx/wordpress.conf @@ -0,0 +1,53 @@ +server { + client_max_body_size 64M; + + listen 80; + server_name {{ domain_wordpress }}; + + location / { + return 301 https://$host$request_uri; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} + +server { + client_max_body_size 64M; + #large_client_header_buffers 4 16k; + + listen 443 ssl http2; + + server_name {{ domain_wordpress }}; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; + + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + #auth_basic "Restricted Content"; + #auth_basic_user_file letsbe-htpasswd; + + location / { + proxy_pass http://0.0.0.0:3001; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + #proxy_buffers 16 4k; + #proxy_buffer_size 2k; + } + + location ^~ /.well-known/acme-challenge/ { + alias /var/www/html/.well-known/acme-challenge/; + default_type "text/plain"; + allow all; + } +} diff --git a/script/setup.sh b/script/setup.sh new file mode 100644 index 0000000..22295f4 --- /dev/null +++ b/script/setup.sh @@ -0,0 +1,662 @@ +#!/bin/bash +# +# LetsBe Server Setup Script +# This script sets up the server and deploys selected tools. +# +# Usage: +# ./setup.sh --tools "all" --domain "example.com" +# ./setup.sh --tools "portainer,n8n,baserow" --domain "example.com" +# ./setup.sh --tools "1,2,3" +# ./setup.sh # Foundation only, no tools deployed +# +# Arguments: +# --tools Comma-separated list of tools to deploy, "all", or tool numbers +# --domain Domain name for SSL email (administrator@domain) +# --skip-ssl Skip SSL certificate setup (useful for testing) +# + +set -euo pipefail + +# ============================================================================= +# ARGUMENT PARSING +# ============================================================================= + +TOOLS_TO_DEPLOY="" +SKIP_SSL=false +ROOT_SSL=false +DOMAIN="" + +while [[ $# -gt 0 ]]; do + case $1 in + --tools) + TOOLS_TO_DEPLOY="$2" + shift 2 + ;; + --domain) + DOMAIN="$2" + shift 2 + ;; + --skip-ssl) + SKIP_SSL=true + shift + ;; + --root-ssl) + ROOT_SSL=true + shift + ;; + --help|-h) + echo "Usage: $0 [--tools \"tool1,tool2,...\"|\"all\"] [--domain DOMAIN] [--skip-ssl] [--root-ssl]" + echo "" + echo "Options:" + echo " --tools Comma-separated list of tools, 'all', or tool numbers" + echo " --domain Domain name for SSL email (administrator@domain)" + echo " --skip-ssl Skip SSL certificate setup" + echo " --root-ssl Include root domain in SSL certificate" + echo "" + echo "Examples:" + echo " $0 --tools \"all\" --domain \"example.com\"" + echo " $0 --tools \"portainer,n8n,baserow\"" + echo " $0 --tools \"1,5,10\"" + echo " $0 # Foundation only" + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +echo "=== LetsBe Server Setup ===" +echo "" + +# ============================================================================= +# PACKAGE INSTALLATION +# ============================================================================= + +echo "[1/10] Installing system packages..." +sudo apt update && sudo apt upgrade -y +sudo apt install -y build-essential net-tools tree wget jq nano curl htop ufw fail2ban unattended-upgrades apt-listchanges apticron git gnupg ca-certificates apache2-utils acl certbot python3-certbot-nginx rsync rclone s3cmd zip sudo iptables htop dstat openssl + +# ============================================================================= +# DOCKER INSTALLATION +# ============================================================================= + +echo "[2/10] Installing Docker..." +sudo install -m 0755 -d /etc/apt/keyrings +# Use --batch and --yes for non-interactive gpg (required for nohup/background execution) +sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg +sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg +rm -f /tmp/docker.gpg +sudo chmod a+r /etc/apt/keyrings/docker.gpg +sudo echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt update +sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +sudo systemctl enable docker + +sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" -o /usr/local/bin/docker-compose +sudo chmod 755 /usr/local/bin/docker-compose + +# ============================================================================= +# DISABLE CONFLICTING SERVICES +# ============================================================================= + +echo "[3/10] Disabling conflicting services..." +sudo systemctl stop exim4 2>/dev/null || true +sudo systemctl disable exim4 2>/dev/null || true + +sudo systemctl stop apache2 2>/dev/null || true +sudo systemctl disable apache2 2>/dev/null || true +sudo apt remove -y apache2 2>/dev/null || true + +# ============================================================================= +# NGINX INSTALLATION & CONFIGURATION +# ============================================================================= + +echo "[4/10] Installing and configuring nginx..." +sudo apt install -y nginx +sudo systemctl enable nginx + +sudo rm -f /etc/nginx/sites-enabled/default + +openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout /etc/nginx/placeholder.key -out /etc/nginx/placeholder.crt + +cat < /etc/nginx/conf.d/fallback.conf +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 444; +} + +server { + listen 443 ssl default_server; + server_name _; + return 444; + + ssl_certificate /etc/nginx/placeholder.crt; + ssl_certificate_key /etc/nginx/placeholder.key; +} +EOF + +sudo systemctl restart nginx + +# ============================================================================= +# FIREWALL CONFIGURATION +# ============================================================================= + +echo "[5/10] Configuring UFW firewall..." +ufw allow 22 +ufw allow 22022 +ufw allow 80 +ufw allow 443 +ufw allow 25 +ufw allow 587 +ufw allow 143 +ufw allow 110 +ufw allow 4190 +ufw allow 465 +ufw allow 993 +ufw allow 995 +ufw --force enable + +# ============================================================================= +# USER SETUP - STEFAN (DO NOT MODIFY) +# ============================================================================= + +echo "[6/10] Configuring user 'stefan'..." +USER="stefan" +PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINV9ptnNgA4+haqLWh9bOilydlX2LIlAZqjfaDN5qEPf calorie_preset200@simplelogin.com" + +if ! id -u $USER > /dev/null 2>&1; then + echo "User $USER does not exist, will be created." + useradd -m -s /bin/bash $USER +fi + +mkdir -p /home/$USER/.ssh +chmod 700 /home/$USER/.ssh + +echo "$PUBLIC_KEY" >> /home/$USER/.ssh/authorized_keys +chmod 600 /home/$USER/.ssh/authorized_keys +chown -R $USER:$USER /home/$USER/.ssh + +usermod -aG docker $USER + +echo "Public key was added for user $USER." + +# ============================================================================= +# SSH SECURITY HARDENING +# ============================================================================= + +echo "[7/10] Hardening SSH configuration..." +cat < /etc/ssh/sshd_config +Include /etc/ssh/sshd_config.d/*.conf + +Port 22022 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#HostKey /etc/ssh/ssh_host_rsa_key +#HostKey /etc/ssh/ssh_host_ecdsa_key +#HostKey /etc/ssh/ssh_host_ed25519_key + +SyslogFacility AUTH +LogLevel VERBOSE + +LoginGraceTime 2m +PermitRootLogin yes +#StrictModes yes +MaxAuthTries 6 +MaxSessions 10 + +PasswordAuthentication yes +PermitEmptyPasswords no + +ChallengeResponseAuthentication no + +UsePAM yes + +X11Forwarding yes +PrintMotd no +PrintLastLog yes + +AcceptEnv LANG LC_* + +Subsystem sftp /usr/lib/openssh/sftp-server + +UsePrivilegeSeparation sandbox +AuthenticationMethods publickey +EOF + +# NOTE: SSH restart moved to end of script to keep connection alive + +# ============================================================================= +# AUTOMATIC SECURITY UPDATES +# ============================================================================= + +echo "[8/10] Configuring automatic security updates..." +cat < /etc/apt/apt.conf.d/20auto-upgrades +// Enable the update/upgrade script (0=disable) +APT::Periodic::Enable "1"; + +// Do "apt-get update" automatically every n-days (0=disable) +APT::Periodic::Update-Package-Lists "1"; + +// Do "apt-get upgrade --download-only" every n-days (0=disable) +APT::Periodic::Download-Upgradeable-Packages "1"; + +// Do "apt-get autoclean" every n-days (0=disable) +APT::Periodic::AutocleanInterval "7"; + +// Send report mail to root +// 0: no report (or null string) +// 1: progress report (actually any string) +// 2: + command outputs (remove -qq, remove 2>/dev/null, add -d) +APT::Periodic::Unattended-Upgrade "1"; + +// Automatically upgrade packages from these +Unattended-Upgrade::Origins-Pattern { +// "o=Debian,a=stable"; +// "o=Debian,a=stable-updates"; + "origin=Debian,codename=\${distro_codename},label=Debian-Security"; +}; + +// You can specify your own packages to NOT automatically upgrade here +Unattended-Upgrade::Package-Blacklist { +}; + +// Run dpkg --force-confold --configure -a if a unclean dpkg state is detected to true to ensure that updates get installed even when the system got interrupted during a previous run +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; + +// Perform the upgrade when the machine is running because we wont be shutting our server down often +Unattended-Upgrade::InstallOnShutdown "false"; + +// Send an email to this address with information about the packages upgraded. +Unattended-Upgrade::Mail "administrator@letsbe.biz"; + +// Always send an e-mail +Unattended-Upgrade::MailOnlyOnError "true"; + +// Remove all unused dependencies after the upgrade has finished +Unattended-Upgrade::Remove-Unused-Dependencies "true"; + +// Remove any new unused dependencies after the upgrade has finished +Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; + +// Automatically reboot WITHOUT CONFIRMATION if the file /var/run/reboot-required is found after the upgrade. +Unattended-Upgrade::Automatic-Reboot "false"; + +// Automatically reboot even if users are logged in. +Unattended-Upgrade::Automatic-Reboot-WithUsers "false"; +EOF + +# ============================================================================= +# BACKUP SCRIPT & CRON (DISABLED - TODO: fix for non-interactive mode) +# ============================================================================= + +echo "Skipping backup script setup (will be configured manually later)..." +chmod 750 /opt/letsbe/scripts/backups.sh 2>/dev/null || true +mkdir -p /root/.config/rclone + +# NOTE: Backup cron disabled - crontab hangs in non-interactive mode +# To enable manually after setup: +# crontab -e +# Add: 0 2 * * * /bin/bash /opt/letsbe/scripts/backups.sh >> /var/log/letsbe-backups.log 2>&1 + +# ============================================================================= +# TOOL DEPLOYMENT +# ============================================================================= + +echo "[9/10] Deploying tools..." + +# Get list of available tools +mapfile -t available_tools < <(ls /opt/letsbe/stacks/*/docker-compose.yml 2>/dev/null | xargs -I {} dirname {} | xargs -I {} basename {}) + +if [[ -z "$TOOLS_TO_DEPLOY" ]]; then + echo "No tools specified. Skipping tool deployment." + echo "Available tools: ${available_tools[*]}" + echo "Use --tools to deploy tools later." +else + # Determine which tools to deploy + declare -a tools_list=() + + if [[ "$TOOLS_TO_DEPLOY" == "all" || "$TOOLS_TO_DEPLOY" == "a" ]]; then + tools_list=("${available_tools[@]}") + else + # Parse comma-separated list + IFS=',' read -ra requested_tools <<< "$TOOLS_TO_DEPLOY" + for tool in "${requested_tools[@]}"; do + tool=$(echo "$tool" | xargs) # Trim whitespace + + # Check if it's a number (index) + if [[ "$tool" =~ ^[0-9]+$ ]]; then + idx=$((tool - 1)) + if [[ $idx -ge 0 && $idx -lt ${#available_tools[@]} ]]; then + tools_list+=("${available_tools[$idx]}") + else + echo "Warning: Tool index $tool out of range, skipping." + fi + else + # It's a tool name + if [[ " ${available_tools[*]} " =~ " ${tool} " ]]; then + tools_list+=("$tool") + else + echo "Warning: Tool '$tool' not found, skipping." + fi + fi + done + fi + + echo "Deploying tools: ${tools_list[*]}" + + # Track deployed tools for SSL setup + DEPLOYED_TOOLS=() + + for tool_name in "${tools_list[@]}"; do + compose_file="/opt/letsbe/stacks/${tool_name}/docker-compose.yml" + + if [[ -f "$compose_file" ]]; then + # Copy .env file to centralized env directory if it exists + stack_env="/opt/letsbe/stacks/${tool_name}/.env" + central_env="/opt/letsbe/env/${tool_name}.env" + if [[ -f "$stack_env" ]]; then + cp "$stack_env" "$central_env" + chmod 640 "$central_env" + echo "Copied env file for $tool_name" + fi + + # Tool-specific pre-deployment setup + if [[ "$tool_name" == "nextcloud" ]]; then + echo "Creating Nextcloud bind mount directories..." + mkdir -p /opt/letsbe/config/nextcloud + mkdir -p /opt/letsbe/data/nextcloud + # Set appropriate ownership for www-data (uid 33 in Nextcloud container) + chown -R 33:33 /opt/letsbe/config/nextcloud + chown -R 33:33 /opt/letsbe/data/nextcloud + fi + + if [[ "$tool_name" == "sysadmin" ]]; then + echo "Cloning/updating sysadmin repository..." + SYSADMIN_DIR="/opt/letsbe/stacks/sysadmin" + SYSADMIN_REPO="https://code.letsbe.solutions/letsbe/letsbe-sysadmin.git" + + # Save our docker-compose.yml before clone + if [[ -f "${SYSADMIN_DIR}/docker-compose.yml" ]]; then + cp "${SYSADMIN_DIR}/docker-compose.yml" /tmp/sysadmin-compose.yml + fi + + # Clone or pull the repo + if [[ -d "${SYSADMIN_DIR}/.git" ]]; then + echo " Pulling latest changes..." + cd "${SYSADMIN_DIR}" && git pull origin main || git pull origin master + else + echo " Cloning repository..." + # Clone into temp, then move contents + rm -rf /tmp/letsbe-sysadmin + git clone "${SYSADMIN_REPO}" /tmp/letsbe-sysadmin + # Move repo contents to sysadmin dir (preserving our docker-compose) + cp -r /tmp/letsbe-sysadmin/* "${SYSADMIN_DIR}/" 2>/dev/null || true + cp -r /tmp/letsbe-sysadmin/.* "${SYSADMIN_DIR}/" 2>/dev/null || true + rm -rf /tmp/letsbe-sysadmin + fi + + # Restore our docker-compose.yml (with template variables) + if [[ -f /tmp/sysadmin-compose.yml ]]; then + cp /tmp/sysadmin-compose.yml "${SYSADMIN_DIR}/docker-compose.yml" + rm /tmp/sysadmin-compose.yml + fi + + echo " Building sysadmin image..." + docker-compose -f "$compose_file" build + fi + + echo "Starting $tool_name..." + docker-compose -f "$compose_file" up -d + + # Tool-specific post-deployment initialization + if [[ "$tool_name" == "chatwoot" ]]; then + echo "Initializing Chatwoot database (pgvector + migrations)..." + + # Get the customer prefix from the container name + CHATWOOT_POSTGRES=$(docker ps --format '{{.Names}}' | grep chatwoot-postgres | head -1) + CHATWOOT_RAILS=$(docker ps --format '{{.Names}}' | grep chatwoot-rails | head -1) + + if [[ -n "$CHATWOOT_POSTGRES" && -n "$CHATWOOT_RAILS" ]]; then + # Wait for Postgres to be ready + echo " Waiting for Postgres to be ready..." + for i in {1..30}; do + if docker exec "$CHATWOOT_POSTGRES" pg_isready -U chatwoot -d chatwoot_production >/dev/null 2>&1; then + echo " Postgres is ready." + break + fi + sleep 2 + done + + # Create pgvector extension + echo " Creating pgvector extension..." + docker exec "$CHATWOOT_POSTGRES" psql -U chatwoot -d chatwoot_production -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true + + # Wait for Rails container to be fully up + echo " Waiting for Rails container..." + sleep 10 + + # Run database migrations + echo " Running Chatwoot database prepare..." + docker exec "$CHATWOOT_RAILS" bundle exec rails db:chatwoot_prepare 2>&1 || echo " Note: db:chatwoot_prepare may have already run" + + echo " Chatwoot initialization complete." + else + echo " Warning: Could not find Chatwoot containers for initialization" + fi + fi + + # Link nginx config if exists + nginx_conf="/opt/letsbe/nginx/${tool_name}.conf" + if [[ -f "$nginx_conf" ]]; then + cp "$nginx_conf" /etc/nginx/sites-available/ + ln -sf /etc/nginx/sites-available/${tool_name}.conf /etc/nginx/sites-enabled/ + echo "Nginx config linked for $tool_name" + DEPLOYED_TOOLS+=("$tool_name") + else + echo "No nginx config for $tool_name (may not need one)" + fi + else + echo "Warning: docker-compose.yml not found for $tool_name" + fi + done + + # Restart nginx to apply new configs + systemctl restart nginx +fi + +# ============================================================================= +# SYSADMIN AGENT (Always deployed) +# ============================================================================= + +echo "[9.5/10] Deploying sysadmin agent..." + +SYSADMIN_COMPOSE="/opt/letsbe/stacks/sysadmin/docker-compose.yml" +if [[ -f "$SYSADMIN_COMPOSE" ]]; then + # Check if sysadmin is already running + if docker ps --format '{{.Names}}' | grep -q "agent$"; then + echo "Sysadmin agent already running, updating..." + fi + + SYSADMIN_DIR="/opt/letsbe/stacks/sysadmin" + SYSADMIN_REPO="https://code.letsbe.solutions/letsbe/letsbe-sysadmin.git" + + # Save our docker-compose.yml before clone + cp "${SYSADMIN_DIR}/docker-compose.yml" /tmp/sysadmin-compose.yml + + # Clone or pull the repo + if [[ -d "${SYSADMIN_DIR}/.git" ]]; then + echo " Pulling latest sysadmin changes..." + cd "${SYSADMIN_DIR}" && git pull origin main || git pull origin master || true + else + echo " Cloning sysadmin repository..." + rm -rf /tmp/letsbe-sysadmin + git clone "${SYSADMIN_REPO}" /tmp/letsbe-sysadmin + cp -r /tmp/letsbe-sysadmin/* "${SYSADMIN_DIR}/" 2>/dev/null || true + cp -r /tmp/letsbe-sysadmin/.* "${SYSADMIN_DIR}/" 2>/dev/null || true + rm -rf /tmp/letsbe-sysadmin + fi + + # Restore our docker-compose.yml (with template variables replaced) + cp /tmp/sysadmin-compose.yml "${SYSADMIN_DIR}/docker-compose.yml" + rm /tmp/sysadmin-compose.yml + + echo " Building sysadmin image..." + docker-compose -f "$SYSADMIN_COMPOSE" build + + echo " Starting sysadmin agent..." + docker-compose -f "$SYSADMIN_COMPOSE" up -d + + echo "Sysadmin agent deployed successfully." +else + echo "Warning: Sysadmin docker-compose.yml not found at $SYSADMIN_COMPOSE" +fi + +# Collect domains from deployed tools' nginx configs (for SSL) +SSL_DOMAINS=() +if [[ ${#DEPLOYED_TOOLS[@]} -gt 0 ]]; then + for tool_name in "${DEPLOYED_TOOLS[@]}"; do + tool_conf="/etc/nginx/sites-enabled/${tool_name}.conf" + if [[ -f "$tool_conf" ]]; then + # Extract server_name values (excluding placeholders and _) + while IFS= read -r domain; do + if [[ -n "$domain" && "$domain" != "_" && ! "$domain" =~ \{\{ ]]; then + SSL_DOMAINS+=("$domain") + fi + done < <(grep -h "server_name" "$tool_conf" 2>/dev/null | awk '{print $2}' | tr -d ';' | sort -u) + fi + done +fi + +# ============================================================================= +# SSL CERTIFICATE SETUP +# ============================================================================= + +echo "[10/10] Setting up SSL certificates..." + +# NOTE: Certbot cron disabled - crontab hangs in non-interactive mode +# Certbot installs its own systemd timer, so manual cron not needed +echo "Certbot renewal handled by systemd timer (certbot.timer)" + +if [[ "$SKIP_SSL" == "true" ]]; then + echo "Skipping SSL setup (--skip-ssl flag set)" +elif [[ ${#SSL_DOMAINS[@]} -eq 0 ]]; then + echo "No deployed tools with valid domains found." + echo "Skipping SSL setup. Either:" + echo " - No tools were deployed, or" + echo " - Templates not replaced (run env_setup.sh first with --domain parameter)" + echo "To manually setup SSL later: certbot --nginx -d yourdomain.com" +else + # Remove duplicates from SSL_DOMAINS + SSL_DOMAINS=($(printf '%s\n' "${SSL_DOMAINS[@]}" | sort -u)) + + # Add root domain if --root-ssl flag is set + if [[ "$ROOT_SSL" == "true" && -n "$DOMAIN" ]]; then + # Check if root domain is not already in the list + if [[ ! " ${SSL_DOMAINS[*]} " =~ " ${DOMAIN} " ]]; then + SSL_DOMAINS+=("$DOMAIN") + echo "Including root domain: $DOMAIN" + fi + fi + + echo "----" + echo "Setting up SSL certificates for deployed tools:" + for domain in "${SSL_DOMAINS[@]}"; do + echo " - $domain" + done + echo "" + echo "Make sure DNS entries point to this server IP before proceeding." + + # Derive email from domain parameter or use default + if [[ -n "$DOMAIN" ]]; then + SSL_EMAIL="administrator@${DOMAIN}" + else + # Try to extract base domain from first SSL domain + FIRST_DOMAIN="${SSL_DOMAINS[0]}" + # Extract base domain (remove subdomain) + BASE_DOMAIN=$(echo "$FIRST_DOMAIN" | awk -F. '{if(NF>2) print $(NF-1)"."$NF; else print $0}') + SSL_EMAIL="administrator@${BASE_DOMAIN}" + fi + + echo "Using email: $SSL_EMAIL" + + # Build domain arguments for certbot + DOMAIN_ARGS="" + for domain in "${SSL_DOMAINS[@]}"; do + DOMAIN_ARGS="$DOMAIN_ARGS -d $domain" + done + + # Run certbot non-interactively with specific domains + sudo certbot --nginx \ + --non-interactive \ + --agree-tos \ + --email "$SSL_EMAIL" \ + --redirect \ + $DOMAIN_ARGS \ + || echo "Certbot completed (some domains may have failed - check DNS)" +fi + +# ============================================================================= +# COMPLETION SUMMARY +# ============================================================================= + +echo "" +echo "----" +echo "Configured domains:" +for conf_file in /etc/nginx/sites-enabled/*.conf; do + if [[ -f "$conf_file" ]]; then + server_names=$(grep -E "^\s*server_name\s+" "$conf_file" 2>/dev/null | awk '{print $2}' | tr -d ';' | sort | uniq) + for server_name in $server_names; do + if [[ "$server_name" != "_" ]]; then + echo " - $server_name ($conf_file)" + fi + done + fi +done + +SERVER_IP=$(curl -4 -s ifconfig.co) + +echo "" +echo "==============================================" +echo " LetsBe Server Setup Complete" +echo "==============================================" +echo "" +echo "Server IP: $SERVER_IP" +echo "SSH Port: 22022" +echo "SSH User: stefan (key-based auth only)" +echo "" +echo "Portainer (if deployed): https://$SERVER_IP:9443" +echo "" +echo "Important:" +echo " - Configure rclone for backups: rclone config" +echo " - SSH port changed to 22022" +echo " - User 'stefan' has sudo access (key in /home/stefan/.ssh/)" +echo "" +echo "==============================================" + +# ============================================================================= +# MARK SETUP AS COMPLETE (before SSH restart) +# ============================================================================= + +touch /opt/letsbe/.setup_installed +echo "Setup marked as complete." + +# ============================================================================= +# RESTART SSH (MUST BE LAST - This will disconnect the session!) +# ============================================================================= + +echo "" +echo "Restarting SSH on port 22022... (connection will drop)" +echo "Reconnect with: ssh -i id_ed25519 -p 22022 stefan@$SERVER_IP" +echo "" + +# Small delay to ensure output is sent before disconnect +sleep 2 + +systemctl restart sshd diff --git a/script/stacks/activepieces/.env b/script/stacks/activepieces/.env new file mode 100644 index 0000000..4228cd5 --- /dev/null +++ b/script/stacks/activepieces/.env @@ -0,0 +1,31 @@ +## It's advisable to consult the documentation and use the tools/deploy.sh to generate the passwords, keys, instead of manually filling them. + +AP_ENGINE_EXECUTABLE_PATH=dist/packages/engine/main.js + +## Random Long Password (Optional for community edition) +AP_API_KEY={{ activepieces_api_key }} + +## 256 bit encryption key, 32 hex character +AP_ENCRYPTION_KEY={{ activepieces_encryption_key }} + +## JWT Secret +AP_JWT_SECRET={{ activepieces_jwt_secret }} + +AP_ENVIRONMENT=prod +AP_FRONTEND_URL=https://{{ domain_activepieces }} +AP_WEBHOOK_TIMEOUT_SECONDS=30 +AP_TRIGGER_DEFAULT_POLL_INTERVAL=5 +AP_POSTGRES_DATABASE=activepieces +AP_POSTGRES_HOST=postgres +AP_POSTGRES_PORT=5432 +AP_POSTGRES_USERNAME=activepieces-postgres +AP_POSTGRES_PASSWORD={{ activepieces_postgres_password }} +AP_EXECUTION_MODE=UNSANDBOXED +AP_REDIS_HOST=redis +AP_REDIS_PORT=6379 +AP_FLOW_TIMEOUT_SECONDS=600 +AP_TELEMETRY_ENABLED=true +AP_TEMPLATES_SOURCE_URL="https://cloud.activepieces.com/api/v1/flow-templates" +AP_PROJECT_RATE_LIMITER_ENABLED=false +AP_PIECES_SOURCE=DB +AP_FILE_STORAGE_LOCATION=DB \ No newline at end of file diff --git a/script/stacks/activepieces/docker-compose.yml b/script/stacks/activepieces/docker-compose.yml new file mode 100644 index 0000000..c912212 --- /dev/null +++ b/script/stacks/activepieces/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.0' + +services: + activepieces: + image: ghcr.io/activepieces/activepieces:0.39.2 + container_name: {{ customer }}-activepieces + restart: unless-stopped + labels: + - "diun.enable=true" + ## Enable the following line if you already use AP_EXECUTION_MODE with SANDBOXED or old activepieces, checking the breaking change documentation for mor> + ports: + - '3056:80' + depends_on: + - postgres + - redis + env_file: /opt/letsbe/env/activepieces.env + networks: + {{ customer }}-activepieces: + ipv4_address: 172.20.27.2 + postgres: + image: 'postgres:14.4' + container_name: activepieces-postgres + restart: unless-stopped + environment: + - 'POSTGRES_DB=${AP_POSTGRES_DATABASE}' + - 'POSTGRES_PASSWORD=${AP_POSTGRES_PASSWORD}' + - 'POSTGRES_USER=${AP_POSTGRES_USERNAME}' + volumes: + - activepieces_postgres_data:/var/lib/postgresql/data + networks: + {{ customer }}-activepieces: + ipv4_address: 172.20.27.3 + redis: + image: 'redis:7.0.7' + container_name: activepieces-redis + restart: unless-stopped + volumes: + - 'activepieces_redis_data:/data' + networks: + {{ customer }}-activepieces: + ipv4_address: 172.20.27.4 + +volumes: + activepieces_postgres_data: + activepieces_redis_data: + +networks: + {{ customer }}-activepieces: + ipam: + config: + - subnet: 172.20.27.0/28 + gateway: 172.20.27.1 diff --git a/script/stacks/baserow/docker-compose.yml b/script/stacks/baserow/docker-compose.yml new file mode 100644 index 0000000..0a440cf --- /dev/null +++ b/script/stacks/baserow/docker-compose.yml @@ -0,0 +1,61 @@ +version: '3.9' + +services: + baserow: + container_name: {{ customer }}-baserow + image: baserow/baserow:latest + restart: always + environment: + - BASEROW_PUBLIC_URL=https://{{ domain_baserow }} + - DATABASE_URL=postgresql://{{ baserow_postgres_user }}:{{ baserow_postgres_password }}@baserow-db:5432/baserow + - EMAIL_SMTP=True + - EMAIL_SMTP_USE_TLS=True + - EMAIL_SMTP_HOST= + - EMAIL_SMTP_PORT=587 + - FROM_EMAIL= + - EMAIL_SMTP_USER= + - EMAIL_SMTP_PASSWORD= + volumes: + - {{ customer }}-baserow-data:/baserow/data + - {{ customer }}-baserow-backups:/tmp/backups + ports: + - "127.0.0.1:3012:80" + #- "127.0.0.1:3013:443" + depends_on: + - baserow-db + networks: + {{ customer }}-baserow: + ipv4_address: 172.20.17.2 + + baserow-db: + container_name: {{ customer }}-baserow-db + image: postgres:15-alpine + restart: always + environment: + POSTGRES_DB: 'baserow' + POSTGRES_USER: '{{ baserow_postgres_user }}' + POSTGRES_PASSWORD: '{{ baserow_postgres_password }}' + volumes: + - {{ customer }}-baserow-postgres:/var/lib/postgresql/data + - {{ customer }}-baserow-backups:/tmp/backups + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + {{ customer }}-baserow: + ipv4_address: 172.20.17.3 + +networks: + {{ customer }}-baserow: + ipam: + driver: default + config: + - subnet: 172.20.17.0/28 + gateway: 172.20.17.1 + +volumes: + {{ customer }}-baserow-data: + {{ customer }}-baserow-postgres: + {{ customer }}-baserow-backups: diff --git a/script/stacks/calcom/.env b/script/stacks/calcom/.env new file mode 100644 index 0000000..caa1c31 --- /dev/null +++ b/script/stacks/calcom/.env @@ -0,0 +1,61 @@ +# Set this value to 'agree' to accept our license: +# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE +# +# Summary of terms: +# - The codebase has to stay open source, whether it was modified or not +# - You can not repackage or sell the codebase +# - Acquire a commercial license to remove these terms by emailing: license@cal.com +NEXT_PUBLIC_LICENSE_CONSENT= +LICENSE= + +# BASE_URL and NEXT_PUBLIC_APP_URL are both deprecated. Both are replaced with one variable, NEXT_PUBLIC_WEBAPP_URL +# BASE_URL=http://localhost:3000 +# NEXT_PUBLIC_APP_URL=http://localhost:3000 + +NEXT_PUBLIC_WEBAPP_URL=https://{{ domain_calcom }} + +# Configure NEXTAUTH_URL manually if needed, otherwise it will resolve to {NEXT_PUBLIC_WEBAPP_URL}/api/auth +# NEXTAUTH_URL=http://localhost:3000/api/auth + +# It is highly recommended that the NEXTAUTH_SECRET must be overridden and very unique +# Use `openssl rand -base64 32` to generate a key +NEXTAUTH_SECRET={{ calcom_nextauth_secret }} + +# Encryption key that will be used to encrypt CalDAV credentials, choose a random string, for example with `dd if=/dev/urandom bs=1K count=1 | md5sum` +CALENDSO_ENCRYPTION_KEY=md5sum + +# Deprecation note: JWT_SECRET is no longer used +# JWT_SECRET=secret + +POSTGRES_USER={{ calcom_postgres_user }} +POSTGRES_PASSWORD={{ calcom_postgres_password }} +POSTGRES_DB=calcom +DATABASE_HOST={{ customer }}-calcom-postgres:5432 +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}/${POSTGRES_DB} +GOOGLE_API_CREDENTIALS={} +#Fix calcom db migration issues +DATABASE_DIRECT_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}/${POSTGRES_DB} + +# Set this to '1' if you don't want Cal to collect anonymous usage +CALCOM_TELEMETRY_DISABLED=1 + +# Used for the Office 365 / Outlook.com Calendar integration +MS_GRAPH_CLIENT_ID= +MS_GRAPH_CLIENT_SECRET= + +# Used for the Zoom integration +ZOOM_CLIENT_ID= +ZOOM_CLIENT_SECRET= + +# E-mail settings +# Configures the global From: header whilst sending emails. +EMAIL_FROM=system@{{ domain }} +SUPPORT_MAIL_ADDRESS=support@{{ domain }} +# Configure SMTP settings (@see https://nodemailer.com/smtp/). +EMAIL_SERVER_HOST=mail.{{ domain }} +EMAIL_SERVER_PORT=587 +EMAIL_SERVER_USER=system@{{ domain }} +EMAIL_SERVER_PASSWORD= +#EMAIL_SERVER_SECURE=false + +NODE_ENV=production \ No newline at end of file diff --git a/script/stacks/calcom/docker-compose.yml b/script/stacks/calcom/docker-compose.yml new file mode 100644 index 0000000..c4973f5 --- /dev/null +++ b/script/stacks/calcom/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.8' + +services: + calcom-postgres: + container_name: {{ customer }}-calcom-postgres + image: postgres:16 #original postgres + restart: always + volumes: + - {{ customer }}-calcom-postgres:/var/lib/postgresql/data/ + - {{ customer }}-calcom-backups:/tmp/backups + env_file: /opt/letsbe/env/calcom.env + networks: + {{ customer }}-calcom: + ipv4_address: 172.20.18.2 + + calcom: + container_name: {{ customer }}-calcom + image: calcom/cal.com:latest + restart: always + labels: + - "diun.enable=true" + ports: + - '127.0.0.1:3018:3000' + env_file: /opt/letsbe/env/calcom.env + environment: + - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}/${POSTGRES_DB} + depends_on: + - calcom-postgres + networks: + {{ customer }}-calcom: + ipv4_address: 172.20.18.3 + +networks: + {{ customer }}-calcom: + ipam: + driver: default + config: + - subnet: 172.20.18.0/28 + gateway: 172.20.18.1 + +volumes: + {{ customer }}-calcom-postgres: + {{ customer }}-calcom-backups: diff --git a/script/stacks/chatwoot/.env b/script/stacks/chatwoot/.env new file mode 100644 index 0000000..a17d2f7 --- /dev/null +++ b/script/stacks/chatwoot/.env @@ -0,0 +1,239 @@ +SECRET_KEY_BASE={{ chatwoot_secret_key_base }} + +# Replace with the URL you are planning to use for your app +FRONTEND_URL=https://{{ domain_chatwoot }} +# To use a dedicated URL for help center pages +HELPCENTER_URL=https://{{ domain_chatwoot_helpdesk }} + +# If the variable is set, all non-authenticated pages would fallback to the default locale. +# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en +# DEFAULT_LOCALE=en + +# If you plan to use CDN for your assets, set Asset CDN Host +ASSET_CDN_HOST= + +# Force all access to the app over SSL, default is set to false +FORCE_SSL=false + +# This lets you control new sign ups on your chatwoot installation +# true : default option, allows sign ups +# false : disables all the end points related to sign ups +# api_only: disables the UI for signup, but you can create sign ups via the account apis +ENABLE_ACCOUNT_SIGNUP=false + +# Redis config +REDIS_URL=redis://:{{ chatwoot_redis_password }}@redis:6379 +# If you are using docker-compose, set this variable's value to be any string, +# which will be the password for the redis service running inside the docker-compose +# to make it secure +REDIS_PASSWORD={{ chatwoot_redis_password }} +# Redis Sentinel can be used by passing list of sentinel host and ports e,g. sentinel_host1:port1,sentinel_host2:port2 +REDIS_SENTINELS= +# Redis sentinel master name is required when using sentinel, default value is "mymaster". +# You can find list of master using "SENTINEL masters" command +REDIS_SENTINEL_MASTER_NAME= + +# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels +# Use the following environment variable to customize passwords for sentinels. +# Use empty string if sentinels are configured with out passwords +# REDIS_SENTINEL_PASSWORD= + +# Redis premium breakage in heroku fix +# enable the following configuration +# ref: https://github.com/chatwoot/chatwoot/issues/2420 +# REDIS_OPENSSL_VERIFY_MODE=none + +# Postgres Database config variables +# You can leave POSTGRES_DATABASE blank. The default name of +# the database in the production environment is chatwoot_production +POSTGRES_DATABASE=chatwoot_production +POSTGRES_HOST=postgres +POSTGRES_USERNAME={{ chatwoot_postgres_username }} +POSTGRES_PASSWORD={{ chatwoot_postgres_password }} +RAILS_ENV=production +# Changes the Postgres query timeout limit. The default is 14 seconds. Modify only when required. +# POSTGRES_STATEMENT_TIMEOUT=14s +RAILS_MAX_THREADS=5 + +# The email from which all outgoing emails are sent +# could user either `email@yourdomain.com` or `BrandName ` +MAILER_SENDER_EMAIL={{ company_name }} + +#SMTP domain key is set up for HELO checking +SMTP_DOMAIN=mail.{{ domain }} +# Set the value to "mailhog" if using docker-compose for development environments, +# Set the value as "localhost" or your SMTP address in other environments +# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix) +SMTP_ADDRESS=support@{{ domain }} +SMTP_PORT=587 +SMTP_USERNAME=support@{{ domain }} # Optional, only if SMTP server requires authentication +SMTP_PASSWORD= # Optional, only if SMTP server requires authentication +# plain,login,cram_md5 +SMTP_AUTHENTICATION=login +SMTP_ENABLE_STARTTLS_AUTO=true +# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html +SMTP_OPENSSL_VERIFY_MODE=peer +# Comment out the following environment variables if required by your SMTP server +SMTP_TLS=true +SMTP_SSL= + +# Mail Incoming +# This is the domain set for the reply emails when conversation continuity is enabled +MAILER_INBOUND_EMAIL_DOMAIN={{ domain }} +# Set this to appropriate ingress channel with regards to incoming emails +# Possible values are : +# relay for Exim, Postfix, Qmail +# mailgun for Mailgun +# mandrill for Mandrill +# postmark for Postmark +# sendgrid for Sendgrid +RAILS_INBOUND_EMAIL_SERVICE=relay +# Use one of the following based on the email ingress service +# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html +RAILS_INBOUND_EMAIL_PASSWORD= {{ chatwoot_rails_inbound_email_password }} +MAILGUN_INGRESS_SIGNING_KEY= +MANDRILL_INGRESS_API_KEY= + +# Storage +ACTIVE_STORAGE_SERVICE=local + +# Amazon S3 +# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage +S3_BUCKET_NAME= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION= + +# Log settings +# Disable if you want to write logs to a file +RAILS_LOG_TO_STDOUT=true +LOG_LEVEL=info +LOG_SIZE=500 +# Configure this environment variable if you want to use lograge instead of rails logger +#LOGRAGE_ENABLED=true + +### This environment variables are only required if you are setting up social media channels + +# Facebook +# documentation: https://www.chatwoot.com/docs/facebook-setup +FB_VERIFY_TOKEN= +FB_APP_SECRET= +FB_APP_ID= + +# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard +IG_VERIFY_TOKEN= + +# Twitter +# documentation: https://www.chatwoot.com/docs/twitter-app-setup +TWITTER_APP_ID= +TWITTER_CONSUMER_KEY= +TWITTER_CONSUMER_SECRET= +TWITTER_ENVIRONMENT= + +#slack integration +SLACK_CLIENT_ID= +SLACK_CLIENT_SECRET= + +# Google OAuth +GOOGLE_OAUTH_CLIENT_ID= +GOOGLE_OAUTH_CLIENT_SECRET= +GOOGLE_OAUTH_CALLBACK_URL= + +### Change this env variable only if you are using a custom build mobile app +## Mobile app env variables +IOS_APP_ID=L7YLMN4634.com.chatwoot.app +ANDROID_BUNDLE_ID=com.chatwoot.app + +# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section) +ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:D4:5D:D4:53:F8:3B:FB:D3:C6:28:64:1D:AA:08:1E:D8 + +### Smart App Banner +# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html +# You can find your app-id in https://itunesconnect.apple.com +#IOS_APP_IDENTIFIER=1495796682 + +## Push Notification +## generate a new key value here : https://d3v.one/vapid-key-generator/ +# VAPID_PUBLIC_KEY= +# VAPID_PRIVATE_KEY= +# +# for mobile apps +# FCM_SERVER_KEY= + +### APM and Error Monitoring configurations +## Elastic APM +## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html +# ELASTIC_APM_SERVER_URL= +# ELASTIC_APM_SECRET_TOKEN= + +## Sentry +# SENTRY_DSN= + +## LogRocket +# LOG_ROCKET_PROJECT_ID=xxxxx/some-project + +# MICROSOFT CLARITY +# MS_CLARITY_TOKEN=xxxxxxxxx + +## Scout +## https://scoutapm.com/docs/ruby/configuration +# SCOUT_KEY=YOURKEY +# SCOUT_NAME=YOURAPPNAME (Production) +# SCOUT_MONITOR=true + +## NewRelic +# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/ +# NEW_RELIC_LICENSE_KEY= +# Set this to true to allow newrelic apm to send logs. +# This is turned off by default. +# NEW_RELIC_APPLICATION_LOGGING_ENABLED= + +## Datadog +## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables +# DD_TRACE_AGENT_URL= + +# MaxMindDB API key to download GeoLite2 City database +# IP_LOOKUP_API_KEY= + +## Rack Attack configuration +## To prevent and throttle abusive requests +# ENABLE_RACK_ATTACK=true + +## Running chatwoot as an API only server +## setting this value to true will disable the frontend dashboard endpoints +# CW_API_ONLY_SERVER=false + +## Development Only Config +# if you want to use letter_opener for local emails +# LETTER_OPENER=true +# meant to be used in github codespaces +# WEBPACKER_DEV_SERVER_PUBLIC= + +# If you want to use official mobile app, +# the notifications would be relayed via a Chatwoot server +ENABLE_PUSH_RELAY_SERVER=true + +# Stripe API key +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= + +# Set to true if you want to upload files to cloud storage using the signed url +# Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true. +DIRECT_UPLOADS_ENABLED= + +#MS OAUTH creds +AZURE_APP_ID= +AZURE_APP_SECRET= + +## Advanced configurations +## Change these values to fine tune performance +# control the concurrency setting of sidekiq +# SIDEKIQ_CONCURRENCY=10 + + +# AI powered features +## OpenAI key +OPENAI_API_KEY= + +# Sentiment analysis model file path +SENTIMENT_FILE_PATH= \ No newline at end of file diff --git a/script/stacks/chatwoot/docker-compose.yml b/script/stacks/chatwoot/docker-compose.yml new file mode 100644 index 0000000..012fa35 --- /dev/null +++ b/script/stacks/chatwoot/docker-compose.yml @@ -0,0 +1,121 @@ +version: '3' + +services: + rails: + image: chatwoot/chatwoot:latest + container_name: {{ customer }}-chatwoot-rails + env_file: /opt/letsbe/env/chatwoot.env + restart: always + labels: + - "diun.enable=true" + depends_on: + - postgres + - redis + ports: + - '127.0.0.1:3011:3000' + environment: + - NODE_ENV=production + - RAILS_ENV=production + - INSTALLATION_ENV=docker + entrypoint: docker/entrypoints/rails.sh + command: ['bundle', 'exec', 'rails', 's', '-p', '3000', '-b', '0.0.0.0'] + volumes: + - {{ customer }}-chatwoot-storage:/app/storage + - {{ customer }}-chatwoot-backups:/tmp/backups + networks: + {{ customer }}-chatwoot: + ipv4_address: 172.20.1.2 + + sidekiq: + container_name: {{ customer }}-chatwoot-sidekiq + image: chatwoot/chatwoot:latest + restart: always + env_file: /opt/letsbe/env/chatwoot.env + depends_on: + - postgres + - redis + environment: + - NODE_ENV=production + - RAILS_ENV=production + - INSTALLATION_ENV=docker + command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml'] + volumes: + - {{ customer }}-chatwoot-storage:/app/storage + networks: + {{ customer }}-chatwoot: + ipv4_address: 172.20.1.3 + + postgres: + container_name: {{ customer }}-chatwoot-postgres + image: pgvector/pgvector:pg16 + restart: always + ports: + - '127.0.0.1:3049:5432' + volumes: + - {{ customer }}-chatwoot-postgres:/var/lib/postgresql/data + - {{ customer }}-chatwoot-backups:/tmp/backups + environment: + - POSTGRES_DB=chatwoot_production + - POSTGRES_USER={{ chatwoot_postgres_username }} + # Please provide your own password. + - POSTGRES_PASSWORD={{ chatwoot_postgres_password }} + networks: + {{ customer }}-chatwoot: + ipv4_address: 172.20.1.4 + + redis: + image: redis:alpine + container_name: {{ customer }}-chatwoot-redis + restart: always + command: ["sh", "-c", "redis-server --requirepass \"$REDIS_PASSWORD\""] + env_file: /opt/letsbe/env/chatwoot.env + volumes: + - {{ customer }}-chatwoot-redis:/data + ports: + - '127.0.0.1:3050:6379' + networks: + {{ customer }}-chatwoot: + ipv4_address: 172.20.1.5 + + getmail: + image: python:3.12-alpine + container_name: {{ customer }}-chatwoot-getmail + restart: always + depends_on: + - rails + environment: + INGRESS_PASSWORD: ${RAILS_INBOUND_EMAIL_PASSWORD} + CHATWOOT_RELAY_URL: http://rails:3000/rails/action_mailbox/relay/inbound_emails + volumes: + - type: bind + source: /opt/letsbe/stacks/chatwoot/getmail + target: /opt/getmail + entrypoint: > + sh -c " + apk add --no-cache curl ca-certificates && + pip install --no-cache-dir getmail6 && + chmod +x /opt/getmail/import_mail_to_chatwoot || true && + while true; do + for f in /opt/getmail/getmailrc /opt/getmail/getmailrc-*; do + [ -f \"$f\" ] || continue + getmail --getmaildir /opt/getmail --rcfile \"$(basename \"$f\")\" --quiet > done + sleep 60 + done + " + networks: + {{ customer }}-chatwoot: + ipv4_address: 172.20.1.6 + +networks: + {{ customer }}-chatwoot: + ipam: + driver: default + config: + - subnet: 172.20.1.0/28 + gateway: 172.20.1.1 + +volumes: + {{ customer }}-chatwoot-storage: + {{ customer }}-chatwoot-postgres: + {{ customer }}-chatwoot-redis: + {{ customer }}-chatwoot-backups: diff --git a/script/stacks/diun-watchtower/diun.yml b/script/stacks/diun-watchtower/diun.yml new file mode 100644 index 0000000..5857a26 --- /dev/null +++ b/script/stacks/diun-watchtower/diun.yml @@ -0,0 +1,21 @@ +watch: + workers: 20 + schedule: "0 */6 * * *" + jitter: 30s + firstCheckNotif: true + +providers: + docker: + watchStopped: true + watchByDefault: false + +notif: + mail: + host: mail.{{ domain }} # your mail server (Poste in your case) + port: 465 # SSL port + ssl: true + insecureSkipVerify: false + username: updates@{{ domain }} # change to your sender address + password: ##EmailPassword # use a strong app password + from: updates@{{ domain }} + to: matt@letsbe.solutions diff --git a/script/stacks/diun-watchtower/docker-compose.yml b/script/stacks/diun-watchtower/docker-compose.yml new file mode 100644 index 0000000..93e9bed --- /dev/null +++ b/script/stacks/diun-watchtower/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.7" + +services: + diun: + container_name: {{ customer }}-diun + image: crazymax/diun:latest + command: serve + labels: + - "diun.enable=true" + volumes: + - ./data:/data + - ./diun.yml:/diun.yml:ro + - /var/run/docker.sock:/var/run/docker.sock + environment: + - TZ=Europe/Paris + - DIUN_CONFIG=/diun.yml + - LOG_LEVEL=info + - LOG_JSON=false + restart: always diff --git a/script/stacks/documenso/.env b/script/stacks/documenso/.env new file mode 100644 index 0000000..098bd86 --- /dev/null +++ b/script/stacks/documenso/.env @@ -0,0 +1,47 @@ +# Database Settings +POSTGRES_USER={{ documenso_postgres_user }} +POSTGRES_PASSWORD={{ documenso_postgres_password }} +POSTGRES_DB=documenso_db +POSTGRES_PORT=5432 + +# Documenso App Settings +DOCUMENSO_PORT=3020 +NEXTAUTH_URL=https://{{ domain_documenso }} +NEXTAUTH_SECRET={{ documenso_nextauth_secret }} +NEXT_PRIVATE_ENCRYPTION_KEY={{ documenso_encryption_key }} +NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY={{ documenso_encryption_secondary_key }} +NEXT_PRIVATE_GOOGLE_CLIENT_ID= +NEXT_PRIVATE_GOOGLE_CLIENT_SECRET= +NEXT_PUBLIC_WEBAPP_URL=https://{{ domain_documenso }} +NEXT_PUBLIC_MARKETING_URL=https://{{ domain }} +NEXT_PRIVATE_DATABASE_URL=postgres://{{ documenso_postgres_user }}:{{ documenso_postgres_password }}@{{ customer }}-documenso-db:5432/documenso_db +NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://{{ documenso_postgres_user }}:{{ documenso_postgres_password }}@{{ customer }}-documenso-db:5432/documenso_db +NEXT_PUBLIC_UPLOAD_TRANSPORT=db +NEXT_PRIVATE_UPLOAD_ENDPOINT=https://{{ domain_s3 }} +NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true +NEXT_PRIVATE_UPLOAD_REGION=eu-central-1 +NEXT_PRIVATE_UPLOAD_BUCKET=signatures +NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID={{ minio_root_user }} +NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY={{ minio_root_password }} +NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth +NEXT_PRIVATE_SMTP_HOST={{ domain_poste }} +NEXT_PRIVATE_SMTP_PORT=465 +NEXT_PRIVATE_SMTP_USERNAME=noreply@{{ domain }} +NEXT_PRIVATE_SMTP_PASSWORD= +NEXT_PRIVATE_SMTP_APIKEY_USER= +NEXT_PRIVATE_SMTP_APIKEY= +NEXT_PRIVATE_SMTP_SECURE=true +NEXT_PRIVATE_SMTP_FROM_NAME="{{ company_name }} Signatures" +NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@{{ domain }} +NEXT_PRIVATE_SMTP_SERVICE= +NEXT_PRIVATE_RESEND_API_KEY= +NEXT_PRIVATE_MAILCHANNELS_API_KEY= +NEXT_PRIVATE_MAILCHANNELS_ENDPOINT= +NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN= +NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR= +NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY= +NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=50MB +NEXT_PUBLIC_POSTHOG_KEY= +NEXT_PUBLIC_DISABLE_SIGNUP=true +NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/certificate.p12 +NEXT_PRIVATE_SIGNING_PASSPHRASE= diff --git a/script/stacks/documenso/docker-compose.yml b/script/stacks/documenso/docker-compose.yml new file mode 100644 index 0000000..0c373d9 --- /dev/null +++ b/script/stacks/documenso/docker-compose.yml @@ -0,0 +1,60 @@ +version: "3.8" + +services: + database: + container_name: {{ customer }}-documenso-db + image: postgres:15 + restart: always + env_file: + - /opt/letsbe/env/documenso.env + environment: + - POSTGRES_USER={{ documenso_postgres_user }} + - POSTGRES_PASSWORD={{ documenso_postgres_password }} + - POSTGRES_DB=documenso_db + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U {{ documenso_postgres_user }} -d documenso_db'] + interval: 10s + timeout: 5s + retries: 5 + volumes: + - {{ customer }}-documenso-database:/var/lib/postgresql/data + - {{ customer }}-documenso-backups:/tmp/backups + networks: + {{ customer }}-documenso: + ipv4_address: 172.20.29.2 + ports: + - "127.0.0.1:5432:5432" + + documenso: + container_name: {{ customer }}-documenso-app + image: documenso/documenso:latest + restart: always + depends_on: + database: + condition: service_healthy + env_file: + - /opt/letsbe/env/documenso.env + environment: + - PORT=3020 + - NEXT_PRIVATE_INTERNAL_WEBAPP_URL=https://{{ domain_documenso }} + ports: + - "127.0.0.1:3020:3020" + volumes: + - /opt/documenso/certificate.p12:/opt/documenso/certificate.p12 + - /etc/localtime:/etc/localtime:ro + networks: + {{ customer }}-documenso: + ipv4_address: 172.20.29.3 + labels: + - "diun.enable=true" + +networks: + {{ customer }}-documenso: + driver: bridge + ipam: + config: + - subnet: 172.20.29.0/28 + +volumes: + {{ customer }}-documenso-database: + {{ customer }}-documenso-backups: diff --git a/script/stacks/ghost/config.production.json b/script/stacks/ghost/config.production.json new file mode 100644 index 0000000..ff1ee20 --- /dev/null +++ b/script/stacks/ghost/config.production.json @@ -0,0 +1,48 @@ +{ + "url": "https://{{ domain_ghost }}", + "server": { + "host": "0.0.0.0", + "port": 2368 + }, + "database": { + "client": "mysql", + "connection": { + "host": "{{ customer }}-ghost-db", + "user": "root", + "password": "{{ ghost_mysql_password }}", + "database": "ghost" + } + }, + "mail": { + "transport": "SMTP", + "options": { + "service": "Mailgun", + "host": "{{ domain_poste }}", + "port": 465, + "secure": true, + "auth": { + "user": "ghost@{{ domain }}", + "pass": "" + } + } + }, + "storage": { + "active": "s3", + "s3": { + "accessKeyId": "{{ ghost_s3_access_key }}", + "secretAccessKey": "{{ ghost_s3_secret_key }}", + "region": "eu-central-1", + "bucket": "ghost", + "endpoint": "https://{{ domain_s3 }}", + "forcePathStyle": true + } + }, + "logging": { + "level": "info", + "transports": ["file", "stdout"] + }, + "process": "systemd", + "paths": { + "contentPath": "/var/lib/ghost/content" + } +} diff --git a/script/stacks/ghost/docker-compose.yml b/script/stacks/ghost/docker-compose.yml new file mode 100644 index 0000000..5c6a7e3 --- /dev/null +++ b/script/stacks/ghost/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.8' + +services: + ghost: + container_name: {{ customer }}-ghost + image: ghost:alpine + restart: always + ports: + - "127.0.0.1:2368:2368" + environment: + # see https://ghost.org/docs/config/#configuration-options + database__client: mysql + database__connection__host: {{ customer }}-ghost-db + database__connection__user: root + database__connection__password: {{ ghost_mysql_password }} + database__connection__database: ghost + url: https://{{ domain_ghost }} + volumes: + - {{ customer }}-ghost-data:/var/lib/ghost/content + - ./config.production.json:/var/lib/ghost/config.production.json + networks: + {{ customer }}-ghost: + ipv4_address: 172.20.30.2 + depends_on: + - ghost-db + labels: + - "diun.enable=true" + + ghost-db: + container_name: {{ customer }}-ghost-db + image: mysql:8.0 + restart: always + environment: + MYSQL_ROOT_PASSWORD: {{ ghost_mysql_password }} + MYSQL_DATABASE: ghost + volumes: + - {{ customer }}-ghost-db:/var/lib/mysql + - {{ customer }}-ghost-backups:/tmp/backups + networks: + {{ customer }}-ghost: + ipv4_address: 172.20.30.3 + +networks: + {{ customer }}-ghost: + driver: bridge + ipam: + config: + - subnet: 172.20.30.0/28 + +volumes: + {{ customer }}-ghost-data: + {{ customer }}-ghost-db: + {{ customer }}-ghost-backups: diff --git a/script/stacks/gitea-drone/docker-compose.yml b/script/stacks/gitea-drone/docker-compose.yml new file mode 100644 index 0000000..e2bcd6d --- /dev/null +++ b/script/stacks/gitea-drone/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.9' + +services: + drone_gitea: + container_name: {{ customer }}-drone_gitea + image: drone/drone:2 + restart: always + labels: + - "diun.enable=true" + volumes: + - {{ customer }}-drone-gitea-data:/data + - {{ customer }}-drone-gitea-backups:/tmp/backups + ports: + - "127.0.0.1:3009:80" + # - "127.0.0.1:3010:443" + environment: + DRONE_GITEA_SERVER: 'https://{{ domain_gitea }}' + DRONE_GITEA_CLIENT_ID: '' + DRONE_GITEA_CLIENT_SECRET: '' + DRONE_RPC_SECRET: '{{ drone_gitea_rpc_secret }}' + DRONE_SERVER_HOST: '{{ domain_gitea_drone }}' + DRONE_SERVER_PROTO: https + networks: + {{ customer }}-drone-gitea: + ipv4_address: 172.20.2.2 + +networks: + {{ customer }}-drone-gitea: + ipam: + driver: default + config: + - subnet: 172.20.2.0/28 + gateway: 172.20.2.1 + +volumes: + {{ customer }}-drone-gitea-data: + {{ customer }}-drone-gitea-backups: diff --git a/script/stacks/gitea/docker-compose.yml b/script/stacks/gitea/docker-compose.yml new file mode 100644 index 0000000..9c2073a --- /dev/null +++ b/script/stacks/gitea/docker-compose.yml @@ -0,0 +1,76 @@ +### - POSTGRES - ### +version: '3.9' + +services: + gitea: + container_name: {{ customer }}-gitea + image: gitea/gitea:latest + restart: always + labels: + - "diun.enable=true" + volumes: + - {{ customer }}-gitea-data:/data + - {{ customer }}-gitea-backups:/tmp/backups + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "127.0.0.1:3007:3000" + - "3036:22" + environment: + USER_UID: 1000 + USER_GID: 1000 + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: {{ customer }}-gitea-db:5432 + GITEA__database__NAME: 'gitea' + GITEA__database__USER: '{{ gitea_postgres_user }}' + GITEA__database__PASSWD: '{{ gitea_postgres_password }}' + networks: + {{ customer }}-gitea: + ipv4_address: 172.20.3.2 + depends_on: + - gitea-db + + gitea-db: + container_name: {{ customer }}-gitea-db + image: postgres:14 + restart: always + environment: + POSTGRES_USER: '{{ gitea_postgres_user }}' + POSTGRES_PASSWORD: '{{ gitea_postgres_password }}' + POSTGRES_DB: 'gitea' + volumes: + - {{ customer }}-gitea-postgres:/var/lib/postgresql/data + - {{ customer }}-gitea-backups:/tmp/backups + networks: + {{ customer }}-gitea: + ipv4_address: 172.20.3.3 + + # runner: + # image: gitea/act_runner:latest-dind-rootless + # restart: always + # privileged: true + # volumes: + # - {{ customer }}-gitea-runner:/data + # environment: + # - GITEA_INSTANCE_URL=https://{{ domain_gitea }} + # - DOCKER_HOST=unix:///var/run/user/1000/docker.sock + # - GITEA_RUNNER_REGISTRATION_TOKEN= + # networks: + # {{ customer }}-gitea: + # ipv4_address: 172.20.3.5 + # depends_on: + # - gitea + +networks: + {{ customer }}-gitea: + ipam: + driver: default + config: + - subnet: 172.20.3.0/28 + gateway: 172.20.3.1 + +volumes: + {{ customer }}-gitea-data: + {{ customer }}-gitea-postgres: + {{ customer }}-gitea-runner: + {{ customer }}-gitea-backups: diff --git a/script/stacks/glitchtip/docker-compose.yml b/script/stacks/glitchtip/docker-compose.yml new file mode 100644 index 0000000..613fc5a --- /dev/null +++ b/script/stacks/glitchtip/docker-compose.yml @@ -0,0 +1,109 @@ +version: "3.8" + +services: + postgres: + container_name: {{ customer }}-glitchtip-postgres + image: postgres:15 + ports: + - "127.0.0.1:3046:5432" + environment: + #POSTGRES_HOST_AUTH_METHOD: "trust" # Consider removing this and setting a password + POSTGRES_PASSWORD: '{{ glitchtip_database_password }}' + restart: always + volumes: + - {{ customer }}-glitchtip-postgres:/var/lib/postgresql/data + - {{ customer }}-glitchtip-backups:/tmp/backups + networks: + {{ customer }}-glitchtip: + ipv4_address: 172.20.4.2 + + redis: + image: redis + container_name: {{ customer }}-glitchtip_redis + restart: always + networks: + {{ customer }}-glitchtip: + ipv4_address: 172.20.4.3 + + web: + container_name: {{ customer }}-glitchtip-web + image: glitchtip/glitchtip + labels: + - "diun.enable=true" + depends_on: + - postgres + - redis + ports: + - "127.0.0.1:3017:8000" + environment: + DATABASE_URL: 'postgres://postgres:{{ glitchtip_database_password }}@{{ customer }}-glitchtip-postgres:5432/postgres' + SECRET_KEY: '{{ glitchtip_secret_key }}' # best to run openssl rand -hex 32 + PORT: 8000 + #EMAIL_URL: 'consolemail://email:password@smtp-url:port' # Example smtp://email:password@smtp_url:port https://glitchtip.com/documentation/install#configuration + GLITCHTIP_DOMAIN: 'https://{{ domain_glitchtip }}' # Change this to your domain + DEFAULT_FROM_EMAIL: 'no-reply@{{ domain }}' # Change this to your email + #CELERY_WORKER_AUTOSCALE: "1,2" # Scale between 1 and 3 to prevent excessive memory usage. Change it or remove to set it to the number of cpu cores. + #CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000" + restart: always + volumes: + - {{ customer }}-glitchtip-uploads:/code/uploads + - {{ customer }}-glitchtip-backups:/tmp/backups + networks: + {{ customer }}-glitchtip: + ipv4_address: 172.20.4.4 + + worker: + container_name: {{ customer }}-glitchtip-worker + image: glitchtip/glitchtip + command: ./bin/run-celery-with-beat.sh + depends_on: + - postgres + - redis + environment: + DATABASE_URL: 'postgres://postgres:{{ glitchtip_database_password }}@{{ customer }}-glitchtip-postgres:5432/postgres' + SECRET_KEY: '{{ glitchtip_secret_key }}' # best to run openssl rand -hex 32 + PORT: 8000 + #EMAIL_URL: 'consolemail://email:password@smtp-url:port' # Example smtp://email:password@smtp_url:port https://glitchtip.com/documentation/install#configuration + GLITCHTIP_DOMAIN: 'https://{{ domain_glitchtip }}' # Change this to your domain + DEFAULT_FROM_EMAIL: 'no-reply@{{ domain }}' # Change this to your email + #CELERY_WORKER_AUTOSCALE: "1,2" # Scale between 1 and 3 to prevent excessive memory usage. Change it or remove to set it to the number of cpu cores. + #CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000" + restart: always + volumes: + - {{ customer }}-glitchtip-uploads:/code/uploads + networks: + {{ customer }}-glitchtip: + ipv4_address: 172.20.4.5 + + migrate: + container_name: {{ customer }}-glitchtip-migrate + image: glitchtip/glitchtip + depends_on: + - postgres + - redis + command: "./manage.py migrate" + environment: + DATABASE_URL: 'postgres://postgres:{{ glitchtip_database_password }}@{{ customer }}-glitchtip-postgres:5432/postgres' + SECRET_KEY: '{{ glitchtip_secret_key }}' # best to run openssl rand -hex 32 + PORT: 8000 + #EMAIL_URL: 'consolemail://email:password@smtp-url:port' # Example smtp://email:password@smtp_url:port https://glitchtip.com/documentation/install#configuration + GLITCHTIP_DOMAIN: 'https://{{ domain_glitchtip }}' # Change this to your domain + DEFAULT_FROM_EMAIL: 'no-reply@{{ domain }}' # Change this to your email + #CELERY_WORKER_AUTOSCALE: "1,2" # Scale between 1 and 3 to prevent excessive memory usage. Change it or remove to set it to the number of cpu cores. + #CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000" + networks: + {{ customer }}-glitchtip: + ipv4_address: 172.20.4.6 + +networks: + {{ customer }}-glitchtip: + ipam: + driver: default + config: + - subnet: 172.20.4.0/28 + gateway: 172.20.4.1 + +volumes: + {{ customer }}-glitchtip-postgres: + {{ customer }}-glitchtip-uploads: + {{ customer }}-glitchtip-backups: diff --git a/script/stacks/html/docker-compose.yml b/script/stacks/html/docker-compose.yml new file mode 100644 index 0000000..a1c5dbb --- /dev/null +++ b/script/stacks/html/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.9' + +services: + html: + container_name: {{ customer }}-html-website + image: nginx + restart: always + labels: + - "diun.enable=true" + volumes: + - {{ customer }}-html:/usr/share/nginx/html:ro + - {{ customer }}-html-backups:/tmp/backups + ports: + - "127.0.0.1:3000:80" + networks: + {{ customer }}-html: + ipv4_address: 172.20.5.2 + +networks: + {{ customer }}-html: + ipam: + driver: default + config: + - subnet: 172.20.5.0/28 + gateway: 172.20.5.1 + +volumes: + {{ customer }}-html: + {{ customer }}-html-backups: diff --git a/script/stacks/keycloak/.env b/script/stacks/keycloak/.env new file mode 100644 index 0000000..9889b47 --- /dev/null +++ b/script/stacks/keycloak/.env @@ -0,0 +1,10 @@ +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD={{ keycloak_admin_password }} +KC_DB=postgres +KC_DB_URL=jdbc:postgresql://{{ customer }}-keycloak-db:5432/keycloak +KC_DB_USERNAME=keycloak +KC_DB_PASSWORD={{ keycloak_postgres_password }} +KC_HOSTNAME_STRICT=false +KC_PROXY=edge +KC_HTTP_RELATIVE_PATH=/ +KC_HEALTH_ENABLED=true diff --git a/script/stacks/keycloak/docker-compose.yml b/script/stacks/keycloak/docker-compose.yml new file mode 100644 index 0000000..0f5f898 --- /dev/null +++ b/script/stacks/keycloak/docker-compose.yml @@ -0,0 +1,54 @@ +version: '3.8' + +services: + postgres: + container_name: {{ customer }}-keycloak-db + image: postgres:14 + restart: always + volumes: + - {{ customer }}-keycloak-postgres:/var/lib/postgresql/data + - {{ customer }}-keycloak-backups:/tmp/backups + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: {{ keycloak_postgres_password }} + networks: + {{ customer }}-keycloak: + ipv4_address: 172.20.31.2 + + keycloak: + container_name: {{ customer }}-keycloak + image: quay.io/keycloak/keycloak:latest + restart: always + command: start + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://{{ customer }}-keycloak-db:5432/keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: {{ keycloak_postgres_password }} + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: {{ keycloak_admin_password }} + KC_HOSTNAME_STRICT: false + KC_PROXY: edge + KC_HTTP_RELATIVE_PATH: / + KC_HEALTH_ENABLED: true + depends_on: + - postgres + ports: + - "127.0.0.1:8080:8080" + networks: + {{ customer }}-keycloak: + ipv4_address: 172.20.31.3 + labels: + - "diun.enable=true" + +networks: + {{ customer }}-keycloak: + driver: bridge + ipam: + config: + - subnet: 172.20.31.0/28 + +volumes: + {{ customer }}-keycloak-postgres: + {{ customer }}-keycloak-backups: diff --git a/script/stacks/librechat/.env b/script/stacks/librechat/.env new file mode 100644 index 0000000..5736af4 --- /dev/null +++ b/script/stacks/librechat/.env @@ -0,0 +1,574 @@ +#=====================================================================# +# LibreChat Configuration # +#=====================================================================# +# Please refer to the reference documentation for assistance # +# with configuring your LibreChat environment. # +# # +# https://www.librechat.ai/docs/configuration/dotenv # +#=====================================================================# + +#==================================================# +# Server Configuration # +#==================================================# + +HOST=0.0.0.0 +PORT=3080 + +MONGO_URI=mongodb://127.0.0.1:27017/LibreChat + +DOMAIN_CLIENT=https://{{ domain_librechat }} +DOMAIN_SERVER=https://{{ domain_librechat }} + +NO_INDEX=true +# Use the address that is at most n number of hops away from the Express application. +# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left. +# A value of 0 means that the first untrusted address would be req.socket.remoteAddress, i.e. there is no reverse proxy. +# Defaulted to 1. +TRUST_PROXY=1 + +#===============# +# JSON Logging # +#===============# + +# Use when process console logs in cloud deployment like GCP/AWS +CONSOLE_JSON=false + +#===============# +# Debug Logging # +#===============# + +DEBUG_LOGGING=true +DEBUG_CONSOLE=false + +#=============# +# Permissions # +#=============# + +# UID=1000 +# GID=1000 + +#===============# +# Configuration # +#===============# +# Use an absolute path, a relative path, or a URL + +# CONFIG_PATH="/alternative/path/to/librechat.yaml" + +#===================================================# +# Endpoints # +#===================================================# + +# ENDPOINTS=openAI,assistants,azureOpenAI,google,gptPlugins,anthropic + +PROXY= + +#===================================# +# Known Endpoints - librechat.yaml # +#===================================# +# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints + +# ANYSCALE_API_KEY= +# APIPIE_API_KEY= +# COHERE_API_KEY= +# DEEPSEEK_API_KEY= +# DATABRICKS_API_KEY= +# FIREWORKS_API_KEY= +# GROQ_API_KEY= +# HUGGINGFACE_TOKEN= +# MISTRAL_API_KEY= +# OPENROUTER_KEY= +# PERPLEXITY_API_KEY= +# SHUTTLEAI_API_KEY= +# TOGETHERAI_API_KEY= +# UNIFY_API_KEY= +# XAI_API_KEY= + +#============# +# Anthropic # +#============# + +ANTHROPIC_API_KEY=user_provided +# ANTHROPIC_MODELS=claude-3-7-sonnet-latest,claude-3-7-sonnet-20250219,claude-3-5-haiku-20241022,claude-3-5-sonnet-20241022,claude-3-5-sonnet-latest,claude-3-5-sonnet-20240620,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307,claude-2.1,claude-2,claude-1.2,claude-1,claude-1-100k,claude-instant-1,claude-instant-1-100k +# ANTHROPIC_REVERSE_PROXY= + +#============# +# Azure # +#============# + +# Note: these variables are DEPRECATED +# Use the `librechat.yaml` configuration for `azureOpenAI` instead +# You may also continue to use them if you opt out of using the `librechat.yaml` configuration + +# AZURE_OPENAI_DEFAULT_MODEL=gpt-3.5-turbo # Deprecated +# AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4 # Deprecated +# AZURE_USE_MODEL_AS_DEPLOYMENT_NAME=TRUE # Deprecated +# AZURE_API_KEY= # Deprecated +# AZURE_OPENAI_API_INSTANCE_NAME= # Deprecated +# AZURE_OPENAI_API_DEPLOYMENT_NAME= # Deprecated +# AZURE_OPENAI_API_VERSION= # Deprecated +# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME= # Deprecated +# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME= # Deprecated +# PLUGINS_USE_AZURE="true" # Deprecated + +#=================# +# AWS Bedrock # +#=================# + +# BEDROCK_AWS_DEFAULT_REGION=us-east-1 # A default region must be provided +# BEDROCK_AWS_ACCESS_KEY_ID=someAccessKey +# BEDROCK_AWS_SECRET_ACCESS_KEY=someSecretAccessKey +# BEDROCK_AWS_SESSION_TOKEN=someSessionToken + +# Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you. +# BEDROCK_AWS_MODELS=anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0 + +# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns + +# Notes on specific models: +# The following models are not support due to not supporting streaming: +# ai21.j2-mid-v1 + +# The following models are not support due to not supporting conversation history: +# ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14 + +#============# +# Google # +#============# + +GOOGLE_KEY= + +# GOOGLE_REVERSE_PROXY= +# Some reverse proxies do not support the X-goog-api-key header, uncomment to pass the API key in Authorization header instead. +# GOOGLE_AUTH_HEADER=true + +# Gemini API (AI Studio) +# GOOGLE_MODELS=gemini-2.5-pro-exp-03-25,gemini-2.0-flash-exp,gemini-2.0-flash-thinking-exp-1219,gemini-exp-1121,gemini-exp-1114,gemini-1.5-flash-latest,gemini-1.0-pro,gemini-1.0-pro-001,gemini-1.0-pro-latest,gemini-1.0-pro-vision-latest,gemini-1.5-pro-latest,gemini-pro,gemini-pro-vision + +# Vertex AI +# GOOGLE_MODELS=gemini-1.5-flash-preview-0514,gemini-1.5-pro-preview-0514,gemini-1.0-pro-vision-001,gemini-1.0-pro-002,gemini-1.0-pro-001,gemini-pro-vision,gemini-1.0-pro + +# GOOGLE_TITLE_MODEL=gemini-pro + +# GOOGLE_LOC=us-central1 + +# Google Safety Settings +# NOTE: These settings apply to both Vertex AI and Gemini API (AI Studio) +# +# For Vertex AI: +# To use the BLOCK_NONE setting, you need either: +# (a) Access through an allowlist via your Google account team, or +# (b) Switch to monthly invoiced billing: https://cloud.google.com/billing/docs/how-to/invoiced-billing +# +# For Gemini API (AI Studio): +# BLOCK_NONE is available by default, no special account requirements. +# +# Available options: BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE +# +# GOOGLE_SAFETY_SEXUALLY_EXPLICIT=BLOCK_ONLY_HIGH +# GOOGLE_SAFETY_HATE_SPEECH=BLOCK_ONLY_HIGH +# GOOGLE_SAFETY_HARASSMENT=BLOCK_ONLY_HIGH +# GOOGLE_SAFETY_DANGEROUS_CONTENT=BLOCK_ONLY_HIGH +# GOOGLE_SAFETY_CIVIC_INTEGRITY=BLOCK_ONLY_HIGH + +#============# +# OpenAI # +#============# + +OPENAI_API_KEY= +# OPENAI_MODELS=o1,o1-mini,o1-preview,gpt-4o,gpt-4.5-preview,chatgpt-4o-latest,gpt-4o-mini,gpt-3.5-turbo-0125,gpt-3.5-turbo-0301,gpt-3.5-turbo,gpt-4,gpt-4-0613,gpt-4-vision-preview,gpt-3.5-turbo-0613,gpt-3.5-turbo-16k-0613,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview,gpt-3.5-turbo-1106,gpt-3.5-turbo-instruct,gpt-3.5-turbo-instruct-0914,gpt-3.5-turbo-16k + +DEBUG_OPENAI=false + +# TITLE_CONVO=false +# OPENAI_TITLE_MODEL=gpt-4o-mini + +# OPENAI_SUMMARIZE=true +# OPENAI_SUMMARY_MODEL=gpt-4o-mini + +# OPENAI_FORCE_PROMPT=true + +# OPENAI_REVERSE_PROXY= + +# OPENAI_ORGANIZATION= + +#====================# +# Assistants API # +#====================# + +ASSISTANTS_API_KEY= +# ASSISTANTS_BASE_URL= +# ASSISTANTS_MODELS=gpt-4o,gpt-4o-mini,gpt-3.5-turbo-0125,gpt-3.5-turbo-16k-0613,gpt-3.5-turbo-16k,gpt-3.5-turbo,gpt-4,gpt-4-0314,gpt-4-32k-0314,gpt-4-0613,gpt-3.5-turbo-0613,gpt-3.5-turbo-1106,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview + +#==========================# +# Azure Assistants API # +#==========================# + +# Note: You should map your credentials with custom variables according to your Azure OpenAI Configuration +# The models for Azure Assistants are also determined by your Azure OpenAI configuration. + +# More info, including how to enable use of Assistants with Azure here: +# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints/azure#using-assistants-with-azure + +#============# +# Plugins # +#============# + +# PLUGIN_MODELS=gpt-4o,gpt-4o-mini,gpt-4,gpt-4-turbo-preview,gpt-4-0125-preview,gpt-4-1106-preview,gpt-4-0613,gpt-3.5-turbo,gpt-3.5-turbo-0125,gpt-3.5-turbo-1106,gpt-3.5-turbo-0613 + +DEBUG_PLUGINS=true + +CREDS_KEY=f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0 +CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb + +# Azure AI Search +#----------------- +AZURE_AI_SEARCH_SERVICE_ENDPOINT= +AZURE_AI_SEARCH_INDEX_NAME= +AZURE_AI_SEARCH_API_KEY= + +AZURE_AI_SEARCH_API_VERSION= +AZURE_AI_SEARCH_SEARCH_OPTION_QUERY_TYPE= +AZURE_AI_SEARCH_SEARCH_OPTION_TOP= +AZURE_AI_SEARCH_SEARCH_OPTION_SELECT= + +# DALL·E +#---------------- +DALLE_API_KEY= +DALLE3_API_KEY= +DALLE2_API_KEY= +# DALLE3_SYSTEM_PROMPT= +# DALLE2_SYSTEM_PROMPT= +# DALLE_REVERSE_PROXY= +# DALLE3_BASEURL= +# DALLE2_BASEURL= + +# DALL·E (via Azure OpenAI) +# Note: requires some of the variables above to be set +#---------------- +# DALLE3_AZURE_API_VERSION= +# DALLE2_AZURE_API_VERSION= + +# Flux +#----------------- +FLUX_API_BASE_URL=https://api.us1.bfl.ai +# FLUX_API_BASE_URL = 'https://api.bfl.ml'; + +# Get your API key at https://api.us1.bfl.ai/auth/profile +# FLUX_API_KEY= + +# Google +#----------------- +GOOGLE_SEARCH_API_KEY= +GOOGLE_CSE_ID= + +# YOUTUBE +#----------------- +YOUTUBE_API_KEY= + +# SerpAPI +#----------------- +SERPAPI_API_KEY= + +# Stable Diffusion +#----------------- +SD_WEBUI_URL=http://host.docker.internal:7860 + +# Tavily +#----------------- +TAVILY_API_KEY= + +# Traversaal +#----------------- +TRAVERSAAL_API_KEY= + +# WolframAlpha +#----------------- +WOLFRAM_APP_ID= + +# Zapier +#----------------- +ZAPIER_NLA_API_KEY= + +#==================================================# +# Search # +#==================================================# + +SEARCH=true +MEILI_NO_ANALYTICS=true +MEILI_HOST=http://0.0.0.0:7700 +MEILI_MASTER_KEY=6211530205576eaa3d97d215d5c12813 + +# Optional: Disable indexing, useful in a multi-node setup +# where only one instance should perform an index sync. +# MEILI_NO_SYNC=true + +#==================================================# +# Speech to Text & Text to Speech # +#==================================================# + +STT_API_KEY= +TTS_API_KEY= + +#==================================================# +# RAG # +#==================================================# +# More info: https://www.librechat.ai/docs/configuration/rag_api + +#RAG_OPENAI_BASEURL=http://localhost:8000 +RAG_OPENAI_API_KEY= +RAG_USE_FULL_CONTEXT=true +EMBEDDINGS_PROVIDER=openai +EMBEDDINGS_MODEL=text-embedding-3-small +POSTGRES_DB=librechat +POSTGRES_USER={{ librechat_postgres_user}} +POSTGRES_PASSWORD={{ librechat_postgres_password }} +DEBUG_RAG_API=true + +#===================================================# +# User System # +#===================================================# + +#========================# +# Moderation # +#========================# + +OPENAI_MODERATION=false +OPENAI_MODERATION_API_KEY= +# OPENAI_MODERATION_REVERSE_PROXY= + +BAN_VIOLATIONS=true +BAN_DURATION=1000 * 60 * 60 * 2 +BAN_INTERVAL=20 + +LOGIN_VIOLATION_SCORE=1 +REGISTRATION_VIOLATION_SCORE=1 +CONCURRENT_VIOLATION_SCORE=1 +MESSAGE_VIOLATION_SCORE=1 +NON_BROWSER_VIOLATION_SCORE=20 + +LOGIN_MAX=7 +LOGIN_WINDOW=5 +REGISTER_MAX=5 +REGISTER_WINDOW=60 + +LIMIT_CONCURRENT_MESSAGES=true +CONCURRENT_MESSAGE_MAX=2 + +LIMIT_MESSAGE_IP=true +MESSAGE_IP_MAX=40 +MESSAGE_IP_WINDOW=1 + +LIMIT_MESSAGE_USER=false +MESSAGE_USER_MAX=40 +MESSAGE_USER_WINDOW=1 + +ILLEGAL_MODEL_REQ_SCORE=5 + +#========================# +# Balance # +#========================# + +# CHECK_BALANCE=false +# START_BALANCE=20000 # note: the number of tokens that will be credited after registration. + + +#========================# +# Registration and Login # +#========================# + +ALLOW_EMAIL_LOGIN=true +ALLOW_REGISTRATION=true +ALLOW_SOCIAL_LOGIN=true +ALLOW_SOCIAL_REGISTRATION=true +ALLOW_PASSWORD_RESET=false +# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out +ALLOW_UNVERIFIED_EMAIL_LOGIN=true + +SESSION_EXPIRY=1000 * 60 * 15 +REFRESH_TOKEN_EXPIRY=(1000 * 60 * 60 * 24) * 7 + +JWT_SECRET={{ librechat_jwt_secret }} +JWT_REFRESH_SECRET={{ librechat_jwt_refresh_secret }} + +# Discord +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_CALLBACK_URL=/oauth/discord/callback + +# Facebook +FACEBOOK_CLIENT_ID= +FACEBOOK_CLIENT_SECRET= +FACEBOOK_CALLBACK_URL=/oauth/facebook/callback + +# GitHub +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL=/oauth/github/callback +# GitHub Enterprise +# GITHUB_ENTERPRISE_BASE_URL= +# GITHUB_ENTERPRISE_USER_AGENT= + +# Google +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL=/oauth/google/callback + +# Apple +APPLE_CLIENT_ID= +APPLE_TEAM_ID= +APPLE_KEY_ID= +APPLE_PRIVATE_KEY_PATH= +APPLE_CALLBACK_URL=/oauth/apple/callback + +# OpenID +OPENID_CLIENT_ID= +OPENID_CLIENT_SECRET= +OPENID_ISSUER= +OPENID_SESSION_SECRET= +OPENID_SCOPE="openid profile email" +OPENID_CALLBACK_URL=/oauth/openid/callback +OPENID_REQUIRED_ROLE= +OPENID_REQUIRED_ROLE_TOKEN_KIND= +OPENID_REQUIRED_ROLE_PARAMETER_PATH= +# Set to determine which user info property returned from OpenID Provider to store as the User's username +OPENID_USERNAME_CLAIM= +# Set to determine which user info property returned from OpenID Provider to store as the User's name +OPENID_NAME_CLAIM= + +OPENID_BUTTON_LABEL= +OPENID_IMAGE_URL= +# Set to true to automatically redirect to the OpenID provider when a user visits the login page +# This will bypass the login form completely for users, only use this if OpenID is your only authentication method +OPENID_AUTO_REDIRECT=false + +# LDAP +LDAP_URL= +LDAP_BIND_DN= +LDAP_BIND_CREDENTIALS= +LDAP_USER_SEARCH_BASE= +#LDAP_SEARCH_FILTER="mail=" +LDAP_CA_CERT_PATH= +# LDAP_TLS_REJECT_UNAUTHORIZED= +# LDAP_STARTTLS= +# LDAP_LOGIN_USES_USERNAME=true +# LDAP_ID= +# LDAP_USERNAME= +# LDAP_EMAIL= +# LDAP_FULL_NAME= + +#========================# +# Email Password Reset # +#========================# + +EMAIL_SERVICE= +EMAIL_HOST= +EMAIL_PORT=25 +EMAIL_ENCRYPTION= +EMAIL_ENCRYPTION_HOSTNAME= +EMAIL_ALLOW_SELFSIGNED= +EMAIL_USERNAME= +EMAIL_PASSWORD= +EMAIL_FROM_NAME= +EMAIL_FROM=noreply@librechat.ai + +#========================# +# Firebase CDN # +#========================# + +FIREBASE_API_KEY= +FIREBASE_AUTH_DOMAIN= +FIREBASE_PROJECT_ID= +FIREBASE_STORAGE_BUCKET= +FIREBASE_MESSAGING_SENDER_ID= +FIREBASE_APP_ID= + +#========================# +# S3 AWS Bucket # +#========================# + +AWS_ENDPOINT_URL= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION= +AWS_BUCKET_NAME= + +#========================# +# Azure Blob Storage # +#========================# + +AZURE_STORAGE_CONNECTION_STRING= +AZURE_STORAGE_PUBLIC_ACCESS=false +AZURE_CONTAINER_NAME=files + +#========================# +# Shared Links # +#========================# + +ALLOW_SHARED_LINKS=true +ALLOW_SHARED_LINKS_PUBLIC=true + +#==============================# +# Static File Cache Control # +#==============================# + +# Leave commented out to use defaults: 1 day (86400 seconds) for s-maxage and 2 days (172800 seconds) for max-age +# NODE_ENV must be set to production for these to take effect +# STATIC_CACHE_MAX_AGE=172800 +# STATIC_CACHE_S_MAX_AGE=86400 + +# If you have another service in front of your LibreChat doing compression, disable express based compression here +# DISABLE_COMPRESSION=true + +#===================================================# +# UI # +#===================================================# + +APP_TITLE={{ customer }} AI +CUSTOM_FOOTER= +HELP_AND_FAQ_URL=https://{{ domain }} + +# SHOW_BIRTHDAY_ICON=true + +# Google tag manager id +#ANALYTICS_GTM_ID=user provided google tag manager id + +#===============# +# REDIS Options # +#===============# + +# REDIS_URI=10.10.10.10:6379 +# USE_REDIS=true + +# USE_REDIS_CLUSTER=true +# REDIS_CA=/path/to/ca.crt + +#==================================================# +# Others # +#==================================================# +# You should leave the following commented out # + +# NODE_ENV= + +# E2E_USER_EMAIL= +# E2E_USER_PASSWORD= + +#=====================================================# +# Cache Headers # +#=====================================================# +# Headers that control caching of the index.html # +# Default configuration prevents caching to ensure # +# users always get the latest version. Customize # +# only if you understand caching implications. # + +# INDEX_HTML_CACHE_CONTROL=no-cache, no-store, must-revalidate +# INDEX_HTML_PRAGMA=no-cache +# INDEX_HTML_EXPIRES=0 + +# no-cache: Forces validation with server before using cached version +# no-store: Prevents storing the response entirely +# must-revalidate: Prevents using stale content when offline + +#=====================================================# +# OpenWeather # +#=====================================================# +OPENWEATHER_API_KEY= \ No newline at end of file diff --git a/script/stacks/librechat/docker-compose.yml b/script/stacks/librechat/docker-compose.yml new file mode 100644 index 0000000..9f4e196 --- /dev/null +++ b/script/stacks/librechat/docker-compose.yml @@ -0,0 +1,96 @@ +services: + api: + # build: + # context: . + # dockerfile: Dockerfile.multi + # target: api-build + image: ghcr.io/danny-avila/librechat:v0.7.8 + container_name: {{ customer }}-librechat + labels: + - "diun.enable=true" + ports: + - 3080:3080 + depends_on: + - mongodb + - rag_api + restart: always + user: "${UID}:${GID}" + extra_hosts: + - "host.docker.internal:host-gateway" + env_file: + - /opt/letsbe/env/librechat.env + environment: + - HOST=0.0.0.0 + - NODE_ENV=production + - MONGO_URI=mongodb://mongodb:27017/LibreChat + - MEILI_HOST=http://meilisearch:7700 + - RAG_PORT=${RAG_PORT:-8000} + - RAG_API_URL=http://rag_api:${RAG_PORT:-8000} + - EMAIL_HOST=mail.{{ domain }} + - EMAIL_PORT=587 + - EMAIL_ENCRYPTION=starttls + - EMAIL_USERNAME=noreply@{{ domain }} + - EMAIL_PASSWORD=Q2WjzJ05525I0cmyxAYn57wKhRSumMHXnHN8 + - EMAIL_FROM_NAME={{customer}}AI + - EMAIL_FROM=noreply@{{ domain }} + volumes: + - type: bind + source: ./librechat.yaml + target: /app/librechat.yaml + - ./images:/app/client/public/images + - ./uploads:/app/uploads + - ./logs:/app/api/logs + + mongodb: + container_name: librechat-mongodb + # ports: # Uncomment this to access mongodb from outside docker, not safe in deployment + # - 27018:27017 + image: mongo + restart: always + user: "${UID}:${GID}" + volumes: + - ./data-node:/data/db + command: mongod --noauth + meilisearch: + container_name: librechat-meilisearch + image: getmeili/meilisearch:v1.12.3 + restart: always + user: "${UID}:${GID}" + # ports: # Uncomment this to access meilisearch from outside docker + # - 7700:7700 # if exposing these ports, make sure your master key is not the default value + env_file: + - /opt/letsbe/env/librechat.env + environment: + - MEILI_HOST=http://meilisearch:7700 + - MEILI_NO_ANALYTICS=true + volumes: + - ./meili_data_v1.12:/meili_data + vectordb: + image: ankane/pgvector:latest + environment: + POSTGRES_DB: librechat + POSTGRES_USER: {{ librechat_postgres_user }} + POSTGRES_PASSWORD: {{ librechat_postgres_password }} + restart: always + volumes: + - pgdata2:/var/lib/postgresql/data + rag_api: + image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest + environment: + - DB_HOST=vectordb + - RAG_PORT=8000 + restart: always + depends_on: + - vectordb + env_file: + - /opt/letsbe/env/librechat.env + ports: + - "8000:8000" + +networks: + # Declare the same network name as in the ActivePieces file, but mark as external + {{ customer }}-activepieces: + external: true + +volumes: + pgdata2: diff --git a/script/stacks/librechat/librechat.yaml b/script/stacks/librechat/librechat.yaml new file mode 100644 index 0000000..7e84937 --- /dev/null +++ b/script/stacks/librechat/librechat.yaml @@ -0,0 +1,318 @@ +# For more information, see the Configuration Guide: +# https://www.librechat.ai/docs/configuration/librechat_yaml + +# Configuration version (required) +version: 1.2.1 + +# Cache settings: Set to true to enable caching +cache: true + +# File strategy s3/firebase +# fileStrategy: "s3" + +# Custom interface configuration +interface: + customWelcome: "Welcome to {{ customer }} AI! Enjoy your experience." + # Privacy policy settings + privacyPolicy: + externalUrl: 'https://librechat.ai/privacy-policy' + openNewTab: true + + # Terms of service + termsOfService: + externalUrl: 'https://librechat.ai/tos' + openNewTab: true + modalAcceptance: true + modalTitle: "Terms of Service for LibreChat" + modalContent: | + # Terms and Conditions for LibreChat + + *Effective Date: February 18, 2024* + + Welcome to LibreChat, the informational website for the open-source AI chat platform, available at https://librechat.ai. These Terms of Service ("Terms") govern your use of our website and the services we offer. By accessing or using the Website, you agree to be bound by these Terms and our Privacy Policy, accessible at https://librechat.ai//privacy. + + ## 1. Ownership + + Upon purchasing a package from LibreChat, you are granted the right to download and use the code for accessing an admin panel for LibreChat. While you own the downloaded code, you are expressly prohibited from reselling, redistributing, or otherwise transferring the code to third parties without explicit permission from LibreChat. + + ## 2. User Data + + We collect personal data, such as your name, email address, and payment information, as described in our Privacy Policy. This information is collected to provide and improve our services, process transactions, and communicate with you. + + ## 3. Non-Personal Data Collection + + The Website uses cookies to enhance user experience, analyze site usage, and facilitate certain functionalities. By using the Website, you consent to the use of cookies in accordance with our Privacy Policy. + + ## 4. Use of the Website + + You agree to use the Website only for lawful purposes and in a manner that does not infringe the rights of, restrict, or inhibit anyone else's use and enjoyment of the Website. Prohibited behavior includes harassing or causing distress or inconvenience to any person, transmitting obscene or offensive content, or disrupting the normal flow of dialogue within the Website. + + ## 5. Governing Law + + These Terms shall be governed by and construed in accordance with the laws of the United States, without giving effect to any principles of conflicts of law. + + ## 6. Changes to the Terms + + We reserve the right to modify these Terms at any time. We will notify users of any changes by email. Your continued use of the Website after such changes have been notified will constitute your consent to such changes. + + ## 7. Contact Information + + If you have any questions about these Terms, please contact us at contact@librechat.ai. + + By using the Website, you acknowledge that you have read these Terms of Service and agree to be bound by them. + + endpointsMenu: true + modelSelect: true + parameters: true + sidePanel: true + presets: true + prompts: true + bookmarks: true + multiConvo: true + agents: true + +# Example Registration Object Structure (optional) +registration: + socialLogins: ['github', 'google', 'discord', 'openid', 'facebook', 'apple'] + # allowedDomains: + # - "gmail.com" + + +# Example Balance settings +# balance: +# enabled: false +# startBalance: 20000 +# autoRefillEnabled: false +# refillIntervalValue: 30 +# refillIntervalUnit: 'days' +# refillAmount: 10000 + +speech: + tts: + openai: + url: 'https://api.openai.com/v1' + apiKey: '' + model: 'tts-1-hd' + voices: ['alloy'] + +# + stt: + openai: + url: 'https://api.openai.com/v1' + apiKey: '' + model: 'whisper-1' + +# rateLimits: +# fileUploads: +# ipMax: 100 +# ipWindowInMinutes: 60 # Rate limit window for file uploads per IP +# userMax: 50 +# userWindowInMinutes: 60 # Rate limit window for file uploads per user +# conversationsImport: +# ipMax: 100 +# ipWindowInMinutes: 60 # Rate limit window for conversation imports per IP +# userMax: 50 +# userWindowInMinutes: 60 # Rate limit window for conversation imports per user + +# Example Actions Object Structure +actions: + allowedDomains: + - "swapi.dev" + - "librechat.ai" + - "google.com" + - "{{ domain_librechat }}" + - "{{ domain_activepieces }}" +# Example MCP Servers Object Structure +# mcpServers: +# everything: +# # type: sse # type can optionally be omitted +# url: http://localhost:3001/sse +# timeout: 60000 # 1 minute timeout for this server, this is the default timeout for MCP servers. +# puppeteer: +# type: stdio +# command: npx +# args: +# - -y +# - "@modelcontextprotocol/server-puppeteer" +# timeout: 300000 # 5 minutes timeout for this server +# filesystem: +# # type: stdio +# command: npx +# args: +# - -y +# - "@modelcontextprotocol/server-filesystem" +# - /home/user/LibreChat/ +# iconPath: /home/user/LibreChat/client/public/assets/logo.svg +# mcp-obsidian: +# command: npx +# args: +# - -y +# - "mcp-obsidian" +# - /path/to/obsidian/vault +#mcpServers: + #PortNimaraAI: + #type: sse + #url: "https://automation.portnimara.com/api/v1/mcp/d6br5VnJuHUPuzpFUGJEo/sse" + #command: npx + #args: + # - -y + # - mcp-remote + # - "https://automation.portnimara.com/api/v1/mcp/d6br5VnJuHUPuzpFUGJEo/sse" + +# Definition of custom endpoints +endpoints: + # assistants: + # disableBuilder: false # Disable Assistants Builder Interface by setting to `true` + # pollIntervalMs: 3000 # Polling interval for checking assistant updates + # timeoutMs: 180000 # Timeout for assistant operations + # # Should only be one or the other, either `supportedIds` or `excludedIds` + # supportedIds: ["asst_supportedAssistantId1", "asst_supportedAssistantId2"] + # # excludedIds: ["asst_excludedAssistantId"] + # # Only show assistants that the user created or that were created externally (e.g. in Assistants playground). + # # privateAssistants: false # Does not work with `supportedIds` or `excludedIds` + # # (optional) Models that support retrieval, will default to latest known OpenAI models that support the feature + # retrievalModels: ["gpt-4-turbo-preview"] + # # (optional) Assistant Capabilities available to all users. Omit the ones you wish to exclude. Defaults to list below. + # capabilities: ["code_interpreter", "retrieval", "actions", "tools", "image_vision"] + # agents: + # # (optional) Default recursion depth for agents, defaults to 25 + # recursionLimit: 50 + # # (optional) Max recursion depth for agents, defaults to 25 + # maxRecursionLimit: 100 + # # (optional) Disable the builder interface for agents + # disableBuilder: false + # # (optional) Agent Capabilities available to all users. Omit the ones you wish to exclude. Defaults to list below. + # capabilities: ["execute_code", "file_search", "actions", "tools"] + custom: + # Groq Example + - name: 'groq' + apiKey: '${GROQ_API_KEY}' + baseURL: 'https://api.groq.com/openai/v1/' + models: + default: + [ + 'llama3-70b-8192', + 'llama3-8b-8192', + 'llama2-70b-4096', + 'mixtral-8x7b-32768', + 'gemma-7b-it', + ] + fetch: false + titleConvo: true + titleModel: 'mixtral-8x7b-32768' + modelDisplayLabel: 'groq' + + # Mistral AI Example + - name: 'Mistral' # Unique name for the endpoint + # For `apiKey` and `baseURL`, you can use environment variables that you define. + # recommended environment variables: + apiKey: '${MISTRAL_API_KEY}' + baseURL: 'https://api.mistral.ai/v1' + + # Models configuration + models: + # List of default models to use. At least one value is required. + default: ['mistral-tiny', 'mistral-small', 'mistral-medium'] + # Fetch option: Set to true to fetch models from API. + fetch: true # Defaults to false. + + # Optional configurations + + # Title Conversation setting + titleConvo: true # Set to true to enable title conversation + + # Title Method: Choose between "completion" or "functions". + # titleMethod: "completion" # Defaults to "completion" if omitted. + + # Title Model: Specify the model to use for titles. + titleModel: 'mistral-tiny' # Defaults to "gpt-3.5-turbo" if omitted. + + # Summarize setting: Set to true to enable summarization. + # summarize: false + + # Summary Model: Specify the model to use if summarization is enabled. + # summaryModel: "mistral-tiny" # Defaults to "gpt-3.5-turbo" if omitted. + + # Force Prompt setting: If true, sends a `prompt` parameter instead of `messages`. + # forcePrompt: false + + # The label displayed for the AI model in messages. + modelDisplayLabel: 'Mistral' # Default is "AI" when not set. + + # Add additional parameters to the request. Default params will be overwritten. + # addParams: + # safe_prompt: true # This field is specific to Mistral AI: https://docs.mistral.ai/api/ + + # Drop Default params parameters from the request. See default params in guide linked below. + # NOTE: For Mistral, it is necessary to drop the following parameters or you will encounter a 422 Error: + dropParams: ['stop', 'user', 'frequency_penalty', 'presence_penalty'] + + # OpenRouter Example + - name: 'OpenRouter' + # For `apiKey` and `baseURL`, you can use environment variables that you define. + # recommended environment variables: + apiKey: '${OPENROUTER_KEY}' + baseURL: 'https://openrouter.ai/api/v1' + models: + default: ['meta-llama/llama-3-70b-instruct'] + fetch: true + titleConvo: true + titleModel: 'meta-llama/llama-3-70b-instruct' + # Recommended: Drop the stop parameter from the request as Openrouter models use a variety of stop tokens. + dropParams: ['stop'] + modelDisplayLabel: 'OpenRouter' + + # Portkey AI Example + - name: "Portkey" + apiKey: "dummy" + baseURL: 'https://api.portkey.ai/v1' + headers: + x-portkey-api-key: '${PORTKEY_API_KEY}' + x-portkey-virtual-key: '${PORTKEY_OPENAI_VIRTUAL_KEY}' + models: + default: ['gpt-4o-mini', 'gpt-4o', 'chatgpt-4o-latest'] + fetch: true + titleConvo: true + titleModel: 'current_model' + summarize: false + summaryModel: 'current_model' + forcePrompt: false + modelDisplayLabel: 'Portkey' + iconURL: https://images.crunchbase.com/image/upload/c_pad,f_auto,q_auto:eco,dpr_1/rjqy7ghvjoiu4cd1xjbf + +# fileConfig: +# endpoints: +# assistants: +# fileLimit: 5 +# fileSizeLimit: 10 # Maximum size for an individual file in MB +# totalSizeLimit: 50 # Maximum total size for all files in a single request in MB +# supportedMimeTypes: +# - "image/.*" +# - "application/pdf" +# openAI: +# disabled: false # Disables file uploading to the OpenAI endpoint +# default: +# totalSizeLimit: 20 +# YourCustomEndpointName: +# fileLimit: 2 +# fileSizeLimit: 5 +# serverFileSizeLimit: 100 # Global server file size limit in MB +# avatarSizeLimit: 2 # Limit for user avatar image size in MB +# # See the Custom Configuration Guide for more information on Assistants Config: +# # https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/assistants_endpoint + + +#modelSpecs: + # (optional) force the UI to only ever pick from this list + #enforce: false + #prioritize: true + + #list: + # - name: "port-nimara-agent" + #label: "{{ customer }} AI" + #default: true # ← makes it the default on new chats + #description: "Agent" + + #preset: + #endpoint: "agents" # ← use the Agents endpoint + #agent_id: "" # ← your actual agent’s ID \ No newline at end of file diff --git a/script/stacks/listmonk/config.toml b/script/stacks/listmonk/config.toml new file mode 100644 index 0000000..f14cbf8 --- /dev/null +++ b/script/stacks/listmonk/config.toml @@ -0,0 +1,15 @@ +[app] +address = "0.0.0.0:9000" +admin_username = "{{ listmonk_admin_username }}" +admin_password = "{{ listmonk_admin_password }}" + +[db] +host = "{{ customer }}-listmonk-db" +port = 5432 +user = "{{ listmonk_db_user }}" +password = "{{ listmonk_db_password }}" +database = "listmonk" +ssl_mode = "disable" +max_open = 25 +max_idle = 25 +max_lifetime = "300s" \ No newline at end of file diff --git a/script/stacks/listmonk/docker-compose.yml b/script/stacks/listmonk/docker-compose.yml new file mode 100644 index 0000000..8b6d44b --- /dev/null +++ b/script/stacks/listmonk/docker-compose.yml @@ -0,0 +1,57 @@ +version: '3.9' + +services: + listmonk-db: + container_name: {{ customer }}-listmonk-db + image: postgres:13 + restart: always + volumes: + - {{ customer }}-listmonk-postgresql:/var/lib/postgresql/data + - {{ customer }}-listmonk-backups:/tmp/backups + ports: + - "127.0.0.1:3037:5432" + environment: + POSTGRES_DB: listmonk + POSTGRES_USER: {{ listmonk_db_user }} + POSTGRES_PASSWORD: {{ listmonk_db_password }} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U {{ listmonk_db_user }} -d listmonk"] + interval: 10s + timeout: 5s + retries: 6 + networks: + {{ customer }}-listmonk: + ipv4_address: 172.20.6.2 + + listmonk-web: + container_name: {{ customer }}-listmonk-web + image: listmonk/listmonk:latest + restart: always + labels: + - "diun.enable=true" + command: [sh, -c, "yes | ./listmonk --install --config config.toml && ./listmonk --config config.toml"] + volumes: + - ./config.toml:/listmonk/config.toml + - {{ customer }}-listmonk-backups:/tmp/backups + ports: + - "127.0.0.1:3006:9000" + depends_on: + - listmonk-db + environment: + TZ: Etc/UTC + networks: + {{ customer }}-listmonk: + ipv4_address: 172.20.6.3 + + +networks: + {{ customer }}-listmonk: + ipam: + driver: default + config: + - subnet: 172.20.6.0/28 + gateway: 172.20.6.1 + +volumes: + {{ customer }}-listmonk-postgresql: + {{ customer }}-listmonk-backups: diff --git a/script/stacks/minio/docker-compose.yml b/script/stacks/minio/docker-compose.yml new file mode 100644 index 0000000..1f4b831 --- /dev/null +++ b/script/stacks/minio/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3' + +services: + minio: + image: minio/minio:latest + container_name: {{ customer }}-minio + restart: always + labels: + - "diun.enable=true" + volumes: + - {{ customer }}-minio-data:/data + environment: + - MINIO_ROOT_USER={{ minio_root_user }} + - MINIO_ROOT_PASSWORD={{ minio_root_password }} + command: server /data --console-address ":9001" + ports: + - "0.0.0.0:3058:9000" + - "0.0.0.0:3059:9001" + networks: + {{ customer }}-minio: + ipv4_address: 172.20.26.2 + +networks: + {{ customer }}-minio: + ipam: + driver: default + config: + - subnet: 172.20.26.0/28 + gateway: 172.20.26.1 + +volumes: + {{ customer }}-minio-data: diff --git a/script/stacks/n8n/docker-compose.yml b/script/stacks/n8n/docker-compose.yml new file mode 100644 index 0000000..4e3a4a5 --- /dev/null +++ b/script/stacks/n8n/docker-compose.yml @@ -0,0 +1,63 @@ +version: '3.8' + +services: + n8n-postgres: + container_name: {{ customer }}-n8n-postgres + restart: always + image: postgres:16 #original: postgres:latest + environment: + - POSTGRES_DB=n8n + - POSTGRES_USER={{ n8n_postgres_user }} + - POSTGRES_PASSWORD={{ n8n_postgres_password }} + volumes: + - {{ customer }}-n8n-postgres:/var/lib/postgresql/data + - {{ customer }}-n8n-backups:/tmp/backups + networks: + {{ customer }}-n8n: + ipv4_address: 172.20.8.2 + + n8n: + container_name: {{ customer }}-n8n + restart: always + image: docker.n8n.io/n8nio/n8n + labels: + - "diun.enable=true" + ports: + - "127.0.0.1:3025:5678" + environment: + - DB_TYPE=postgresdb + - DB_POSTGRESDB_DATABASE=n8n + - DB_POSTGRESDB_HOST=n8n-postgres + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_USER={{ n8n_postgres_user }} + #- DB_POSTGRESDB_SCHEMA=public + - DB_POSTGRESDB_PASSWORD={{ n8n_postgres_password }} + - N8N_EDITOR_BASE_URL=https://{{ domain_n8n }} + - N8N_EMAIL_MODE=smtp + - N8N_SMTP_SSL=false + - N8N_SMTP_HOST= + - N8N_SMTP_PORT= + - N8N_SMTP_USER= + - N8N_SMTP_PASS= + - N8N_SMTP_SENDER= + volumes: + - {{ customer }}-n8n-storage:/home/node/.n8n + - {{ customer }}-n8n-backups:/tmp/backups + links: + - n8n-postgres + networks: + {{ customer }}-n8n: + ipv4_address: 172.20.8.3 + +networks: + {{ customer }}-n8n: + ipam: + driver: default + config: + - subnet: 172.20.8.0/28 + gateway: 172.20.8.1 + +volumes: + {{ customer }}-n8n-postgres: + {{ customer }}-n8n-storage: + {{ customer }}-n8n-backups: diff --git a/script/stacks/nextcloud/docker-compose.yml b/script/stacks/nextcloud/docker-compose.yml new file mode 100644 index 0000000..793775b --- /dev/null +++ b/script/stacks/nextcloud/docker-compose.yml @@ -0,0 +1,130 @@ +version: '3.9' + +services: + db: + container_name: {{ customer }}-nextcloud-postgres + image: postgres:16-alpine #original postgres:alpine + restart: always + volumes: + - {{ customer }}-nextcloud-database:/var/lib/postgresql/data:Z + - {{ customer }}-nextcloud-backups:/tmp/backups + environment: + POSTGRES_DB: nextcloud + POSTGRES_USER: {{ nextcloud_postgres_user }} + POSTGRES_PASSWORD: {{ nextcloud_postgres_password }} + networks: + {{ customer }}-nextcloud: + ipv4_address: 172.20.9.2 + + redis: + container_name: {{ customer }}-nextcloud-redis + image: redis:alpine + restart: always + networks: + {{ customer }}-nextcloud: + ipv4_address: 172.20.9.3 + + app: + container_name: {{ customer }}-nextcloud-app + image: nextcloud:production-apache + restart: always + labels: + - "diun.enable=true" + ports: + - '127.0.0.1:3023:80' + volumes: + - {{ customer }}-nextcloud-html:/var/www/html:z + - /opt/letsbe/config/nextcloud:/var/www/html/config + - /opt/letsbe/data/nextcloud:/var/www/html/data + - {{ customer }}-nextcloud-backups:/tmp/backups + environment: + #Nextcloud + POSTGRES_HOST: {{ customer }}-nextcloud-postgres + REDIS_HOST: {{ customer }}-nextcloud-redis + POSTGRES_DB: nextcloud + POSTGRES_USER: {{ nextcloud_postgres_user }} + POSTGRES_PASSWORD: {{ nextcloud_postgres_password }} + # #SMTP + # SMTP_HOST: 'mail.{{ domain }}' + # SMTP_PORT: '587' + # SMTP_NAME: 'system@{{ domain }}' + # SMTP_PASSWORD: '' + # MAIL_FROM_ADDRESS: 'system' + # MAIL_DOMAIN: '{{ domain }}' + #Admin + NEXTCLOUD_ADMIN_USER: administrator@letsbe.biz + NEXTCLOUD_ADMIN_PASSWORD: '{{ nextcloud_admin_password }}' + #Config + NEXTCLOUD_TRUSTED_DOMAINS: '{{ domain_nextcloud }} 127.0.0.1 0.0.0.0' + TRUSTED_PROXIES: '{{ domain_nextcloud }} 127.0.0.1 0.0.0.0 172.*.*.*' + OVERWRITECLIURL: https://{{ domain_nextcloud }} + OVERWRITEPROTOCOL: https + OVERWRITEHOST: {{ domain_nextcloud }} + #APACHE_DISABLE_REWRITE_IP: 1 + depends_on: + - db + - redis + networks: + {{ customer }}-nextcloud: + ipv4_address: 172.20.9.4 + + cron: + container_name: {{ customer }}-nextcloud-cron + image: nextcloud:production-apache + restart: always + volumes: + - {{ customer }}-nextcloud-html:/var/www/html:z + - /opt/letsbe/config/nextcloud:/var/www/html/config + - /opt/letsbe/data/nextcloud:/var/www/html/data + entrypoint: /cron.sh + depends_on: + - db + - redis + networks: + {{ customer }}-nextcloud: + ipv4_address: 172.20.9.5 + + collabora: + image: collabora/code:latest + container_name: {{ customer }}-nextcloud-collabora + restart: always + environment: + - password={{ collabora_password }} + - username={{ collabora_user }} + - domain={{ domain_collabora }} + - extra_params=--o:ssl.enable=true + ports: + - '127.0.0.1:3044:9980' + networks: + {{ customer }}-nextcloud: + ipv4_address: 172.20.9.7 + + nextcloud-whiteboard-server: + image: ghcr.io/nextcloud-releases/whiteboard:release + ports: + - '127.0.0.1:3060:3002' + environment: + NEXTCLOUD_URL: '{{ domain_nextcloud }}' + JWT_SECRET_KEY: '{{ nextcloud_jwt_secret }}' + networks: + {{ customer }}-nextcloud: + ipv4_address: 172.20.9.8 + +networks: + {{ customer }}-nextcloud: + ipam: + driver: default + config: + - subnet: 172.20.9.0/28 + gateway: 172.20.9.1 + +volumes: + {{ customer }}-nextcloud-html: + # driver: local + # driver_opts: + # size: 100g + {{ customer }}-nextcloud-database: + # driver: local + # driver_opts: + # size: 100g + {{ customer }}-nextcloud-backups: diff --git a/script/stacks/nocodb/docker-compose.yml b/script/stacks/nocodb/docker-compose.yml new file mode 100644 index 0000000..914928b --- /dev/null +++ b/script/stacks/nocodb/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.9' + +services: + nocodb: + container_name: {{ customer }}-nocodb + image: nocodb/nocodb:latest + restart: always + labels: + - "diun.enable=true" + environment: + - NC_DB=pg://{{ customer }}-nocodb-db:5432?u=postgres&p={{ nocodb_postgres_password }}&d=nocodb + volumes: + - {{ customer }}-nocodb-data:/usr/app/data + - {{ customer }}-nocodb-backups:/tmp/backups + ports: + - "127.0.0.1:3057:8080" # Host port 3057 -> Container port 8080 + depends_on: + nocodb-db: + condition: service_healthy + networks: + {{ customer }}-nocodb: + ipv4_address: 172.20.24.2 + + nocodb-db: + container_name: {{ customer }}-nocodb-db + image: postgres:16.6 + restart: always + environment: + POSTGRES_DB: nocodb + POSTGRES_USER: postgres + POSTGRES_PASSWORD: {{ nocodb_postgres_password }} + volumes: + - {{ customer }}-nocodb-postgres:/var/lib/postgresql/data + - {{ customer }}-nocodb-backups:/tmp/backups + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + {{ customer }}-nocodb: + ipv4_address: 172.20.24.3 + +networks: + {{ customer }}-nocodb: + ipam: + driver: default + config: + - subnet: 172.20.24.0/28 + gateway: 172.20.24.1 + +volumes: + {{ customer }}-nocodb-data: + {{ customer }}-nocodb-postgres: + {{ customer }}-nocodb-backups: diff --git a/script/stacks/odoo/docker-compose.yml b/script/stacks/odoo/docker-compose.yml new file mode 100644 index 0000000..af98b2e --- /dev/null +++ b/script/stacks/odoo/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.8' + +services: + odoo-web: + container_name: {{ customer }}-odoo-web + restart: always + image: odoo:latest + labels: + - "diun.enable=true" + depends_on: + - odoo-postgres + ports: + - "127.0.0.1:3019:8069" + environment: + - HOST=odoo-postgres + - USER={{ odoo_postgres_user }} + - PASSWORD={{ odoo_postgres_password }} + volumes: + - {{ customer }}-odoo-web-data:/var/lib/odoo + - {{ customer }}-odoo-web-config:/etc/odoo + - {{ customer }}-odoo-web-addons:/mnt/extra-addons + - {{ customer }}-odoo-backups:/tmp/backups + networks: + {{ customer }}-odoo: + ipv4_address: 172.20.19.2 + + odoo-postgres: + container_name: {{ customer }}-odoo-postgres + image: postgres:15 + restart: always + environment: + POSTGRES_DB: postgres + POSTGRES_USER: {{ odoo_postgres_user }} + POSTGRES_PASSWORD: {{ odoo_postgres_password }} + volumes: + - {{ customer }}-odoo-postgres:/var/lib/postgresql/data/ + - {{ customer }}-odoo-backups:/tmp/backups + networks: + {{ customer }}-odoo: + ipv4_address: 172.20.19.3 + +networks: + {{ customer }}-odoo: + ipam: + driver: default + config: + - subnet: 172.20.19.0/28 + gateway: 172.20.19.1 + +volumes: + {{ customer }}-odoo-postgres: + {{ customer }}-odoo-web-data: + {{ customer }}-odoo-web-config: + {{ customer }}-odoo-web-addons: + {{ customer }}-odoo-backups: diff --git a/script/stacks/penpot/docker-compose.yml b/script/stacks/penpot/docker-compose.yml new file mode 100644 index 0000000..77dbb54 --- /dev/null +++ b/script/stacks/penpot/docker-compose.yml @@ -0,0 +1,165 @@ +--- +version: "3.5" + +services: + penpot-frontend: + container_name: {{ customer }}-penpot-frontend + image: "penpotapp/frontend:latest" + restart: always + labels: + - "diun.enable=true" + ports: + - '127.0.0.1:3021:80' + volumes: + - {{ customer }}-penpot-assets:/opt/data/assets + depends_on: + - penpot-backend + - penpot-exporter + # labels: + # - "traefik.enable=true" + environment: + - PENPOT_FLAGS=enable-registration enable-login-with-password + networks: + {{ customer }}-penpot: + ipv4_address: 172.20.10.2 + + penpot-backend: + container_name: {{ customer }}-penpot-backend + image: "penpotapp/backend:latest" + restart: always + volumes: + - {{ customer }}-penpot-assets:/opt/data/assets + - {{ customer }}-penpot-backups:/tmp/backups + depends_on: + - penpot-postgres + - penpot-redis + environment: + - PENPOT_FLAGS=enable-registration enable-login-with-password disable-email-verification enable-smtp enable-prepl-server + - PENPOT_SECRET_KEY={{ penpot_secret_key }} + - PENPOT_TELEMETRY_ENABLED=false + # - PENPOT_PREPL_HOST=0.0.0.0 + - PENPOT_PUBLIC_URI=https://{{ domain_penpot }} #http://localhost:9001 + ## Database + - PENPOT_DATABASE_URI=postgresql://{{ customer }}-penpot-postgres/penpot + - PENPOT_DATABASE_USERNAME={{ penpot_db_user }} + - PENPOT_DATABASE_PASSWORD={{ penpot_db_password }} + - PENPOT_REDIS_URI=redis://{{ customer }}-penpot-redis/0 + - PENPOT_ASSETS_STORAGE_BACKEND=assets-fs + - PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets + ## S3 + # - AWS_ACCESS_KEY_ID= + # - AWS_SECRET_ACCESS_KEY= + # - PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 + # - PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://penpot-minio:9000 + # - PENPOT_STORAGE_ASSETS_S3_BUCKET= + ## SMTP + - PENPOT_SMTP_DEFAULT_FROM=no-reply@{{ domain }} + - PENPOT_SMTP_DEFAULT_REPLY_TO=support@{{ domain }} + - PENPOT_SMTP_HOST=mail.{{ domain }} + - PENPOT_SMTP_PORT=587 + - PENPOT_SMTP_USERNAME= + - PENPOT_SMTP_PASSWORD= + - PENPOT_SMTP_TLS=true + - PENPOT_SMTP_SSL=false + networks: + {{ customer }}-penpot: + ipv4_address: 172.20.10.3 + + penpot-exporter: + container_name: {{ customer }}-penpot-exporter + image: "penpotapp/exporter:latest" + restart: always + environment: + - PENPOT_PUBLIC_URI=http://{{ customer }}-penpot-frontend + - PENPOT_REDIS_URI=redis://{{ customer }}-penpot-redis/0 + networks: + {{ customer }}-penpot: + ipv4_address: 172.20.10.4 + + penpot-postgres: + container_name: {{ customer }}-penpot-postgres + image: "postgres:15" + restart: always + stop_signal: SIGINT + volumes: + - {{ customer }}-penpot-postgres:/var/lib/postgresql/data + - {{ customer }}-penpot-backups:/tmp/backups + environment: + - POSTGRES_INITDB_ARGS=--data-checksums + - POSTGRES_DB=penpot + - POSTGRES_USER={{ penpot_db_user }} + - POSTGRES_PASSWORD={{ penpot_db_password }} + networks: + {{ customer }}-penpot: + ipv4_address: 172.20.10.5 + + penpot-redis: + container_name: {{ customer }}-penpot-redis + image: redis:7 + restart: always + networks: + {{ customer }}-penpot: + ipv4_address: 172.20.10.6 + + # penpot-mailcatch: + # container_name: {{ customer }}-penpot-mailcatch + # image: sj26/mailcatcher:latest + # restart: always + # # expose: + # # - '1025' + # ports: + # - "127.0.0.1:3048:1080" + # networks: + # {{ customer }}-penpot: + # ipv4_address: 172.20.10.7 + +networks: + {{ customer }}-penpot: + ipam: + driver: default + config: + - subnet: 172.20.10.0/28 + gateway: 172.20.10.1 + +# networks: +# penpot: + +volumes: + {{ customer }}-penpot-assets: + {{ customer }}-penpot-postgres: + {{ customer }}-penpot-backups: + +## Relevant flags for frontend: +## - demo-users +## - login-with-github +## - login-with-gitlab +## - login-with-google +## - login-with-ldap +## - login-with-oidc +## - login-with-password +## - registration +## - webhooks +## +## You can read more about all available flags on: +## https://help.penpot.app/technical-guide/configuration/#advanced-configuration + +##Environment variables +## Relevant flags for backend: +## - demo-users +## - email-verification +## - log-emails +## - log-invitation-tokens +## - login-with-github +## - login-with-gitlab +## - login-with-google +## - login-with-ldap +## - login-with-oidc +## - login-with-password +## - registration +## - secure-session-cookies +## - smtp +## - smtp-debug +## - telemetry +## - webhooks +## - prepl-server +## https://help.penpot.app/technical-guide/configuration/#advanced-configuration diff --git a/script/stacks/portainer/docker-compose.yml b/script/stacks/portainer/docker-compose.yml new file mode 100644 index 0000000..9db151d --- /dev/null +++ b/script/stacks/portainer/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.9' + +services: + portainer: + container_name: {{ customer }}-portainer + image: portainer/portainer-ce:latest + restart: always + labels: + - "diun.enable=true" + ports: + - '9443:9443' + # - '127.0.0.1:8000:8000' + # - '127.0.0.1:9443:9443' + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - {{ customer }}-portainer_data:/data + - {{ customer }}-portainer-backups:/tmp/backups + networks: + {{ customer }}-portainer: + ipv4_address: 172.20.20.2 + +networks: + {{ customer }}-portainer: + ipam: + driver: default + config: + - subnet: 172.20.20.0/28 + gateway: 172.20.20.1 + +volumes: + {{ customer }}-portainer_data: + {{ customer }}-portainer-backups: diff --git a/script/stacks/poste/docker-compose.yml b/script/stacks/poste/docker-compose.yml new file mode 100644 index 0000000..785f4b1 --- /dev/null +++ b/script/stacks/poste/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3.9' + +services: + poste: + container_name: {{ customer }}-poste + image: analogic/poste.io:latest + restart: always + labels: + - "diun.enable=true" + hostname: {{ domain_poste }} + # network_mode: host + volumes: + - {{ customer }}-poste-data:/data + - {{ customer }}-poste-backups:/tmp/backups + ports: + - "25:25" + - "127.0.0.1:3003:80" + - "127.0.0.1:3004:443" + - "110:110" + - "143:143" + - "465:465" + - "587:587" + - "993:993" + - "995:995" + - "4190:4190" + environment: + TZ: Europe/Berlin + HTTPS: ON + DISABLE_CLAMAV: TRUE + DISABLE_RSPAMD: TRUE + DISABLE_ROUNDCUBE: TRUE + VIRTUAL_HOST: {{ domain_poste }} + networks: + {{ customer }}-poste: + ipv4_address: 172.20.11.2 + +networks: + {{ customer }}-poste: + ipam: + driver: default + config: + - subnet: 172.20.11.0/28 + gateway: 172.20.11.1 + +volumes: + {{ customer }}-poste-data: + {{ customer }}-poste-backups: diff --git a/script/stacks/redash/.env b/script/stacks/redash/.env new file mode 100644 index 0000000..017e97d --- /dev/null +++ b/script/stacks/redash/.env @@ -0,0 +1,16 @@ +REDASH_HOST=https://{{ redash_domain }} +REDASH_MAIL_SERVER=mail.{{ domain }} +REDASH_MAIL_PORT=465 +REDASH_MAIL_USE_TLS=false +REDASH_MAIL_USE_SSL=true +REDASH_MAIL_USERNAME=noreply@{{ domain }} +REDASH_MAIL_PASSWORD= +REDASH_MAIL_DEFAULT_SENDER="Redash " +REDASH_SECRET_KEY={{ redash_secret_key }} +REDASH_DATABASE_URL=postgresql://{{ redash_postgres_user }}:{{ redash_postgres_password }}@redash-postgres:5432/redash +POSTGRES_USER={{ redash_postgres_user }} +POSTGRES_PASSWORD={{ redash_postgres_password }} +POSTGRES_DB=redash +REDASH_COOKIE_SECRET={{ redash_cookie_secret }} +REDASH_ENFORCE_HTTPS=true +REDASH_REDIS_URL=redis://redash-redis:6379/0 \ No newline at end of file diff --git a/script/stacks/redash/docker-compose.yml b/script/stacks/redash/docker-compose.yml new file mode 100644 index 0000000..bd8dc6f --- /dev/null +++ b/script/stacks/redash/docker-compose.yml @@ -0,0 +1,101 @@ +version: "3.8" + +networks: + redash_network: + driver: bridge + ipam: + config: + - subnet: 172.20.28.0/28 + gateway: 172.20.28.1 + +x-redash-service: &redash-service + image: redash/redash:25.1.0 + depends_on: + - postgres + - redis + env_file: /opt/letsbe/env/redash.env + restart: always + networks: + redash_network: + ipv4_address: 172.20.28.2 + +services: + server: + <<: *redash-service + command: server + labels: + - "diun.enable=true" + ports: + - "3064:5000" + environment: + REDASH_WEB_WORKERS: 4 + container_name: {{ customer }}-redash-server + networks: + redash_network: + ipv4_address: 172.20.28.3 + + scheduler: + <<: *redash-service + command: scheduler + depends_on: + - server + container_name: {{ customer }}-redash-scheduler + networks: + redash_network: + ipv4_address: 172.20.28.4 + + scheduled_worker: + <<: *redash-service + command: worker + depends_on: + - server + environment: + QUEUES: "scheduled_queries,schemas" + WORKERS_COUNT: 1 + container_name: {{ customer }}-redash-scheduled-worker + networks: + redash_network: + ipv4_address: 172.20.28.5 + + adhoc_worker: + <<: *redash-service + command: worker + depends_on: + - server + environment: + QUEUES: "queries" + WORKERS_COUNT: 2 + container_name: {{ customer }}-redash-adhoc-worker + networks: + redash_network: + ipv4_address: 172.20.28.6 + + redis: + image: redis:7-alpine + restart: unless-stopped + container_name: redash-redis + networks: + redash_network: + ipv4_address: 172.20.28.7 + + postgres: + image: postgres:13-alpine + env_file: /opt/letsbe/env/redash.env + volumes: + - ./postgres-data:/var/lib/postgresql/data + restart: unless-stopped + container_name: redash-postgres + networks: + redash_network: + ipv4_address: 172.20.28.8 + + worker: + <<: *redash-service + command: worker + environment: + QUEUES: "periodic,emails,default" + WORKERS_COUNT: 1 + container_name: {{ customer }}-redash-worker + networks: + redash_network: + ipv4_address: 172.20.28.9 diff --git a/script/stacks/squidex/docker-compose.yml b/script/stacks/squidex/docker-compose.yml new file mode 100644 index 0000000..500fed9 --- /dev/null +++ b/script/stacks/squidex/docker-compose.yml @@ -0,0 +1,61 @@ +version: "3.5" + +services: + squidex_mongo: + container_name: {{ customer }}-squidex-mongo + image: "mongo:6" + restart: always + volumes: + - {{ customer }}-squidex-mongo:/data/db + - {{ customer }}-squidex-backups:/tmp/backups + networks: + {{ customer }}-squidex: + ipv4_address: 172.20.12.2 + + squidex_squidex: + container_name: {{ customer }}-squidex-squidex + image: "squidex/squidex:7" + restart: always + labels: + - "diun.enable=true" + ports: + - "127.0.0.1:3002:80" + environment: + URLS__BASEURL: 'https://{{ domain_squidex }}' + UI__ONLYADMINSCANCREATEAPPS: false + UI__ONLYADMINSCANCREATETEAMS: false + EVENTSTORE__TYPE: MongoDB + EVENTSTORE__MONGODB__CONFIGURATION: mongodb://squidex_mongo + STORE__MONGODB__CONFIGURATION: mongodb://squidex_mongo + IDENTITY__ADMINEMAIL: {{ squidex_adminemail }} + IDENTITY__ADMINPASSWORD: {{ squidex_adminpassword }} + # - IDENTITY__GOOGLECLIENT=${SQUIDEX_GOOGLECLIENT} + # - IDENTITY__GOOGLESECRET=${SQUIDEX_GOOGLESECRET} + # - IDENTITY__GITHUBCLIENT=${SQUIDEX_GITHUBCLIENT} + # - IDENTITY__GITHUBSECRET=${SQUIDEX_GITHUBSECRET} + # - IDENTITY__MICROSOFTCLIENT=${SQUIDEX_MICROSOFTCLIENT} + # - IDENTITY__MICROSOFTSECRET=${SQUIDEX_MICROSOFTSECRET} + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80/healthz"] + start_period: 60s + depends_on: + - squidex_mongo + volumes: + - {{ customer }}-squidex-assets:/app/Assets + - {{ customer }}-squidex-backups:/tmp/backups + networks: + {{ customer }}-squidex: + ipv4_address: 172.20.12.3 + +networks: + {{ customer }}-squidex: + ipam: + driver: default + config: + - subnet: 172.20.12.0/28 + gateway: 172.20.12.1 + +volumes: + {{ customer }}-squidex-mongo: + {{ customer }}-squidex-assets: + {{ customer }}-squidex-backups: diff --git a/script/stacks/stirlingpdf/docker-compose.yml b/script/stacks/stirlingpdf/docker-compose.yml new file mode 100644 index 0000000..ffe232d --- /dev/null +++ b/script/stacks/stirlingpdf/docker-compose.yml @@ -0,0 +1,54 @@ +version: '3.8' + +services: + postgres: + container_name: {{ customer }}-stirlingpdf-db + image: postgres:14 + restart: always + volumes: + - {{ customer }}-stirlingpdf-postgres:/var/lib/postgresql/data + - {{ customer }}-stirlingpdf-backups:/tmp/backups + environment: + POSTGRES_DB: stirlingpdf + POSTGRES_USER: {{ stirlingpdf_postgres_user }} + POSTGRES_PASSWORD: {{ stirlingpdf_postgres_password }} + networks: + {{ customer }}-stirlingpdf: + ipv4_address: 172.20.32.2 + + stirlingpdf: + container_name: {{ customer }}-stirlingpdf + image: frooodle/s-pdf:latest + restart: always + ports: + - "127.0.0.1:8080:8080" + volumes: + - {{ customer }}-stirlingpdf-data:/usr/share/s-pdf/data + - {{ customer }}-stirlingpdf-configs:/configs + environment: + - DOCKER_ENABLE_SECURITY=true + - ENABLE_ARIA2=true + - ALLOW_CORS=false + - DISABLE_PLAYGROUND=true + - STIRLING_PDF_DATABASE_URL=postgresql://{{ stirlingpdf_postgres_user }}:{{ stirlingpdf_postgres_password }}@{{ customer }}-stirlingpdf-db:5432/stirlingpdf + - STIRLING_PDF_API_KEY={{ stirlingpdf_api_key }} + depends_on: + - postgres + networks: + {{ customer }}-stirlingpdf: + ipv4_address: 172.20.32.3 + labels: + - "diun.enable=true" + +networks: + {{ customer }}-stirlingpdf: + driver: bridge + ipam: + config: + - subnet: 172.20.32.0/28 + +volumes: + {{ customer }}-stirlingpdf-postgres: + {{ customer }}-stirlingpdf-data: + {{ customer }}-stirlingpdf-configs: + {{ customer }}-stirlingpdf-backups: diff --git a/script/stacks/sysadmin/docker-compose.yml b/script/stacks/sysadmin/docker-compose.yml new file mode 100644 index 0000000..1371d07 --- /dev/null +++ b/script/stacks/sysadmin/docker-compose.yml @@ -0,0 +1,68 @@ +version: "3.8" + +services: + agent: + build: + context: . + dockerfile: Dockerfile + container_name: {{ customer }}-agent + + environment: + # Required: Orchestrator connection + - ORCHESTRATOR_URL=https://orchestrator.letsbe.biz + - AGENT_TOKEN={{ sysadmin_agent_token }} + + # Timing (seconds) + - HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30} + - POLL_INTERVAL=${POLL_INTERVAL:-5} + + # Logging + - LOG_LEVEL=${LOG_LEVEL:-DEBUG} + - LOG_JSON=${LOG_JSON:-false} + + # Resilience + - MAX_CONCURRENT_TASKS=${MAX_CONCURRENT_TASKS:-3} + - BACKOFF_BASE=${BACKOFF_BASE:-1.0} + - BACKOFF_MAX=${BACKOFF_MAX:-60.0} + - CIRCUIT_BREAKER_THRESHOLD=${CIRCUIT_BREAKER_THRESHOLD:-5} + - CIRCUIT_BREAKER_COOLDOWN=${CIRCUIT_BREAKER_COOLDOWN:-300} + + # Security + - ALLOWED_FILE_ROOT=${ALLOWED_FILE_ROOT:-/opt/letsbe} + - MAX_FILE_SIZE=${MAX_FILE_SIZE:-10485760} + - SHELL_TIMEOUT=${SHELL_TIMEOUT:-60} + + volumes: + # Docker socket for docker executor + - /var/run/docker.sock:/var/run/docker.sock + + # Hot reload in development + - ./app:/app/app:ro + + # Host directory mounts for real infrastructure access + - /opt/letsbe/env:/opt/letsbe/env + - /opt/letsbe/stacks:/opt/letsbe/stacks + - /opt/letsbe/nginx:/opt/letsbe/nginx + + # Pending results persistence + - agent_home:/home/agent/.letsbe-agent + + # Run as root for Docker socket access in dev + # In production, use Docker group membership instead + user: root + + restart: unless-stopped + + # Resource limits + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.1' + memory: 64M + +volumes: + agent_home: + name: {{ customer }}-agent-home diff --git a/script/stacks/typebot/.env b/script/stacks/typebot/.env new file mode 100644 index 0000000..c2e5caf --- /dev/null +++ b/script/stacks/typebot/.env @@ -0,0 +1,37 @@ +## Make sure to change this to your own random string of 32 characters (https://docs.typebot.io/self-hosting/deploy/docker#2-add-the-required-configuratio> +ENCRYPTION_SECRET={{ typebot_encryption_secret }} + +DATABASE_URL=postgresql://postgres:{{ typebot_postgres_password }}@{{ customer }}-typebot-db:5432/typebot + +NODE_OPTIONS=--no-node-snapshot + +NEXTAUTH_URL=https://{{ domain_botlab }} +NEXT_PUBLIC_VIEWER_URL=https://{{ domain_bot_viewer }} + +DEFAULT_WORKSPACE_PLAN=UNLIMITED +DISABLE_SIGNUP=false + +ADMIN_EMAIL=administrator@{{ domain }} +## For more configuration options check out: https://docs.typebot.io/self-hosting/configuration + + +## SMTP Configuration (Make noreply email account too) +SMTP_USERNAME=noreply@{{ domain }} +SMTP_PASSWORD= +SMTP_HOST=mail.{{ domain }} +SMTP_PORT=465 +SMTP_SECURE=true +NEXT_PUBLIC_SMTP_FROM="{{ company_name }} " +SMTP_AUTH_DISABLED=false + + + +## S3 Configuration for MinIO +S3_ACCESS_KEY=#replace +S3_SECRET_KEY=#replace +S3_BUCKET=bots +S3_PORT= +S3_ENDPOINT={{ domain_s3 }} +S3_SSL=true +S3_REGION=eu-central +S3_PUBLIC_CUSTOM_DOMAIN=https://{{ domain_s3 }}/bots \ No newline at end of file diff --git a/script/stacks/typebot/docker-compose.yml b/script/stacks/typebot/docker-compose.yml new file mode 100644 index 0000000..3b1be43 --- /dev/null +++ b/script/stacks/typebot/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3.3' + +volumes: + {{ customer }}-typebot-db-data: + +services: + {{ customer }}-typebot-db: + image: postgres:16 + restart: always + volumes: + - {{ customer }}-typebot-db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=typebot + - POSTGRES_PASSWORD={{ typebot_postgres_password }} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + networks: + {{ customer }}-typebot: + ipv4_address: 172.20.25.2 + + typebot-builder: + image: baptistearno/typebot-builder:latest + restart: always + labels: + - "diun.enable=true" + depends_on: + {{ customer }}-typebot-db: + condition: service_healthy + ports: + - '3061:3000' + extra_hosts: + - 'host.docker.internal:host-gateway' + env_file: /opt/letsbe/env/typebot.env + networks: + {{ customer }}-typebot: + ipv4_address: 172.20.25.3 + + typebot-viewer: + image: baptistearno/typebot-viewer:latest + depends_on: + {{ customer }}-typebot-db: + condition: service_healthy + restart: always + ports: + - '3062:3000' + env_file: /opt/letsbe/env/typebot.env + networks: + {{ customer }}-typebot: + ipv4_address: 172.20.25.4 + +networks: + {{ customer }}-typebot: + ipam: + driver: default + config: + - subnet: 172.20.25.0/28 + gateway: 172.20.25.1 diff --git a/script/stacks/umami/docker-compose.yml b/script/stacks/umami/docker-compose.yml new file mode 100644 index 0000000..276bd33 --- /dev/null +++ b/script/stacks/umami/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.9' + +services: + umami: + container_name: {{ customer }}-umami + image: ghcr.io/umami-software/umami:postgresql-latest + restart: always + labels: + - "diun.enable=true" + ports: + - "127.0.0.1:3008:3000" + environment: + DATABASE_URL: postgresql://{{ umami_postgres_user }}:{{ umami_postgres_password }}@{{ customer }}-umami-db:5432/umami + DATABASE_TYPE: postgresql + APP_SECRET: '{{ umami_app_secret }}' + networks: + {{ customer }}-umami: + ipv4_address: 172.20.13.2 + depends_on: + umami-db: + condition: service_healthy + + umami-db: + container_name: {{ customer }}-umami-db + image: postgres:15-alpine + environment: + POSTGRES_DB: 'umami' + POSTGRES_USER: '{{ umami_postgres_user }}' + POSTGRES_PASSWORD: '{{ umami_postgres_password }}' + volumes: + - {{ customer }}-umami-postgres:/var/lib/postgresql/data + - {{ customer }}-umami-backups:/tmp/backups + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + {{ customer }}-umami: + ipv4_address: 172.20.13.3 + +networks: + {{ customer }}-umami: + ipam: + driver: default + config: + - subnet: 172.20.13.0/28 + gateway: 172.20.13.1 + +volumes: + {{ customer }}-umami-postgres: + {{ customer }}-umami-backups: diff --git a/script/stacks/uptime-kuma/docker-compose.yml b/script/stacks/uptime-kuma/docker-compose.yml new file mode 100644 index 0000000..2472736 --- /dev/null +++ b/script/stacks/uptime-kuma/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.9' + +services: + uptime-kuma: + container_name: {{ customer }}-uptime-kuma + image: louislam/uptime-kuma:latest + restart: always + labels: + - "diun.enable=true" + volumes: + - {{ customer }}-uptimekuma-data:/app/data + - {{ customer }}-uptimekuma-backups:/tmp/backups + ports: + - "127.0.0.1:3005:3001" + networks: + {{ customer }}-uptime-kuma: + ipv4_address: 172.20.14.2 + +networks: + {{ customer }}-uptime-kuma: + ipam: + driver: default + config: + - subnet: 172.20.14.0/28 + gateway: 172.20.14.1 + +volumes: + {{ customer }}-uptimekuma-data: + {{ customer }}-uptimekuma-backups: diff --git a/script/stacks/windmill/Caddyfile b/script/stacks/windmill/Caddyfile new file mode 100644 index 0000000..4220df1 --- /dev/null +++ b/script/stacks/windmill/Caddyfile @@ -0,0 +1,6 @@ +{$BASE_URL} { + bind {$ADDRESS} + reverse_proxy /ws/* http://lsp:3001 + # reverse_proxy /ws_mp/* http://multiplayer:3002 + reverse_proxy /* http://windmill_server:8000 +} \ No newline at end of file diff --git a/script/stacks/windmill/docker-compose.yml b/script/stacks/windmill/docker-compose.yml new file mode 100644 index 0000000..437162f --- /dev/null +++ b/script/stacks/windmill/docker-compose.yml @@ -0,0 +1,166 @@ +version: "3.7" + +services: + windmill-db: + container_name: {{ customer }}-windmill-db + # deploy: + # replicas: 1 + image: postgres:15 + restart: always + volumes: + - {{ customer }}-windmill-postgres:/var/lib/postgresql/data + - {{ customer }}-windmill-backups:/tmp/backups + ports: + - "127.0.0.1:3038:5432" + environment: + POSTGRES_PASSWORD: '{{ windmill_database_password }}' + POSTGRES_DB: windmill + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + {{ customer }}-windmill: + ipv4_address: 172.20.15.2 + + windmill_server: + container_name: {{ customer }}-windmill-server + image: ghcr.io/windmill-labs/windmill:main + pull_policy: always + # deploy: + # replicas: 1 + restart: always + labels: + - "diun.enable=true" + ports: + - "127.0.0.1:3039:8000" + environment: + - DATABASE_URL=postgres://postgres:{{ windmill_database_password }}@windmill-db:5432/windmill?sslmode=disable + - BASE_URL='https://{{ domain_windmill }}' + - RUST_LOG=info + - NUM_WORKERS=0 + - DISABLE_SERVER=false + - METRICS_ADDR=false + - REQUEST_SIZE_LIMIT=50097152 + #- LICENSE_KEY=${WM_LICENSE_KEY} + depends_on: + windmill-db: + condition: service_healthy + volumes: + - ./oauth.json:/usr/src/app/oauth.json + - {{ customer }}-windmill-backups:/tmp/backups + networks: + {{ customer }}-windmill: + ipv4_address: 172.20.15.3 + + windmill_worker: + container_name: {{ customer }}-windmill-worker + image: ghcr.io/windmill-labs/windmill:main + pull_policy: always + # deploy: + # replicas: 1 + restart: always + environment: + - DATABASE_URL=postgres://postgres:{{ windmill_database_password }}@windmill-db:5432/windmill?sslmode=disable + - BASE_URL='https://{{ domain_windmill }}' + - RUST_LOG=info + - DISABLE_SERVER=true + - KEEP_JOB_DIR=false + - METRICS_ADDR=false + - WORKER_TAGS=deno,python3,go,bash,powershell,dependency,flow,hub,other,bun + #- LICENSE_KEY=${WM_LICENSE_KEY} + depends_on: + windmill-db: + condition: service_healthy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./oauth.json:/usr/src/app/oauth.json + - {{ customer }}-windmill-worker-cache:/tmp/windmill/cache + networks: + {{ customer }}-windmill: + ipv4_address: 172.20.15.4 + + windmill_worker_native: + container_name: {{ customer }}-windmill-worker-native + image: ghcr.io/windmill-labs/windmill:main + pull_policy: always + # deploy: + # replicas: 1 + # resources: + # limits: + # cpus: "0.25" + # memory: 512M + restart: always + environment: + - DATABASE_URL=postgres://postgres:{{ windmill_database_password }}@windmill-db:5432/windmill?sslmode=disable + - BASE_URL='https://{{ domain_windmill }}' + - RUST_LOG=info + - DISABLE_SERVER=true + - KEEP_JOB_DIR=false + - METRICS_ADDR=false + - NUM_WORKERS=4 + - WORKER_TAGS=nativets,postgresql,mysql,graphql,snowflake + depends_on: + windmill-db: + condition: service_healthy + volumes: + # See Oauth (https://docs.windmill.dev/docs/misc/setup_oauth) + - ./oauth.json:/usr/src/app/oauth.json + networks: + {{ customer }}-windmill: + ipv4_address: 172.20.15.5 + + lsp: + container_name: {{ customer }}-windmill-lsp + image: ghcr.io/windmill-labs/windmill-lsp:latest + restart: always + ports: + - "127.0.0.1:3041:3001" + volumes: + - {{ customer }}-windmill-lsp-cache:/root/.cache + networks: + {{ customer }}-windmill: + ipv4_address: 172.20.15.6 + + multiplayer: + container_name: {{ customer }}-windmill-multiplayer + image: ghcr.io/windmill-labs/windmill-multiplayer:latest + # deploy: + # replicas: 0 # Set to 1 to enable multiplayer, only available on Enterprise Edition + restart: always + ports: + - "127.0.0.1:3047:3002" + networks: + {{ customer }}-windmill: + ipv4_address: 172.20.15.7 + + caddy: + container_name: {{ customer }}-windmill-caddy + image: caddy:2.5.2-alpine + restart: always + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - {{ customer }}-windmill-backups:/tmp/backups + ports: + - "127.0.0.1:3014:80" + environment: + BASE_URL: ":80" + networks: + {{ customer }}-windmill: + ipv4_address: 172.20.15.8 + +networks: + {{ customer }}-windmill: + ipam: + driver: default + config: + - subnet: 172.20.15.0/28 + gateway: 172.20.15.1 + +volumes: + {{ customer }}-windmill-worker-cache: null + {{ customer }}-windmill-lsp-cache: null + {{ customer }}-windmill-postgres: + #{{ customer }}-windmill-data: + {{ customer }}-windmill-backups: diff --git a/script/stacks/windmill/oauth.json b/script/stacks/windmill/oauth.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/script/stacks/windmill/oauth.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/script/stacks/wordpress/docker-compose.yml b/script/stacks/wordpress/docker-compose.yml new file mode 100644 index 0000000..ab63e71 --- /dev/null +++ b/script/stacks/wordpress/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.9' + +services: + wordpress-mysql: + container_name: {{ customer }}-wordpress-mysql + image: mariadb:10.7.8 + restart: always + ports: + - "127.0.0.1:3053:3306" + environment: + MYSQL_ROOT_PASSWORD: {{ wordpresss_mariadb_root_password }} + MYSQL_DATABASE: wordpress + MYSQL_USER: {{ wordpress_db_user }} + MYSQL_PASSWORD: {{ wordpress_db_password }} + volumes: + - {{ customer }}-wordpress-mariadb:/var/lib/mysql + - {{ customer }}-wordpress-backups:/tmp/backups + networks: + {{ customer }}-wordpress: + ipv4_address: 172.20.16.2 + + wordpress: + container_name: {{ customer }}-wordpress + image: wordpress:php8.2-apache + restart: always + labels: + - "diun.enable=true" + volumes: + - {{ customer }}-wordpress-data:/var/www/html + - {{ customer }}-wordpress-backups:/tmp/backups + ports: + - "127.0.0.1:3001:80" + environment: + WORDPRESS_DB_HOST: {{ customer }}-wordpress-mysql + WORDPRESS_DB_USER: {{ wordpress_db_user }} + WORDPRESS_DB_PASSWORD: {{ wordpress_db_password }} + WORDPRESS_DB_NAME: wordpress + depends_on: + - wordpress-mysql + networks: + {{ customer }}-wordpress: + ipv4_address: 172.20.16.3 + +networks: + {{ customer }}-wordpress: + ipam: + driver: default + config: + - subnet: 172.20.16.0/28 + gateway: 172.20.16.1 + +volumes: + {{ customer }}-wordpress-mariadb: + {{ customer }}-wordpress-data: + {{ customer }}-wordpress-backups: diff --git a/script/start.sh b/script/start.sh new file mode 100644 index 0000000..5556a7a --- /dev/null +++ b/script/start.sh @@ -0,0 +1,389 @@ +#!/bin/bash +# +# LetsBe Deployment Script +# Deploys the LetsBe setup to a remote server via SSH. +# +# Usage: +# ./start.sh --host 192.168.1.100 --password "mypassword" --action setup +# ./start.sh --host 192.168.1.100 --key ./id_ed25519 --action setup --tools "all" +# ./start.sh --host 192.168.1.100 --key ./id_ed25519 --action connect +# ./start.sh --config config.json +# +# Arguments: +# --host Server IP address (required) +# --port SSH port (default: 22, after setup: 22022) +# --password SSH password for root (initial setup) +# --key Path to SSH private key (for stefan user) +# --action "setup" or "connect" (required) +# --tools Tools to deploy (passed to setup.sh) +# --skip-ssl Skip SSL setup (passed to setup.sh) +# --config Path to JSON config file +# --json Inline JSON configuration +# +# JSON Config Format: +# { +# "host": "192.168.1.100", +# "port": 22, +# "password": "...", +# "key": "./id_ed25519", +# "action": "setup", +# "tools": "all", +# "skip_ssl": false, +# "customer": "acme", +# "domain": "acme.com", +# "company_name": "Acme Corp" +# } +# + +set -euo pipefail + +# ============================================================================= +# CONFIGURATION DEFAULTS +# ============================================================================= + +SERVER_IP="" +SERVER_PORT="22" +SERVER_PASSWORD="" +SSH_KEY="" +ACTION="" +TOOLS="" +SKIP_SSL="" +ROOT_SSL="" +CONFIG_JSON="" + +# env_setup.sh parameters +CUSTOMER="" +DOMAIN="" +COMPANY_NAME="" + +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + +usage() { + echo "Usage: $0 --host IP --action [setup|connect] [options]" + echo "" + echo "Required:" + echo " --host Server IP address" + echo " --action 'setup' or 'connect'" + echo "" + echo "Authentication (one required for setup):" + echo " --password SSH password for root (initial setup)" + echo " --key Path to SSH private key" + echo "" + echo "Optional:" + echo " --port SSH port (default: 22)" + echo " --tools Tools to deploy (comma-separated or 'all')" + echo " --skip-ssl Skip SSL certificate setup" + echo " --root-ssl Include root domain in SSL certificate" + echo " --customer Customer name for env_setup.sh" + echo " --domain Domain for env_setup.sh" + echo " --company Company name for env_setup.sh" + echo "" + echo "JSON Input:" + echo " --config Path to JSON config file" + echo " --json Inline JSON configuration" + echo "" + echo "Examples:" + echo " $0 --host 1.2.3.4 --password 'pass' --action setup --tools all" + echo " $0 --host 1.2.3.4 --key ./id_ed25519 --port 22022 --action connect" + echo " $0 --config config.json" + exit 1 +} + +check_and_replace_ssh_key() { + local server_ip=$1 + local server_port=$2 + + # Ensure ~/.ssh directory exists + mkdir -p ~/.ssh 2>/dev/null || true + chmod 700 ~/.ssh 2>/dev/null || true + + # Remove old host key if it exists + ssh-keygen -R "[$server_ip]:$server_port" >/dev/null 2>&1 || true + ssh-keygen -R "$server_ip" >/dev/null 2>&1 || true + + # Add current host key + ssh-keyscan -p "$server_port" "$server_ip" >> ~/.ssh/known_hosts 2>/dev/null || true +} + +parse_json() { + local json="$1" + + # Check if jq is available + if ! command -v jq &> /dev/null; then + echo "ERROR: jq is required for JSON parsing. Install with: apt install jq" + exit 1 + fi + + SERVER_IP=$(echo "$json" | jq -r '.host // empty') + SERVER_PORT=$(echo "$json" | jq -r '.port // "22"') + SERVER_PASSWORD=$(echo "$json" | jq -r '.password // empty') + SSH_KEY=$(echo "$json" | jq -r '.key // empty') + ACTION=$(echo "$json" | jq -r '.action // empty') + TOOLS=$(echo "$json" | jq -r '.tools // empty') + SKIP_SSL=$(echo "$json" | jq -r 'if .skip_ssl == true then "true" else "" end') + CUSTOMER=$(echo "$json" | jq -r '.customer // empty') + DOMAIN=$(echo "$json" | jq -r '.domain // empty') + COMPANY_NAME=$(echo "$json" | jq -r '.company_name // empty') +} + +# ============================================================================= +# ARGUMENT PARSING +# ============================================================================= + +while [[ $# -gt 0 ]]; do + case $1 in + --host) + SERVER_IP="$2" + shift 2 + ;; + --port) + SERVER_PORT="$2" + shift 2 + ;; + --password) + SERVER_PASSWORD="$2" + shift 2 + ;; + --key) + SSH_KEY="$2" + shift 2 + ;; + --action) + ACTION="$2" + shift 2 + ;; + --tools) + TOOLS="$2" + shift 2 + ;; + --skip-ssl) + SKIP_SSL="true" + shift + ;; + --root-ssl) + ROOT_SSL="true" + shift + ;; + --customer) + CUSTOMER="$2" + shift 2 + ;; + --domain) + DOMAIN="$2" + shift 2 + ;; + --company) + COMPANY_NAME="$2" + shift 2 + ;; + --config) + CONFIG_JSON=$(cat "$2") + parse_json "$CONFIG_JSON" + shift 2 + ;; + --json) + parse_json "$2" + shift 2 + ;; + --help|-h) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +# ============================================================================= +# VALIDATION +# ============================================================================= + +if [[ -z "$SERVER_IP" ]]; then + echo "ERROR: --host is required" + usage +fi + +if [[ -z "$ACTION" ]]; then + echo "ERROR: --action is required (setup or connect)" + usage +fi + +if [[ "$ACTION" != "setup" && "$ACTION" != "connect" ]]; then + echo "ERROR: --action must be 'setup' or 'connect'" + usage +fi + +# ============================================================================= +# SSH COMMAND BUILDERS +# ============================================================================= + +# SSH options for reliable connections with keep-alive +SSH_OPTS="-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=~/.ssh/known_hosts -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=3" + +# Build SSH/SCP commands based on auth method +if [[ -n "$SERVER_PASSWORD" ]]; then + # Password-based authentication (for initial root setup) + if ! command -v sshpass &> /dev/null; then + echo "ERROR: sshpass is required for password auth. Install with: apt install sshpass" + exit 1 + fi + SSH_CMD="sshpass -p '$SERVER_PASSWORD' ssh $SSH_OPTS -p $SERVER_PORT root@$SERVER_IP" + SCP_CMD="sshpass -p '$SERVER_PASSWORD' scp $SSH_OPTS -P $SERVER_PORT" + SSH_USER="root" +elif [[ -n "$SSH_KEY" ]]; then + # Key-based authentication (for stefan user) + if [[ ! -f "$SSH_KEY" ]]; then + echo "ERROR: SSH key file not found: $SSH_KEY" + exit 1 + fi + SSH_CMD="ssh $SSH_OPTS -i $SSH_KEY -p $SERVER_PORT stefan@$SERVER_IP" + SCP_CMD="scp $SSH_OPTS -i $SSH_KEY -P $SERVER_PORT" + SSH_USER="stefan" +else + echo "ERROR: Either --password or --key is required" + usage +fi + +# ============================================================================= +# MAIN EXECUTION +# ============================================================================= + +echo "=== LetsBe Deployment ===" +echo "Server: $SERVER_IP:$SERVER_PORT" +echo "Action: $ACTION" +echo "Auth: ${SSH_USER:-unknown}" +echo "" + +# Update SSH known hosts +check_and_replace_ssh_key "$SERVER_IP" "$SERVER_PORT" + +if [[ "$ACTION" == "setup" ]]; then + # ========================================================================= + # SETUP MODE + # ========================================================================= + + if [[ "$SSH_USER" != "root" ]]; then + echo "WARNING: Setup typically requires root access for initial deployment." + echo "Proceeding with $SSH_USER user..." + fi + + REMOTE_BASE="/opt/letsbe" + + echo "[0/6] Creating remote directory structure..." + eval "$SSH_CMD 'mkdir -p ${REMOTE_BASE}/{scripts,env,stacks,nginx,config,data}'" + + echo "[1/6] Uploading setup scripts..." + eval "$SCP_CMD env_setup.sh ${SSH_USER}@${SERVER_IP}:${REMOTE_BASE}/scripts/" 2>/dev/null || \ + eval "$SCP_CMD env_setup.sh ${SSH_USER}@${SERVER_IP}:/tmp/" + + eval "$SCP_CMD setup.sh ${SSH_USER}@${SERVER_IP}:${REMOTE_BASE}/scripts/" 2>/dev/null || \ + eval "$SCP_CMD setup.sh ${SSH_USER}@${SERVER_IP}:/tmp/" + + echo "[2/6] Uploading backups script..." + if ! eval "$SSH_CMD '[ -f ${REMOTE_BASE}/scripts/backups.sh ]'" 2>/dev/null; then + eval "$SCP_CMD backups.sh ${SSH_USER}@${SERVER_IP}:${REMOTE_BASE}/scripts/" 2>/dev/null || \ + eval "$SCP_CMD backups.sh ${SSH_USER}@${SERVER_IP}:/tmp/" + else + echo " backups.sh already exists, skipping." + fi + + echo "[3/6] Uploading nginx configurations..." + # Check if nginx folder has .conf files (not just exists as empty dir) + if ! eval "$SSH_CMD 'ls ${REMOTE_BASE}/nginx/*.conf >/dev/null 2>&1'" 2>/dev/null; then + eval "$SCP_CMD -r nginx/* ${SSH_USER}@${SERVER_IP}:${REMOTE_BASE}/nginx/" 2>/dev/null || \ + eval "$SCP_CMD -r nginx ${SSH_USER}@${SERVER_IP}:/tmp/" + else + echo " nginx configs already exist, skipping." + fi + + echo "[4/6] Uploading docker stacks..." + # Check if stacks folder has docker-compose files (not just exists as empty dir) + if ! eval "$SSH_CMD 'ls ${REMOTE_BASE}/stacks/*/docker-compose.yml >/dev/null 2>&1'" 2>/dev/null; then + eval "$SCP_CMD -r stacks/* ${SSH_USER}@${SERVER_IP}:${REMOTE_BASE}/stacks/" 2>/dev/null || \ + eval "$SCP_CMD -r stacks ${SSH_USER}@${SERVER_IP}:/tmp/" + else + echo " stacks already exist, skipping." + fi + + echo "[5/6] Running env_setup.sh..." + if ! eval "$SSH_CMD '[ -f ${REMOTE_BASE}/.env_installed ]'" 2>/dev/null; then + # Build env_setup.sh arguments (escape values for nested shell) + ENV_ARGS="" + [[ -n "$CUSTOMER" ]] && ENV_ARGS="$ENV_ARGS --customer $(printf '%q' "$CUSTOMER")" + [[ -n "$DOMAIN" ]] && ENV_ARGS="$ENV_ARGS --domain $(printf '%q' "$DOMAIN")" + [[ -n "$COMPANY_NAME" ]] && ENV_ARGS="$ENV_ARGS --company $(printf '%q' "$COMPANY_NAME")" + + if [[ -n "$ENV_ARGS" ]]; then + eval "$SSH_CMD \"bash ${REMOTE_BASE}/scripts/env_setup.sh $ENV_ARGS && touch ${REMOTE_BASE}/.env_installed\"" || \ + eval "$SSH_CMD \"bash /tmp/env_setup.sh $ENV_ARGS && touch /tmp/.env_installed\"" + else + echo " WARNING: No customer/domain/company provided. Skipping env_setup.sh" + echo " Run manually: env_setup.sh --customer X --domain Y --company Z" + fi + else + echo " env_setup.sh already ran, skipping." + fi + + # Create initial backup before setup + if ! eval "$SSH_CMD '[ -f ${REMOTE_BASE}/initial_setup_backup.zip ]'" 2>/dev/null; then + echo " Creating initial setup backup..." + eval "$SSH_CMD 'apt-get update && apt-get install -y zip && cd ${REMOTE_BASE} && zip -r initial_setup_backup.zip stacks/* nginx/* scripts/backups.sh 2>/dev/null || true'" + eval "$SCP_CMD ${SSH_USER}@${SERVER_IP}:${REMOTE_BASE}/initial_setup_backup.zip ." 2>/dev/null || true + fi + + echo "[6/6] Running setup.sh..." + if ! eval "$SSH_CMD '[ -f ${REMOTE_BASE}/.setup_installed ]'" 2>/dev/null; then + # Build setup.sh arguments + SETUP_ARGS="" + [[ -n "$TOOLS" ]] && SETUP_ARGS="$SETUP_ARGS --tools '$TOOLS'" + [[ -n "$DOMAIN" ]] && SETUP_ARGS="$SETUP_ARGS --domain '$DOMAIN'" + [[ "$SKIP_SSL" == "true" ]] && SETUP_ARGS="$SETUP_ARGS --skip-ssl" + [[ "$ROOT_SSL" == "true" ]] && SETUP_ARGS="$SETUP_ARGS --root-ssl" + + # Run setup.sh directly in foreground (connection stays alive with PermitRootLogin yes) + echo "Running setup.sh (this may take 10-15 minutes)..." + eval "$SSH_CMD 'bash ${REMOTE_BASE}/scripts/setup.sh $SETUP_ARGS && touch ${REMOTE_BASE}/.setup_installed'" + echo "Setup completed successfully!" + else + echo " setup.sh already ran, skipping." + fi + + echo "" + echo "==============================================" + echo " Setup Complete!" + echo "==============================================" + echo "" + echo "SSH is now on port 22022 with key-only auth." + echo "To connect: $0 --host $SERVER_IP --key ./id_ed25519 --port 22022 --action connect" + echo "" + +elif [[ "$ACTION" == "connect" ]]; then + # ========================================================================= + # CONNECT MODE + # ========================================================================= + + echo "Connecting to server..." + echo "(Disconnect with Ctrl+C or 'exit')" + echo "" + + # Try password auth first, then key auth + if [[ -n "$SERVER_PASSWORD" ]]; then + eval "$SSH_CMD" || { + echo "Password auth failed. Trying key auth..." + if [[ -n "$SSH_KEY" ]]; then + ssh -i "$SSH_KEY" -p "$SERVER_PORT" stefan@"$SERVER_IP" + else + echo "No SSH key provided. Connection failed." + exit 1 + fi + } + elif [[ -n "$SSH_KEY" ]]; then + ssh -i "$SSH_KEY" -p "$SERVER_PORT" stefan@"$SERVER_IP" || { + echo "Key auth failed. Check SSH configuration." + exit 1 + } + fi +fi