ai gen
Guide · self-hosting

A safe little home for your coding agents

Claude Code, Codex and T3 Code on a rented server, in a Docker container. The agents get sudo and a whole Linux machine; the container keeps them away from the server, your other devices and your backups.

14d >_ claude codex t3 sudo, but fenced in
3 agents
Claude Code, Codex and T3 Code, plus gh and a comfy zsh
0 ports
of the container face the internet
14 days
in which no one, you included, can delete a backup

The files: nickypro/.vps-dotfiles › agent-box. Its README has every detail; this page is the tour.

01

How it fits together

Your devices only talk to the server. It passes traffic to the container over its loopback address (127.0.0.1), so the container adds no new doors to the internet. Backups leave from the server, never the container.

Your devices
Terminal, over SSH Browser or the T3 app
The server (“host”)
sshd keys only Tailscale or nginx Network guard fail2ban Your files /srv/agent-box Backup job + its keys
127.0.0.1 only: port 2222 (SSH) and 3773 (T3)
The agent box (container)
user dev, with sudo 5 GB · 3.5 CPUs · 4096 processes no Docker socket
Backblaze B2
Locked 14 days Kopia snapshots

Your home folder, data and logins live on the host, so make rebuild can replace the container without losing anything.

02

What keeps it safe

Assume that sooner or later an agent follows an instruction it shouldn’t. The setup tries to make that boring.

An agent goes off the rails

Capped memory, CPU and processes, and no Docker socket (that would be root on the server). The network guard keeps it to the internet.

Strangers on the internet

No new open ports. SSH is keys only, through the server. T3 sits behind Tailscale, a certificate or a password, plus pairing, or behind SSH alone.

Losing files

Files live on the host. Hourly encrypted backups go to Backblaze, locked for 14 days, with the job and its keys out of the agents’ sight.

A lost laptop or phone

Delete its key from authorized_keys, revoke its certificate, or drop it from your tailnet.

The network guard

A small nftables firewall on the host, in its own table so Docker and ufw leave it alone. The internet works; an attacker’s usual next steps don’t.

Allowed

The internetpip, npm, GitHub, model APIs, web pages
The host’s web ports80 and 443, so agents can fetch your own sites
DNSnames resolve as usual, through Docker’s resolver
Replies to the hostyour SSH jump, and T3 behind Tailscale or nginx
the agent box

Blocked

The host’s other portsits sshd, databases, other apps
The cloud’s metadata service169.254.169.254, which on some clouds hands out credentials
Private networks10/8, 172.16/12, 192.168/16, and the IPv6 equivalents
Your Tailscale devices100.64/10: your laptop, your NAS, everything else on your tailnet

What it doesn’t protect

Every agent can read everything in the box: code, ~/.env, logins. Give it credentials you’d be fine losing: a GitHub token for just its repos, API keys with a spending limit. A container also shares the server’s kernel: use an isolated agent for work you trust less, and a VM for people you don’t.

03

Pick how you reach T3

T3’s web UI is a terminal with sudo, so who can reach it matters. Pick with ACCESS in config.mk; to switch, change it and rerun sudo make host-config.

tailscale

Recommended

Only your own devices, over Tailscale, a free private network between your machines.

Who can even see the loginOnly devices on your tailnet
You needThe Tailscale app on each device
Locks your tailnet + T3 pairing

ssh

Experimental ♥ Author’s pick

No web UI. T3’s app connects over the same SSH as ssh agent-box and pairs itself. Experimental, but the author’s favourite: the least to set up, and no web page to find.

Who can even see the loginNobody: there’s no web page, only SSH
You needThe T3 desktop app, or an unofficial Android build; nothing for iPhone yet
Locks your SSH key, at the server and again at the box

mtls

Client certificates

A public address like code.example.com, but the browser must show a certificate you issued (“mutual TLS”).

Who can even see the loginAnyone, but they get nowhere without your certificate (or the optional password)
You needA domain, and a certificate on each device
Locks certificate + T3 pairing

public

Last resort

A public address with a generated password, rate limited, with fail2ban banning anyone who keeps guessing.

Who can even see the loginAnyone on the internet
You needA domain; nothing on your devices
Locks one password + T3 pairing
04

Set it up, step by step

About an hour. The commands are in the dropdowns; this guide uses sudo make … throughout.

  1. 1

    Prepare the server on the server

    Ubuntu or Debian with 4 GB of RAM or more, and Docker Engine with its compose plugin. Then a few tools, a firewall, key-only SSH and some swap.

    Commands
    sudo apt install -y git make fail2ban nftables jq
    sudo ufw allow OpenSSH
    sudo ufw allow 80,443/tcp        # only for the mtls or public modes
    sudo ufw enable
    
    # keys only (check your key logs you in first)
    printf 'PasswordAuthentication no\nKbdInteractiveAuthentication no\n' \
      | sudo tee /etc/ssh/sshd_config.d/10-keys-only.conf
    sudo systemctl reload ssh
    
    # swap, so a busy agent has room
    sudo fallocate -l 8G /swapfile && sudo chmod 600 /swapfile
    sudo mkswap /swapfile && sudo swapon /swapfile
    echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
  2. 2

    Get the files on the server

    Clone the repo, make the storage and list your SSH keys. For a mode other than tailscale, set ACCESS in config.mk now (and DOMAIN, for mtls or public).

    Commands
    git clone https://github.com/nickypro/.vps-dotfiles
    cd .vps-dotfiles/agent-box
    sudo mkdir -p /srv/agent-box/{home,data,cells}
    sudo chown 1000:1000 /srv/agent-box/{home,data}   # uid 1000 = the dev user inside
    cp authorized_keys.example authorized_keys
    nano authorized_keys                        # your SSH public keys, one per line

    Memory and CPU caps are in docker-compose.yml; the defaults suit a 4-core, 8 GB server.

  3. 3

    Build it and fence it in on the server

    About 10 minutes. Then net-test should show the cloud metadata service and the host’s SSH blocked, and the internet and DNS open.

    Commands
    sudo make rebuild       # build the image and start the container
    sudo make guard-link    # the firewall that keeps the container off the host
    sudo make net-test      # what the container can and can't reach
    sudo make doctor        # checks everything that has broken before
  4. 4

    Turn on the web UI (or don’t) on the server

    Open the one for your mode.

    tailscale recommended
    curl -fsSL https://tailscale.com/install.sh | sh
    sudo tailscale up
    sudo make host-config   # runs tailscale serve: T3 on https, your tailnet only
    • In Tailscale’s admin console, turn on MagicDNS and HTTPS Certificates (DNS page), and disable key expiry for this machine.
    • Machine names show up in public certificate logs, so pick a dull one.
    • Never tailscale funnel: it publishes to the whole internet.
    • If other people share your tailnet, add an Access controls rule so only you reach this machine.
    ssh no web UI, experimental

    Set ACCESS = ssh. Nothing to install; this only removes what another mode left serving T3. Skip make pair: the app pairs itself in step 7.

    sudo make host-config   # no nginx site, no tailscale serve
    mtls client certificates

    Set ACCESS = mtls and DOMAIN in config.mk, put the domain in nginx/agent-box-mtls.conf, and point its DNS at the server.

    sudo apt install -y nginx certbot python3-certbot-nginx
    sudo certbot certonly --nginx -d code.example.com
    sudo make ca                         # your own certificate authority, once
    sudo make cert NAME_ARG=laptop       # makes laptop.p12 and prints its password
    sudo make web-passwd USER_ARG=me     # optional: a password for devices without a cert
    sudo make host-config

    Import the .p12 on each device, then delete the server’s copies. Keep the CA key (/etc/nginx/agent-box/ca.key) in exactly one place.

    public password only

    Set ACCESS = public and DOMAIN, put the domain in nginx/agent-box-public.conf, and point DNS at the server.

    sudo apt install -y nginx certbot python3-certbot-nginx
    sudo certbot certonly --nginx -d code.example.com
    sudo make web-passwd USER_ARG=me     # prints a generated password
    sudo make host-config
  5. 5

    Connect from your laptop on your laptop

    Add two hosts to ~/.ssh/config, then ssh agent-box. The box only listens on the server’s loopback, so you jump through the server.

    The config
    Host myvps
      HostName your.server.address
      User you
    
    Host agent-box
      HostName 127.0.0.1
      Port 2222
      User dev
      HostKeyAlias agent-box
      ProxyJump myvps
      ForwardAgent no
    • HostKeyAlias files the box’s key under its own name, so it doesn’t clash with anything else on port 2222.
    • ForwardAgent no: every agent has sudo, so a forwarded agent would let any of them log in as you elsewhere. The box refuses it too.
  6. 6

    Log the agents in in the box

    Log in to claude, codex and gh once; logins survive rebuilds. For GitHub, prefer a fine-grained token for just the agents’ repos. API keys go in with make secret.

    Commands, and more Claude accounts
    ssh agent-box
    claude          # log in to Claude Code
    codex login
    gh auth login   # or: gh auth login --with-token, with a fine-grained token
    
    sudo make claude-account ACCOUNT=work            # on the server: a second Claude login
    sudo make claude-account ACCOUNT=main LABEL=Me   # give your first one a name too

    Each extra account gets its own name and colour in T3, its own login and history, and a claude-work command. They share your notes and skills in ~/.claude, and every agent starts with box-notes.md, which explains the box to it.

  7. 7

    Open T3 any device

    In a browser (tailscale, mtls or public): sudo make pair LABEL=phone prints a pairing link, good for 30 minutes, to open on that device.

    In the T3 desktop app (any mode): add agent-box as an SSH connection. It finds the running server and pairs itself, and every device sees the same threads.

    On Android (experimental): an unofficial T3 build with SSH, by the box’s author and proposed upstream. On an iPhone there’s no SSH app yet, so use a browser mode.

    Setting up the Android app

    It makes its own key (Settings › SSH). Add that to authorized_keys next to the Makefile, and to ~/.ssh/authorized_keys on the server, limited to the jump:

    restrict,port-forwarding,permitopen="127.0.0.1:2222",command="/bin/false" ssh-ed25519 AAAA… t3code-android

    Paste the two Host blocks from step 5 into the app, then Add environment › Or connect over SSH › agent-box. Check the host keys it shows with ssh-keygen -lf on the server: /etc/ssh/ssh_host_ed25519_key.pub, then ssh_host_keys/ssh_host_ed25519_key.pub next to the Makefile.

  8. 8

    Turn on backups Backblaze + server

    Optional: about 20 minutes, mostly in Backblaze’s web UI. How it works.

    The bucket in Backblaze’s web UI
    1. Sign up for B2. The signup form is an iframe that ad blockers can hide; if the page looks empty, turn the blocker off for it.
    2. Create a bucket: Private, with Object Lock enabled. No default retention; Kopia sets it on each file.
    3. Lifecycle settings, custom rule: {"fileNamePrefix":"","daysFromHidingToDeleting":15}. Pruned files are only hidden; this deletes them 15 days later so they stop costing money.
    4. App keys: add one for this bucket only, with read and write access. Note the keyID and applicationKey (shown once) and the bucket’s endpoint, s3.<region>.backblazeb2.com.
    The repository as root on the server

    Install kopia, then in a root shell (sudo -i):

    mkdir -p /root/backup && chmod 700 /root/backup
    cat > /root/backup/b2.env <<'EOF'
    B2_KEY_ID=<keyID>
    B2_APP_KEY=<applicationKey>
    KOPIA_PASSWORD=<long and random: openssl rand -hex 32>
    # Optional alerts, see below:
    HC_URL=https://hc-ping.com/<uuid>
    EOF
    chmod 600 /root/backup/b2.env
    
    set -a; . /root/backup/b2.env; set +a
    export AWS_ACCESS_KEY_ID="$B2_KEY_ID" AWS_SECRET_ACCESS_KEY="$B2_APP_KEY"
    kopia repository create s3 --bucket=<bucket> --endpoint=s3.<region>.backblazeb2.com --region=<region> \
      --retention-mode=COMPLIANCE --retention-period=14d --description="agent-box backup"
    kopia maintenance set --extend-object-locks true
    kopia policy set --global --keep-latest 10 --keep-hourly 48 --keep-daily 30 --keep-weekly 8 --keep-monthly 12 --keep-annual 2 \
      --compression zstd-fastest --add-ignore ".cache/" --add-ignore "node_modules/" --add-ignore "__pycache__/" --add-ignore ".Trash*/"
    kopia policy set /root --add-ignore "/backup/" --add-ignore "/.config/kopia/"
    kopia policy set "root@$(hostname):/agent-box-container" \
      --add-ignore "/tmp/" --add-ignore "/var/tmp/" --add-ignore "/var/cache/" --add-ignore "/root/.npm/"
    kopia policy set /etc --add-ignore "/nginx/agent-box/"     # mtls: keep the CA key single-copy
    
    cd /path/to/.vps-dotfiles/agent-box
    make backup-link     # script + systemd units, hourly timer on
    make backup-now      # the first run can take hours

    Put the repository password in your password manager now. It’s the only way to decrypt the backups, and b2.env isn’t backed up.

    For HC_URL, a free healthchecks.io check (1-hour period, a few hours’ grace) emails you when backups fail or stop, or a disk passes 90%.

05

Isolated agents

Every agent in the box is you: dev, with sudo, your logins and every project. For work you trust less (a stranger’s repo, a scraper, someone else’s project), make an isolated agent, a “cell”: its own Linux user and folder, as two more providers in the same T3.

One T3, one connectionPick the provider for each thread
MEClaude MECodex CWCell web CWCell web Codex
Your agents user dev
sudo every file in the box your logins + ~/.env T3’s own tools
a different
Linux user
Cell “web” user cell-web
no sudo only /cells/web, plus folders you share its own logins + keys the internet; local ports 20000–29999

Make one

1Create it

On the server: sudo make cell CELL=web. That adds the user, its folders and its two providers in T3.

2Give it a login

Its own, never yours: cell web claude auth login inside the box, or a limited API key with sudo make secret KEY=ANTHROPIC_API_KEY CELL=web.

3Use it in T3

Add the project /cells/web/work and pick Cell web for its threads. Your own providers refuse to start there, on purpose.

Sharing folders, and the other cell commands

A shared folder appears at /cells/web/shared/; the cell sees it and nothing around it.

sudo make cell-share CELL=web DIR=/data/papers        # read-only
sudo make cell-share CELL=web DIR=/data/out RW=1      # read-write
sudo make cell-unshare CELL=web DIR=papers
sudo make cells                                       # cells, their logins, what's shared
cell web import ~/projects/thing                      # inside the box: give it its own copy
cell web                                              # inside the box: a shell as the cell
What a cell can and can’t do
Your agentsA cell
Fileseverything in the boxits own home, work folder and shared folders, and world-readable system files. It can’t list /tmp, where your agents leave files
sudoyesno
Your logins, ~/.env, T3’s settingsyesno; it has its own
T3’s toolsyesno: it could ask an unisolated agent to do the work
Networkthe internet (the guard still applies)the internet, but nothing listening in the box except its own ports 20000–29999
Other users’ processessees them allsees only its own, so it can’t read your commands’ arguments
The traps it closes

Another user is the easy part. The work is in the places where your account would still run something the cell wrote:

T3’s own tools

With them an agent can start threads on your unisolated providers. The guard blocks cells from every port in the box, and cells won’t start without it.

Git

A repo can make git run programs, and T3 runs git constantly. In a cell’s folder git runs as the cell; failing that, git refuses it as “dubious ownership”.

Your agents in its folder

A project can run commands as an agent starts (Claude’s hooks), so your own Claude and Codex refuse to start in /cells.

You

T3’s terminal in a cell’s folder is still your shell, and the prompt says so. cell web gives you the cell’s.

What it doesn’t do

It’s the same container: same kernel, memory cap and internet. Cells can reach each other on ports 20000–29999, and T3 can’t make worktrees for their projects (use Local). Claude’s and Codex’s own sandboxes don’t work in Docker; cells do that job here.

06

A real browser, a desktop, Synthesizer V, keys

Optional, baked into the image: set EXTRAS = desktop wine scraping (or some of them) in config.mk, then sudo make rebuild. All three add about 4 GB.

localhost:6080/desktop.html $ desktop browser agents connect at 127.0.0.1:9222 ♫ sound live

A desktop in your browser EXTRAS=desktop

For sites that block headless browsers, logins, or watching what an agent does: a private desktop with sound, in your own browser. Agents drive its Chromium; you see every click.

How to use it
desktop start          # inside the box: prints a link to open
desktop browser        # Chromium on it, for agents (port 9222)
claude mcp add browser -s user -- npx -y @playwright/mcp@latest --cdp-endpoint http://127.0.0.1:9222
desktop stop           # gives the memory back
  • Private. Its one port is on 127.0.0.1, reached through SSH, and only opens with the two random tokens in the link.
  • Plain browsing needs no desktop: claude mcp add playwright -s user -- npx -y @playwright/mcp@latest --headless --no-sandbox --executable-path /usr/bin/chromium. Cells can too.
  • Not a VM, because most VPSes lack /dev/kvm. Chromium runs without its own sandbox here, so don’t log in to anything you wouldn’t hand the agents.
Synthesizer V Studio 2 Kasane Teto AI 2 kasaneteto

Synthesizer V Studio 2 and Kasane Teto EXTRAS="desktop wine"

Synthesizer V 2 has no Linux version, so it runs under Wine on the desktop above, following mito551/synthv2-linux. Agents write the song (MIDI, or an .svp project, which is JSON) and click Render.

Setup and caveats
/opt/agent-box/extras/synthv/setup   # ~10 minutes, ~2.5 GB
synthv                               # starts it on the desktop
  • Log in once. If the dialog then spins forever, press Escape; under Wine it doesn’t close itself.
  • One device at a time. The licence moves to whichever machine logged in last.
  • Teto’s 7-day trial sings but can’t export, so you listen through the desktop’s sound.

Scraping tools EXTRAS=scraping

In /opt/scrape (Python at /opt/scrape/bin/python), command-line tools on PATH: curl_cffi, trafilatura, yt-dlp, Camoufox, Playwright and markitdown.

What each one does
curl_cffi

Plain HTTP that looks like a real Chrome to anti-bot checks (impersonate="chrome").

trafilatura -u URL

A page’s main text, without menus, ads or cookie banners.

yt-dlp

Video, audio and subtitles from most sites, with ffmpeg.

camoufox fetch

Camoufox: Firefox built to pass bot detection. Run fetch once per user.

playwright

Browser automation from Python; connect_over_cdp drives the desktop’s browser.

markitdown

PDFs, Word files and slides turned into Markdown for the agents.

Many sites block data-centre addresses whatever the browser. For a home connection, use an SSH SOCKS proxy through a machine at home: ssh -N -D 127.0.0.1:1080 home, then --proxy socks5://127.0.0.1:1080.

camofox-browser, a stealth browser for agents

camofox-browser is a Camoufox server for agents: each page comes back as a tidy outline with stable references to click, through its own MCP. By default it listens on every interface and sends crash reports; these settings stop both and require a key:

echo "CAMOFOX_ACCESS_KEY=$(openssl rand -hex 16)" >> ~/.env   # the key, kept for restarts
tmux new -d -s camofox 'set -a; . ~/.env; CAMOFOX_BIND_HOST=127.0.0.1 CAMOFOX_CRASH_REPORT_ENABLED=false npx -y @askjo/camofox-browser'
. ~/.env && claude mcp add camofox -s user -e CAMOFOX_BASE_URL=http://127.0.0.1:9377 \
  -e CAMOFOX_ACCESS_KEY="$CAMOFOX_ACCESS_KEY" -- npx -y @askjo/camofox-browser-mcp

API keys

One command asks for a key without showing it, so it stays out of your shell history and process list:

For your agents

sudo make secret KEY=OPENAI_API_KEY

Saved in ~/.env, read by your shells and by the agents T3 starts (after sudo make t3-restart).

For one cell

sudo make secret KEY=OPENAI_API_KEY CELL=web

In that cell’s own ~/.env. Cells never see yours.

Anything an agent can use, it can read, and a web page can talk it into leaking it. Use separate keys for the box and each cell, each with a spending limit.

07

Day to day

sudo make status

Container, T3, web access and fail2ban at a glance.

sudo make doctor

Checks everything that has broken before, and says how to fix it.

sudo make update

Latest Claude Code, Codex and T3, installed in place, then T3 restarts. Quick.

sudo make rebuild

Fresh base image (security fixes) and latest CLIs, then a new container. Nothing in your home folder or /data is lost.

More commands: T3, devices, keys, accounts, cells, backups
sudo make t3-restart

Restarts just T3. SSH sessions stay up.

sudo make pair LABEL=tablet

A pairing link for one more device.

sudo make cert NAME_ARG=tablet

mtls: a client certificate for a new device.

sudo make cert-revoke NAME_ARG=tablet

mtls: refuse that certificate from now on. make client-cert-list shows which are revoked.

sudo make backup-status

Is a backup running, when’s the next, the latest snapshots.

sudo make net-test

What the container can reach, and what a cell can. Run it after any firewall change.

sudo make secret KEY=NAME

Save an API key for the agents, typed hidden. Add CELL=web for one cell’s own.

sudo make claude-account ACCOUNT=work

One more Claude login, as its own provider in T3. make claude-accounts shows who’s logged in where.

sudo make cell CELL=web

A new isolated agent. make cells lists them, make cell-rm removes one (its files stay).

sudo make cell-share CELL=web DIR=…

Show a cell one of your folders, read-only unless RW=1. cell-unshare takes it back.

To give or take away SSH access, edit authorized_keys next to the Makefile; no rebuild needed. make help lists everything.

08

Backups, in plain words

Every hour

The host snapshots your files, its settings, and whatever was installed inside the container. Caches and node_modules are skipped.

Kopia

Splits files into chunks, stores each chunk once, and encrypts everything. An hourly run only uploads what changed.

Backblaze B2

Object Lock in compliance mode: no one can delete a file until its 14-day lock runs out. Kopia keeps extending the locks on files still in use.

How far back you can go

Snapshots kept at any moment (each mark is one), on a stretched time scale. Every snapshot is complete on its own.

Show as a table
KeptHow manyReaches back
Latest1010 hours
Hourly482 days
Daily3030 days
Weekly88 weeks
Monthly1212 months
Yearly22 years

What if someone takes over the server and deletes the backups?

Day 0
They delete everything. B2 only hides locked files.
Hours later
healthchecks.io emails you: the hourly backups stopped arriving.
Up to day 14
Unhide the files in B2’s web UI (“Show all versions”) and restore.
After ~2 weeks
The locks run out and B2’s cleanup rule removes them for good.

That’s why the alert matters: the lock buys you two weeks, but only if you notice. The job and its keys live on the host, so agents in the box can’t do this in the first place.

Getting files back
sudo -i
kopia snapshot list --all                        # find a snapshot ID
kopia ls -l <snapshot-id>/home/.claude          # browse it
kopia restore <snapshot-id>/home/some/folder /tmp/restored
kopia mount <snapshot-id> /mnt/kopia &           # or browse it as a folder

On a new server: install kopia, recreate b2.env with a new B2 key and the same repository password, kopia repository connect s3 to the same bucket, then kopia maintenance set --owner=me so the locks keep being extended. The README has the exact commands.

09

When things go wrong

The web UI shows “502 Bad Gateway”

T3 isn’t listening where Docker can reach it. The usual cause: the T3 server in tmux died and the T3 desktop app started its own private one in its place. Run sudo make doctor, then sudo make t3-restart.

Agents started from T3’s app don’t see my API keys

The T3 server in tmux had died, so the app started a server of its own. Your threads are all there, but that server doesn’t load ~/.env or the isolated agents’ guards. sudo make doctor spots it; sudo make t3-restart puts the box’s own server back.

The web UI loads but stays empty

T3 runs over a WebSocket. If you wrote your own nginx config, copy the Upgrade/Connection lines from the ones in nginx/.

The build fails at the npm step, or T3’s terminals don’t open

Since version 0.1, the t3 npm package only downloads T3’s real program into ~/.t3/runtime the first time it runs, terminals included. An older Dockerfile that checked for T3’s node-pty folder now fails to build: pull the latest files. If terminals still don’t open, sudo make doctor checks T3’s runtime; sudo make t3-restart fetches it again.

A cell says “the network guard isn’t on”

Cells refuse to start without their firewall rules, because without them a cell could reach T3’s own tools. Run sudo systemctl restart agent-box-guard (the first time: sudo make guard-link), then sudo make net-test.

“This folder belongs to the isolated agent”

You picked one of your own providers in a cell’s project, and it refused, as it should. Pick the cell’s provider (“Cell web”) for that thread, or in a shell run cell web claude.

A cell’s dev server can’t be reached

Cells can only use local ports 20000–29999 (every other port in the box is blocked for them), so npm run dev -- --port 20000. The note in each cell’s own instructions file tells its agents the same.

ssh agent-box says “Permission denied (publickey)”

Your key isn’t in authorized_keys next to the Makefile. That’s the only place the box reads keys from; ~/.ssh/authorized_keys inside it is ignored on purpose.

SSH warns “REMOTE HOST IDENTIFICATION HAS CHANGED”

It shouldn’t, even after a rebuild, because the container’s host keys are kept in ssh_host_keys/. If you see it, find out why (was that folder deleted?) before you accept the new key.

make net-test says DNS is BROKEN

Your provider’s DNS server is in a range the guard blocks (some clouds put it at 169.254.169.253 or in 10.0.0.0/8). Add dns: [1.1.1.1, 9.9.9.9] to the service in docker-compose.yml and run sudo make up.

A device says the .p12 password is wrong

Some older devices can’t read OpenSSL 3’s newer encryption. Export the same .key and .crt again with openssl pkcs12 -export -legacy ….

The backup says “repository is not connected”

Kopia finds its settings through $HOME. The service file sets HOME=/root; for commands by hand, use a root login shell (sudo -i).

I closed port 22 and now I can’t get in

Use your provider’s web console. The README’s section on moving SSH onto Tailscale says to test both ssh myvps and ssh agent-box over the tailnet before closing the port.

10

What changed in October 2026

All 15 changes this month