Tasuku
Operations

Remote Docker on a Hetzner VM

Run Tasuku agent containers on an inexpensive, dedicated Docker host without exposing the Docker API to the internet.

Tasuku can keep its server, worker, and Postgres services on one host while running agent containers on a separate virtual machine. The VM is a remote Docker host; it does not become a container. The worker connects to its Docker Engine endpoint.

The recommended endpoint is:

tcp://10.20.0.3:2376

Port 2376 must bind only to a private interface, require mutual TLS (mTLS), and accept traffic only from the Tasuku worker host. Never publish Docker's unauthenticated port 2375.

Docker access is root-equivalent

A Docker client certificate can create privileged containers, mount host paths, and control the VM. Protect it like a root password. A private network and mTLS protect the connection; they do not make the remote daemon safe for untrusted clients.

Operator-controlled Docker configuration

Docker host selection is a worker setting. Configure DOCKER_HOST on the worker; it is intentionally not exposed as a repository setting.

Tasuku host                                Hetzner Docker VM
server + Postgres                         Docker Engine
worker container                          agent containers
      |                                         ^
      +--- private network + mTLS :2376 --------+

Public ingress to Docker VM: SSH from an allowlisted admin IP only
Public ingress to port 2376: none

Use a dedicated VM for agent execution. Do not colocate a database, customer workload, or another tenant's containers on it. Tasuku's container restrictions reduce the impact of an agent process, but Docker Engine remains a privileged trust boundary.

Why Hetzner and which size

Hetzner is a practical default for a low-cost, single-host deployment. Its cost-optimized CX33 is x86-64 with 4 shared vCPUs, 8 GB RAM, and 80 GB disk. Hetzner's price published for new EU orders from June 15, 2026 is EUR 8.49/month excluding VAT and IPv4. The smaller CX23 costs EUR 5.49/month but has only 4 GB RAM, equal to Tasuku's default per-agent memory limit, so it leaves too little host headroom.

Start with CX33 for one concurrent agent. Measure CPU, memory, and disk use before increasing concurrency. Use a dedicated-vCPU plan when latency or sustained CPU performance matters more than minimum price. Prefer x86-64 unless every tool in the agent image has been verified on Arm.

Sources: Hetzner cost-optimized server specifications, June 2026 Hetzner prices, and Hetzner shared versus dedicated resources.

1. Provision the VM and private network

This example uses these addresses:

ComponentPrivate address
Tasuku worker host10.20.0.2
Remote Docker VM10.20.0.3
  1. Create a Hetzner Cloud Network such as 10.20.0.0/24 and attach both servers. Hetzner Networks provide private layer-3 addresses, but traffic is not automatically encrypted, which is why this guide still requires mTLS.
  2. Create an Ubuntu 24.04 CX33 in the same network and select an SSH key during creation.
  3. Attach a public IP only when needed for administration and outbound package/image access.
  4. Apply a Hetzner Cloud Firewall that allows TCP 22 only from the administrator's fixed public IP. Do not add public rules for 2375 or 2376; unspecified inbound traffic is dropped.
  5. Use SSH keys only. Create a named sudo administrator and disable password authentication and direct root login after testing a second session.

Hetzner Cloud Firewalls do not filter traffic inside a private Cloud Network. The host firewall configured later is therefore required for port 2376. See Hetzner Networks, network encryption guidance, and Cloud Firewall behavior.

If the Tasuku host is outside Hetzner, put both hosts on a WireGuard or equivalent private network and bind Docker to that interface. If that is impossible, allowlist only the Tasuku host's static public IP at both the provider and host firewalls and keep mTLS enabled.

2. Install Docker Engine

On the remote VM, install Docker from Docker's signed apt repository. Docker recommends the repository for production installation; its convenience script is intended for testing and development.

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

. /etc/os-release
printf '%s\n' \
  'Types: deb' \
  'URIs: https://download.docker.com/linux/ubuntu' \
  "Suites: ${UBUNTU_CODENAME:-$VERSION_CODENAME}" \
  'Components: stable' \
  "Architectures: $(dpkg --print-architecture)" \
  'Signed-By: /etc/apt/keyrings/docker.asc' \
  | sudo tee /etc/apt/sources.list.d/docker.sources

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world

Keep the VM patched and follow Docker's Ubuntu installation instructions when upgrading Docker.

3. Create an mTLS certificate set

Follow Docker's daemon socket protection guide. Generate the CA and certificates on a trusted administrator machine, not inside the repository. Replace 10.20.0.3 if the VM uses another private IP.

umask 077
mkdir docker-pki
cd docker-pki
DOCKER_VM_IP=10.20.0.3

openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -sha256 \
  -key ca-key.pem -out ca.pem -subj '/CN=tasuku-docker-ca'

openssl genrsa -out server-key.pem 4096
openssl req -new -sha256 -key server-key.pem \
  -out server.csr -subj '/CN=tasuku-docker'
printf 'subjectAltName = IP:%s\nextendedKeyUsage = serverAuth\n' "$DOCKER_VM_IP" > server-ext.cnf
openssl x509 -req -days 365 -sha256 -in server.csr \
  -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
  -out server-cert.pem -extfile server-ext.cnf

openssl genrsa -out key.pem 4096
openssl req -new -sha256 -key key.pem \
  -out client.csr -subj '/CN=tasuku-worker'
printf 'extendedKeyUsage = clientAuth\n' > client-ext.cnf
openssl x509 -req -days 365 -sha256 -in client.csr \
  -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
  -out cert.pem -extfile client-ext.cnf

chmod 0400 ca-key.pem server-key.pem key.pem
chmod 0444 ca.pem server-cert.pem cert.pem

Distribute only these files:

DestinationFiles
Remote Docker VMca.pem, server-cert.pem, server-key.pem
Tasuku hostca.pem, cert.pem, key.pem

Keep ca-key.pem and the CA serial file offline for rotation. Do not copy the CA private key to either server. Delete the temporary CSR and extension files after verifying the deployment. Set an expiry alert and rotate the server and client certificates before 365 days.

4. Bind Docker to the private interface

On the remote Docker VM, install the three server-side files:

sudo install -d -m 0700 /etc/docker/tls
sudo install -m 0444 ca.pem server-cert.pem /etc/docker/tls/
sudo install -m 0400 server-key.pem /etc/docker/tls/

Run sudo systemctl edit docker.service and add this override. Replace the private IP when necessary.

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://10.20.0.3:2376 --containerd=/run/containerd/containerd.sock --tlsverify --tlscacert=/etc/docker/tls/ca.pem --tlscert=/etc/docker/tls/server-cert.pem --tlskey=/etc/docker/tls/server-key.pem

Do not also set hosts in /etc/docker/daemon.json; Docker documents that configuring listeners in both systemd and daemon.json causes a startup conflict. Reload and verify the listener:

sudo systemctl daemon-reload
sudo systemctl restart docker.service
sudo systemctl --no-pager --full status docker.service
sudo ss -lntp | grep ':2376'

The output must show 10.20.0.3:2376, not 0.0.0.0:2376. See Docker remote daemon access.

5. Add a host firewall

Set the variables to real addresses before running these commands. Add the SSH rule before enabling UFW to avoid locking yourself out.

ADMIN_PUBLIC_IP=203.0.113.10
TASUKU_PRIVATE_IP=10.20.0.2

sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from "$ADMIN_PUBLIC_IP" to any port 22 proto tcp
sudo ufw allow from "$TASUKU_PRIVATE_IP" to any port 2376 proto tcp
sudo ufw enable
sudo ufw status verbose

Keep Tasuku agent containers free of published host ports. Review host firewall behavior after every Docker upgrade.

6. Install the client certificate on the Tasuku host

Copy ca.pem, cert.pem, and key.pem to the Tasuku host, then build the worker image and give only its numeric user access to the directory:

docker compose build worker
WORKER_UID="$(docker compose run --rm --no-deps --entrypoint id worker -u)"
WORKER_GID="$(docker compose run --rm --no-deps --entrypoint id worker -g)"

sudo install -d -m 0700 /etc/tasuku/docker-client
sudo install -m 0444 ca.pem cert.pem /etc/tasuku/docker-client/
sudo install -m 0400 key.pem /etc/tasuku/docker-client/
sudo chown -R "$WORKER_UID:$WORKER_GID" /etc/tasuku/docker-client

Test mTLS from the Tasuku host. This verifies the certificate chain, server identity, firewall, and Docker API before Tasuku is involved:

sudo env \
  DOCKER_HOST=tcp://10.20.0.3:2376 \
  DOCKER_TLS_VERIFY=1 \
  DOCKER_CERT_PATH=/etc/tasuku/docker-client \
  docker version

Both client and server sections must be present.

7. Build the agent image on the remote daemon

The tasuku-agent-runtime:local image built by the default Compose topology exists only on the local Docker host. Build it explicitly on the remote daemon from the Tasuku repository root:

sudo env \
  DOCKER_HOST=tcp://10.20.0.3:2376 \
  DOCKER_TLS_VERIFY=1 \
  DOCKER_CERT_PATH=/etc/tasuku/docker-client \
  docker build --file docker/Dockerfile.agent --tag tasuku-agent-runtime:local .

sudo env \
  DOCKER_HOST=tcp://10.20.0.3:2376 \
  DOCKER_TLS_VERIFY=1 \
  DOCKER_CERT_PATH=/etc/tasuku/docker-client \
  docker image inspect tasuku-agent-runtime:local

Repeat this step whenever the runtime Dockerfile or agent toolchain changes. A private registry is a better distribution path when several Docker VMs must run the same immutable image.

8. Point the Tasuku worker at the endpoint

Require Docker Compose 2.24.4 or later for the !override merge tag. Create docker-compose.remote-docker.yml beside docker-compose.yml:

services:
  worker:
    environment:
      DOCKER_HOST: ${TASUKU_DOCKER_HOST:?TASUKU_DOCKER_HOST is required}
      DOCKER_TLS_VERIFY: "1"
      DOCKER_CERT_PATH: /run/docker-tls
    volumes: !override
      - type: bind
        source: /etc/tasuku/docker-client
        target: /run/docker-tls
        read_only: true
        bind:
          create_host_path: false
    group_add: !reset []
    depends_on: !override
      migrate:
        condition: service_completed_successfully

  agent-runtime:
    profiles: ["local-docker"]

This removes the local Docker socket mount, removes its supplemental group, and stops the local agent-runtime build service from being part of the remote-Docker topology. The merge behavior is defined in Docker's Compose override documentation.

Set the endpoint outside version control, render the merged model, and inspect the worker before starting it:

export TASUKU_DOCKER_HOST=tcp://10.20.0.3:2376

docker compose \
  -f docker-compose.yml \
  -f docker-compose.remote-docker.yml \
  config

The rendered worker must have /run/docker-tls, must not have /var/run/docker.sock, and must contain all three Docker TLS environment variables. Then start Tasuku:

docker compose \
  -f docker-compose.yml \
  -f docker-compose.remote-docker.yml \
  up --build -d

docker compose \
  -f docker-compose.yml \
  -f docker-compose.remote-docker.yml \
  exec worker docker version

In Tasuku, select the Docker sandbox provider and set the runtime image to tasuku-agent-runtime:local. Keep CPU, memory, and PID limits appropriate for the VM. The worker environment is the authoritative Docker endpoint.

9. Verify end to end

  1. Confirm docker version succeeds inside the worker container.
  2. Trigger a small Tasuku workflow against a test repository.
  3. Query the remote engine and confirm a tasuku-* container appears there, not on the Tasuku host.
  4. Confirm the container is removed after completion.
  5. Confirm public scans of the VM cannot reach ports 2375 or 2376.
  6. Reboot the remote VM and verify Docker, the firewall, and the worker connection recover.

Operational controls

  • Treat the VM as disposable compute. Keep the runtime image reproducible and rebuild the VM rather than preserving manual state.
  • Patch Ubuntu and Docker regularly. Test client/server Docker API compatibility during upgrades.
  • Monitor disk, memory, load, Docker daemon restarts, certificate expiry, and failed connections.
  • Review stopped containers, unused images, build cache, and volumes before pruning. Never schedule an indiscriminate docker system prune --volumes.
  • Keep one remote Docker host per security boundary. mTLS authenticates the worker; it does not provide tenant-level Docker authorization.
  • Keep the client key out of Git, .env, Postgres, logs, and backups that do not have root-secret protection.
  • Apply provider-level outbound controls if your deployment requires network destination restrictions; Tasuku does not maintain an outbound destination allowlist.

Troubleshooting

SymptomCheck
connection refusedPrivate route, ss listener, UFW source IP, and Docker service logs.
x509: certificate is valid for ...Reissue the server certificate with the exact private IP or DNS name in its SAN.
certificate signed by unknown authorityVerify the Tasuku host and Docker daemon use certificates from the same CA.
no such image: tasuku-agent-runtime:localBuild or pull the image on the remote daemon, not only on the Tasuku host.
permission denied reading key.pemRecheck the numeric worker UID/GID and /etc/tasuku/docker-client ownership.
Worker still uses the local socketInspect the merged Compose model and confirm the !override file was supplied to every command.
Agent container cannot startVerify worker-level DOCKER_HOST connectivity and that the runtime image exists on the remote host.

EC2 equivalent

The same design works on EC2: put the Tasuku worker and Docker instance in the same VPC, bind Docker to the instance's private address, and allow TCP 2376 only from the Tasuku worker's security group. Keep mTLS because network rules restrict reachability but do not protect a stolen or misrouted Docker client credential. AWS documents that private IPv4 addresses are not internet-reachable and that a security group can use another security group as its source.

Use EC2 when Tasuku already operates inside an AWS VPC or depends on AWS operational controls. For a small standalone deployment, Hetzner is usually the simpler, lower-cost starting point.

On this page