Attached is an illustration of how our SIAAS (sovereign infrastructure as a service) framework operates - as you can probably see, we have applications in what is known as the 'primary vps' and this connects to other vps's in other accounts via secure tunnel which are referred to as 'client vps's' - we use Fasthosts and are part of their Partner Programme - we need to discuss how we can set-up these client-vps's in the most efficient way as we will be doing this each time a new client signs-up with us
To efficiently scale your Sovereign Infrastructure as a Service (SIAAS) model using the Fasthosts Partner Programme, you should build an automated provisioning pipeline. This pipeline will eliminate manual work in your Customer Management Tool when onboarding new clients. Because your framework mandates that clients hold root SSH keys and you hold neither, automation must handle the server spin-up, basic securing, and infrastructure connection immediately at creation. [1, 2]
[New Client Sign-Up]
│
▼
1. Fasthosts API Call (Spin up Ubuntu 24.04 / 26.04 LTS VPS)
│
▼
2. Inject Cloud-Init script (Installs Docker, WireGuard, and PostgreSQL 16)
│
▼
3. Establish WireGuard Tunnel back to Principal VPS
│
▼
4. Add Tenant record to Principal Django App (Triggers SectorDatabaseRouter)
Instead of clicking through the Fasthosts Cloud Panel manually, leverage the Fasthosts API to programmatically provision instances. [2, 3]
python manage.py provision_client_vps).Do not use passwords or manual SSH connections to configure the server. When calling the Fasthosts API, pass a user_data script (Cloud-Init). This script executes automatically upon the server's first boot: [6, 7]
#cloud-config
package_update: true
packages:
- docker.io
- wireguard
- ufw
write_files:
# Pre-configure the WireGuard interface
- path: /etc/wireguard/wg0.conf
permissions: '0600'
content: |
[Interface]
PrivateKey = <GENERATED_CLIENT_PRIVATE_KEY>
Address = 10.0.0.X/24
[Peer]
PublicKey = <PRINCIPAL_VPS_PUBLIC_KEY>
AllowedIPs = 172.18.0.0/16, 10.0.0.1/32
Endpoint = 88.208.212.211:51820
PersistentKeepalive = 25
runcmd:
# Enable firewall securely
- ufw default deny incoming
- ufw allow 51820/udp
- ufw enable
# Bring up the tunnel
- systemctl enable wg-quick@wg0
- systemctl start wg-quick@wg0
# Start client PostgreSQL 16 image isolated to the WireGuard interface
- docker run -d --name client_postgres -p 10.0.0.X:5432:5432 -e POSTGRES_PASSWORD=<SECURE_PASS> postgres:16-alpine
Once the client VPS boots up, it automatically calls home over WireGuard. Your Django command on the Principal VPS completes the onboarding cycle:
test-clinic) to the WireGuard IP (10.0.0.X) and the encrypted credentials.SectorDatabaseRouter and client_monitor unmanaged models will seamlessly pick up the data path (pp. 5, 7).172.18.0.0/16 Docker bridge network (p. 3). Explicitly map your internal WireGuard overlays to a completely distinct subnet block (like 10.0.0.0/24) so client traffic never collides or interferes with internal multi-container routing.0.0.0.0 or public host interfaces to completely eliminate exposure to global brute-force sweeps. [8]Would you like help writing the specific Python/Django function to connect to the Fasthosts API and handle token management, or would you prefer a template for the automated WireGuard peer generation script?
[1] https://www.fasthosts.co.uk
[2] https://www.fasthosts.co.uk
[4] https://www.fasthosts.co.uk
[5] https://www.fasthosts.co.uk
[6] https://www.fasthosts.co.uk
[7] https://www.fasthosts.co.uk
[8] https://www.fasthosts.co.uk
Okay I need to explain a few more things and then we can establish a solid plan - as you can see, the purpose of the client-vps's is that all data relating to a client-vps (the client's data) is stored within that vps - therefore, we install Postgresql - there are currently 2 apps: gp booking app and dental booking app - we have been so far installing and updating the client-vps via ssh as we develop the infrastructure, but we also know when we configure client vps's that we will not be able to do this when real clients create their Fasthost account and then give us a temporary pass - we will need to use another method and I was thinking of images (Docker) - I don't know if this is the most efficient method, but we must first establish that before we go into more detail
Using Docker containers packaged inside an Image is absolutely the most efficient and scalable method for your model (p. 4). Relying on manual SSH or temporary server passwords breaks your Sovereign Infrastructure as a Service (SIAAS) architecture (p. 24). It is a massive security risk and impossible to scale.
By switching to a container-based deployment model, you solve these structural issues entirely.
Instead of configuring database and application environments from scratch on every new server, each Fasthosts client VPS runs an isolated multi-container stack:
┌────────────────────────────────────────────────────────┐
│ Client VPS (Fasthosts Account) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ App Container │ │ DB Container │ │
│ │ (GP / Dental) │──(Local)──>│ (PostgreSQL 16) │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ ▲ │
│ │ (Secure Tunnel) │ │
│ ▼ │ │
│ ┌─────────────────────────────────────────┴────────┐ │
│ │ WireGuard Client Tunnel (wg0) │ │
└──┴────────────────────────┼────────────────────────┴──┘
│
│ (Telemetry & Sync)
▼
[ Central Principal VPS ]
Your core rule is that all client data must remain strictly inside their own VPS (p. 24). Docker fits this requirement perfectly:
client_monitor app over the tunnel (pp. 5, 17).Using Docker images as your foundation is the correct path. To map out the exact setup plan, could you tell me:
git.veripath.co.uk), do you want to configure it to act as your own private secure Docker Image Registry to store and distribute these builds?Okay Docker it is then - to answer your questions, the gp and dental apps currently share the same models, apps etc. - but they have their own databases as you can see from the attached file - whether a client is a gp clinic (gp.veripath) or a dental practice (dental.veripath), this will be established during the on-boarding process of course and the new client will be issued with their sub-domain which is set by adding and A record to the veripath.co.uk domain - therefore, the unique url for the client to log into their account and access data in their vps will be something like: 'https://test-client.gp.veripath.co.uk' for a clinic or 'https://test-client.dental.veripath.co.uk for a dental surgery - we keep our code in Forgejo within the primary vps as well as in our local dev - it would make sense I guess to use if for storing images - in terms of domain routing, at this moment in time there is no need for clients to point their domains to their vps's, but this may change in the future
Based on your architecture log, you have a solid multi-tenant structure (p. 1). Your Django apps dynamically route database queries to either sector_client or dynamic per-client aliases based on the tenant context via a TenantMiddleware and custom router (pp. 1-2).
Confirming Docker as your delivery method clarifies the implementation strategy. Since both the GP and Dental apps share the exact same codebase, model definitions, and apps (pp. 1-2), you do not need to maintain or build separate container images for them. You will build and maintain a single unified application image. Whether an instance behaves as a GP clinic or a Dental practice is determined entirely by environment variables injected at runtime.
The core implementation plan covers the delivery pipelines, registry configuration, and secure network routing.
Instead of managing separate deployment tracks, package all of your Django code into one master image.
sector_client_{subdomain} vs sector_dental structures) (p. 1).postgres_data) mapped to the host file system. This ensures you can destroy, swap, or update the application containers without touching raw user records.Since your source code lives inside Forgejo on the Primary VPS, you can use its built-in container registry features to completely automate your distribution channel.
[Local Dev / Primary VPS Git] ──> [Forgejo Actions CI/CD]
│
▼
[Forgejo Container Registry]
│
(Authenticated Pull via WG Tunnel)
│
▼
[Client VPS Docker Stack]
hub.veripath.co.uk or your primary domain equivalent)..forgejo/workflows/build.yml). Every time you push a stable tag or merge to your main branch, Forgejo automatically runs Docker Buildx, tags the image (e.g., git.veripath.co.uk/org/booking-app:latest), and stores it locally on the Primary VPS registry.Since clients do not point their custom domains directly to their VPS servers, your network architecture can remain secure, hidden, and centralized. Instead of exposing the Client VPS IP addresses directly to public DNS A records, your Primary VPS acts as the global gateway/ingress proxy.
[User Browser] ──> https://test-client.gp.veripath.co.uk (Public Internet)
│
▼
[Primary VPS Reverse Proxy (Nginx)]
│
(Routed over Secure WireGuard Tunnel)
│
▼
[Client VPS WireGuard Interface (10.0.0.X)]
│
▼
[Client App Container]
*.gp.veripath.co.uk and *.dental.veripath.co.uk straight to the static public IP address of your Primary VPS.https://test-client.gp.veripath.co.uk, Nginx reads the subdomain header.test-client belongs to the client VPS allocated at WireGuard IP 10.0.0.5. It proxies the HTTP traffic strictly across the internal wg0 tunnel.With these architectural choices finalized, the automated workflow for every new client signing up on Fasthosts looks like this:
Fasthosts Setup: Your app calls the Fasthosts API to create a vanilla Ubuntu VPS server.
Cloud-Init Execution: Fasthosts boots the machine, installs Docker, establishes the WireGuard link back to the Primary VPS, and brings up an isolated PostgreSQL 16 database (p. 1).
Image Pull: The Cloud-Init script logs into your private Forgejo container registry via the internal WireGuard tunnel IP (10.0.0.1) and pulls the unified Django image.
Launch Context: The script spins up the container using specific environment variable markers tailored to that client:
docker run -d \
--name app_container \
-e TENANT_SUBDOMAIN="test-client" \
-e CLINIC_TYPE="GP" \
-e DATABASE_URL="postgresql://user:pass@127.0.0.1:5432/sector_client" \
git.veripath.co.uk/org/booking-app:latest
Proxy Activation: The Primary VPS appends the new upstream route to its Nginx config, reloads, and the new subdomain securely points straight to the client container.
How should we structure the next steps? If you like, I can provide:
okay I think we are ready to put together a template - this should give us the start we need for our opencode agent to perform the necessary configuration work - we will be adding more apps and databases in the future, but that doesn't alter anything we've discussed here - that is just simply updating Docker images in Forgejo - so let's just put this template together and then I can try with opencode
Here is the foundational automated template package designed for your OpenCode Agent to use during client onboarding.
Because both the GP and Dental apps share the same codebase, this setup relies on a single Unified App Container driven by environment variables, paired with a sovereign PostgreSQL 16 Container using persistent volumes (p. 1).
This blueprint contains three core components: the Fasthosts Cloud-Init Target Configuration, the multi-container Docker Compose Stack, and the Primary VPS Nginx Route Wrapper.
Your OpenCode Agent will inject this script via the Fasthosts API during server creation. It installs Docker, creates the file structures, and safely maps your persistent volumes so patient records remain strictly sovereign inside the client's VPS (p. 1).
#cloud-config
package_update: true
package_upgrade: true
packages:
- docker.io
- docker-compose-v2
- wireguard
- ufw
write_files:
# 1. Establish the Secure WireGuard Tunnel back to Primary VPS
- path: /etc/wireguard/wg0.conf
permissions: '0600'
content: |
[Interface]
PrivateKey = {{ CLIENT_WG_PRIVATE_KEY }}
Address = {{ CLIENT_INTERNAL_WG_IP }}/24
[Peer]
PublicKey = {{ PRIMARY_VPS_WG_PUBLIC_KEY }}
AllowedIPs = 10.0.0.1/32
Endpoint = {{ PRIMARY_VPS_PUBLIC_IP }}:51820
PersistentKeepalive = 25
# 2. Build the Application Directory Stack
- path: /opt/siaas/docker-compose.yml
permissions: '0644'
content: |
version: '3.8'
services:
client_db:
image: postgres:16-alpine
container_name: siaas_postgres_db
restart: always
environment:
POSTGRES_DB: sector_client
POSTGRES_USER: siaas_admin
POSTGRES_PASSWORD: {{ GENERATED_DB_PASSWORD }}
volumes:
- postgres_sovereign_data:/var/lib/postgresql/data
expose:
- "5432"
networks:
- siaas_internal_network
client_app:
image: git.veripath.co.uk/org/booking-app:latest
container_name: siaas_app_service
restart: always
depends_on:
- client_db
environment:
- DEBUG=False
- TENANT_SUBDOMAIN={{ CLIENT_SUBDOMAIN }}
- SECTOR_TYPE={{ CLINIC_SECTOR_TYPE }} # Expected values: 'GP' or 'DENTAL'
- DATABASE_URL=postgres://siaas_admin:{{ GENERATED_DB_PASSWORD }}@client_db:5432/sector_client
- PRIMARY_TUNNEL_URL=http://10.0.0.1:8000
ports:
- "127.0.0.1:8080:8000"
networks:
- siaas_internal_network
volumes:
postgres_sovereign_data:
driver: local
networks:
siaas_internal_network:
driver: bridge
runcmd:
# Firewall Hardening (Block everything publicly except WireGuard)
- ufw default deny incoming
- ufw allow 51820/udp
- ufw enable
# Spin up the networking tunnel
- systemctl enable wg-quick@wg0
- systemctl start wg-quick@wg0
# Authenticate with the Primary VPS Forgejo Registry via internal tunnel line
- docker login git.veripath.co.uk -u {{ FORGEJO_DEPLOY_USER }} -p {{ FORGEJO_DEPLOY_TOKEN }}
# Launch the production application infrastructure
- cd /opt/siaas && docker compose up -d
Because clients are using subdomains under your master domain (e.g., test-client.gp.veripath.co.uk), your OpenCode Agent must add an upstream configuration blocks to the Primary VPS Nginx layer right after creating the server.
# /etc/nginx/sites-available/{{ CLIENT_SUBDOMAIN }}.conf
# Route Configuration for GP practices
server {
listen 443 ssl;
server_name {{ CLIENT_SUBDOMAIN }}.gp.veripath.co.uk {{ CLIENT_SUBDOMAIN }}.dental.veripath.co.uk;
ssl_certificate /etc/letsencrypt/live/veripath.co.uk/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/veripath.co.uk/privkey.pem;
access_log /var/log/nginx/siaas_{{ CLIENT_SUBDOMAIN }}_access.log;
error_log /var/log/nginx/siaas_{{ CLIENT_SUBDOMAIN }}_error.log;
location / {
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;
# Directs raw web traffic directly down the private WireGuard link
proxy_pass http://{{ CLIENT_INTERNAL_WG_IP }}:8080;
}
}
To execute this deployment seamlessly, your OpenCode agent will need to inject the following dynamic parameters into the placeholders {{ VARIABLE_NAME }} above during execution:
CLIENT_SUBDOMAIN: The string identifier chosen by the user (e.g., test-clinic).CLINIC_SECTOR_TYPE: Set to either GP or DENTAL (p. 1). This variable dictates how your dynamic TenantMiddleware and database routers allocate and query datasets inside the container execution path (pp. 1-2).CLIENT_INTERNAL_WG_IP: The dedicated IP allocated on your private overlay network (e.g., 10.0.0.45).GENERATED_DB_PASSWORD: A unique, cryptographically strong database string created on the fly.This setup keeps your infrastructure clean. To deploy new feature releases across all apps or support new database schemas later, you only need to push code updates to Forgejo to rebuild the central container image. The client servers will then pull down the fresh image without altering their database configuration.
Let me know if you would like me to draft the automation Python/Bash sequence for your agent to automatically link the Nginx site configuration, toggle the symlinks, and run the nginx -s reload commands!