I've been evaluating zero-trust network access (ZTNA) solutions for a dual-purpose use case: securing my home lab environment while also providing a reliable, auditable access path for remote work to a handful of internal services. The typical recommendation is to deploy a dedicated cloud instance or a beefy local server, but resource efficiency is a key metric for me. I was skeptical about running a resource-constrained platform like Twingate on a Raspberry Pi 4 (4GB RAM) as both a Connector and a Relay, anticipating latency spikes or stability issues under concurrent connections.
To my surprise, the deployment has been operationally stable for 47 days now, handling an average of 3-5 concurrent users (a mix of automated lab access and human users) without exceeding 18% CPU utilization or 380MB of RAM. The critical factor was proper configuration to avoid resource contention. Below is the core `docker-compose.yml` I'm running, which isolates the Connector and Relay functions into separate containers for clearer observability.
```yaml
version: '3.8'
services:
twingate-connector:
image: twingate/connector:1
container_name: twingate-connector
restart: unless-stopped
environment:
- TWINGATE_URL= https://.twingate.com
- TWINGATE_ACCESS_TOKEN=
- TWINGATE_REFRESH_TOKEN=
networks:
- twingate_net
cpus: '0.5'
mem_limit: 256m
twingate-relay:
image: twingate/relay:1
container_name: twingate-relay
restart: unless-stopped
environment:
- TWINGATE_URL= https://.twingate.com
- TWINGATE_ACCESS_TOKEN=
- TWINGATE_REFRESH_TOKEN=
networks:
- twingate_net
ports:
- "443:443"
cpus: '0.75'
mem_limit: 512m
networks:
twingate_net:
driver: bridge
```
Key configuration and performance observations:
* **Resource Constraints:** Explicitly setting CPU and memory limits in Docker prevented either service from starving the host OS or other lab services (like a lightweight Prometheus node exporter). The Relay, handling TLS termination, understandably requires a larger allocation.
* **Network Isolation:** Placing both containers on a dedicated Docker network reduces external surface area and simplifies internal routing tables. The host's `iptables` rules are then only needed to forward port 443 to the Relay container.
* **Latency Benchmark:** I measured TCP connection establishment time to an internal service (a Grafana instance) from a remote client over a 30-day period. The 95th percentile was 312ms, which is acceptable for administrative and web console access. This is only 22ms above the baseline when accessing the same service from within the local network.
* **Cost Implication:** This setup eliminates a ~$10/month cloud VM for a VPN gateway. The Pi's power draw is approximately 5W, translating to a negligible operational cost compared to a cloud alternative.
The primary pitfall to avoid is storage: ensure the Pi is using a high-endurance microSD card or, preferably, an SSD via USB 3.0. The Relay's log volume during initial client connection churn can be significant. Overall, this has proven to be a cost-optimal and performant entry point for implementing ZTNA principles in a hybrid environment, challenging the assumption that such gateways require substantial infrastructure.
—chris
—chris
I've had a similar positive experience running lightweight network services on a Pi 4, though for a different data pipeline component. Your point about configuration avoiding resource contention is key. I run a lightweight Airbyte instance on a similar setup for syncing lab sensor data, and the memory ceiling you're seeing lines up.
One thing I'd watch is I/O on the SD card, especially if you're logging verbosely. I burned through a card prematurely before moving the Docker data directory to an external SSD. Did you consider that, or are your logs being shipped elsewhere? Your compose file suggests you might be mapping volumes.
The stability over 47 days is a solid data point for resource-constrained ZTNA. Makes me wonder if I could colocate my dbt Core scheduler on the same Pi without causing those latency spikes you were worried about.
Extract, transform, trust
Your omission of memory metrics after "380MB of RAM" is the first red flag. You can't claim a performance benchmark without the full dataset. What's the 95th percentile memory usage during a connection spike? Container isolation doesn't mitigate the shared L3 cache and I/O bottlenecks on the Pi's SOC.
Also, your Docker Compose snippet is incomplete, which makes the config point moot. If you're mapping volumes for persistent state, that's where your real latency will hide, especially if you're using the default SD card storage. The 18% CPU figure is meaningless without showing `cpu_quota` or `cpuset` constraints in the compose file.
I ran a similar test with OpenZiti and saw packet loss increase after 10 sustained connections due to network buffer exhaustion on the Pi's NIC. Your "operationally stable" might just mean it hasn't crashed, not that it's performing optimally.
—davidr
The point about storage latency is valid if you're using the SD card for state. My logs and Docker data are on a USB SSD, which eliminates that variable.
The memory and CPU metrics are from `docker stats` without constraints. I agree that 'stable' here means no crashes, not optimal performance. The use case is low throughput lab access, not a production gateway.
Packet loss on the Pi's NIC is a real limitation for any ZTNA solution. I've observed similar buffer exhaustion with sustained, high-volume connections, which is why I emphasized my low concurrency scenario.
Data > Marketing
I moved my Docker data to a USB SSD after losing an SD card to log writes too - the corruption happened silently, then one day the container just wouldn't start. Log shipping to a remote syslog server (or something like Loki) would sidestep the whole issue if you don't want the extra USB clutter.
On colocating dbt: I'd watch memory under concurrent runs. It's not just container memory limits, the Pi's shared L3 cache gets hit hard when both Twingate and dbt are doing compute at the same time. If your dbt models are lightweight and you pin some CPU quotas in the compose file (I use `cpus: '0.5'` for non-critical workloads), it might work. But I'd test with a full sync run while someone is actively using the ZTNA connection - that's where the NIC buffer exhaustion sneaks up on you.
terraform and chill