Credit where it’s due: this setup is the work of Tobias Dosdal-Feddersen, who described it in a LinkedIn post. All the good ideas here are his. I’ve only expanded his short description into a step-by-step guide so others (including me) can reproduce it. Go read and follow him.
What he built
Tobias moved his entire development environment off his laptop and onto a server, where everything runs in Docker. The setup has four parts:
- Docker holds the whole environment. The setup lives in a single Git repo, so it’s identical on every device.
- T3 Code runs his AI coding agents inside the container. They keep working when the laptop lid is closed, and he can follow along and steer them from both his Mac and his phone.
- Tailscale connects his devices to the server. The server has no ports open to the internet; only his own devices can reach it.
- Proton Pass stores every secret in one place. They are fetched with a single command, so there are no
.envfiles in Git.
The result, in his words (translated from Danish): no hassle with differing dev environments, the same environment across devices, and the whole setup in one repo.
Below is how to build the same thing. His post doesn’t include his exact files, so the configuration here is my reconstruction based on each tool’s documentation. Treat it as a working starting point, not a copy of his repo.
How the pieces fit together
Mac / phone ──(Tailscale, encrypted)──▶ server
│
tailscale serve :443
│
127.0.0.1:3773
│
┌─────────▼─────────┐
│ Docker container │
│ t3 serve │
│ Claude Code/Codex │
│ pass-cli │
│ /workspace (code) │
└────────────────────┘
The key idea: in T3 Code, the machine running the server owns everything. Agents, terminals, Git and files all run there. Your Mac and phone are just windows into it. Close the laptop and the agents carry on.
What you need
- A Linux server you control: a box at home or a small VPS, with Docker and Docker Compose installed.
- A free Tailscale account.
- A Proton Pass plan that includes the CLI (Pass Plus, Pass Family, Pass Professional or a Proton bundle).
- A subscription for at least one coding agent that T3 Code supports, such as Claude Code or Codex.
Step 1: Put the server on your tailnet and close everything else
Install Tailscale on the server and log in:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh
--ssh lets you SSH in over Tailscale, so you don’t need port 22 open either. Install Tailscale on your Mac and phone as well, and log in with the same account.
Now close the public side. With ufw:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0
sudo ufw enable
On a VPS, also block all inbound traffic in the provider’s firewall. Tailscale only needs outbound connections.
Watch out: Docker’s published ports bypass ufw. Never publish a container port as 3773:3773; that binds to all interfaces and exposes it to the internet. Always bind to 127.0.0.1, as in the compose file below.
Finally, in the Tailscale admin console under DNS, make sure MagicDNS and HTTPS certificates are enabled. You’ll need them in step 5.
Step 2: Create the repo
The whole environment lives in one repo:
devenv/
├── Dockerfile
├── compose.yaml
├── .env.template # secret references only, safe to commit
├── .gitignore
└── README.md
Dockerfile:
FROM node:24-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends git curl ca-certificates openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Coding agent(s) and T3 Code
RUN npm install -g @anthropic-ai/claude-code t3
# Proton Pass CLI (the installer puts it in ~/.local/bin; move it somewhere global)
RUN curl -fsSL https://proton.me/download/pass-cli/install.sh | bash \
&& mv /root/.local/bin/pass-cli /usr/local/bin/pass-cli
# Work as a normal user, not root
RUN useradd -m -s /bin/bash dev && mkdir /workspace && chown dev:dev /workspace
USER dev
WORKDIR /workspace
EXPOSE 3773
CMD ["t3", "serve", "--host", "0.0.0.0"]
compose.yaml:
services:
dev:
build: .
restart: unless-stopped
ports:
- "127.0.0.1:3773:3773" # localhost only, never 0.0.0.0
volumes:
- workspace:/workspace # your code
- home:/home/dev # agent logins, T3 state, pass-cli session
volumes:
workspace:
home:
The two named volumes matter. They keep your code and your logins across rebuilds, so you can update the image without logging in to everything again.
Inside the container, t3 serve listens on all of the container’s interfaces. That’s fine, because on the host the port is only bound to 127.0.0.1.
.gitignore:
.env
.env.*
!.env.template
Start it:
docker compose up -d --build
Step 3: Log in to your agent and Proton Pass, once
Open a shell in the container and log in. Thanks to the home volume, this only has to happen once.
docker compose exec dev bash
claude auth login # or: codex login
pass-cli login
pass-cli vault list # check that it works
T3 Code uses each agent’s own CLI login, so you only pay for the subscriptions you already have.
Step 4: Move your secrets into Proton Pass
This is how you get rid of .env files. Store each secret as an item in Proton Pass, for example a vault called Dev with an item myapp. Then commit a template that contains only references:
# .env.template — safe to commit
DATABASE_URL=pass://Dev/myapp/database_url
STRIPE_SECRET_KEY=pass://Dev/myapp/stripe_secret_key
OPENAI_API_KEY=pass://Dev/myapp/openai_api_key
Now the single command:
pass-cli run --env-file .env.template -- npm run dev
pass-cli run resolves the pass://vault/item/field references and hands the real values to your process as environment variables. Nothing is written to disk.
If a tool insists on reading an actual file, use inject instead. Note that it needs double braces around the references:
# config.template
DATABASE_URL={{ pass://Dev/myapp/database_url }}
pass-cli inject --in-file config.template --out-file .env
The generated .env is covered by .gitignore, and you can regenerate it at any time.
Step 5: Publish T3 Code on your tailnet
On the server (not in the container), tell Tailscale Serve to proxy HTTPS to the container:
sudo tailscale serve --bg 3773
T3 Code is now reachable at https://<server-name>.<your-tailnet>.ts.net/, with a valid certificate, from your own devices only. From the public internet there’s nothing to see.
Step 6: Pair your Mac and phone
When t3 serve starts, it prints a one-time pairing token. Find it in the logs:
docker compose logs dev
On the Mac, install the desktop app (brew install --cask t3-code), open Settings → Connections, add an environment, and enter your https://…ts.net address and the token.
On the phone, make sure the Tailscale app is connected, then use the T3 mobile app or open https://app.t3.codes in the browser and pair the same way. The hosted web app works here because the endpoint is HTTPS.
The token is only used once. Each device then gets its own session. To pair more devices later, or revoke old sessions, use:
docker compose exec dev t3 auth --help
Treat pairing tokens like passwords.
Step 7: Add a project and let the agents work
Clone your code into the workspace:
docker compose exec dev git clone git@github.com:you/myapp.git /workspace/myapp
In T3 Code, open the Command Palette, choose Add Project, and pick the server environment. Start a thread, give the agent a task, close the laptop. Check in later from your phone.
Step 8: Check that nothing is exposed
On the server, list listening ports:
sudo ss -tlnp
Port 3773 should only appear on 127.0.0.1. Then, from a machine that is not on your tailnet, scan the server’s public IP (for example with nmap). Nothing should answer.
Why this is a nice setup
- Agents aren’t tied to your laptop. Long tasks keep running while you sleep or commute.
- One environment everywhere. Mac, phone or a borrowed machine all see the same container, the same tools and the same state.
- Small attack surface. No public ports. Access requires being on your tailnet and holding a paired session.
- No secrets in Git. The repo only contains references; the values stay encrypted in Proton Pass.
- Reproducible. The environment is a repo. Rebuild it, move it to a new server, or share it.
A caveat: T3 Code is young and moves fast, so check t3 serve --help and the project’s remote access docs if a flag has changed.
Credit
Once more: this is Tobias Dosdal-Feddersen‘s setup, and his original post is here. If you found this useful, go give his post a like.