My dev environment: a server with no public ports
How the server I develop on is set up: reachable only over a private network, SSH and terminal sessions without open ports, updates on autopilot, and a prompt you can copy to set up the same thing step by step.
Request an AI summary
I don’t develop on my laptop. I work on a small cloud server running Ubuntu, and my laptop, tablet and phone are only windows into it. My projects, terminals and running programs stay on the server, so I can close the laptop in one place and carry on from another device exactly where I stopped.
A server like that holds code, access tokens and the keys to deploy. So it’s set up around one idea: nobody on the open internet can even see it. Here is how, layer by layer. At the end there’s a prompt you can copy to have an assistant guide you through the same setup on your own server.
1. The network: private only
The server joins a private network built with Tailscale. Every device I work from is in that network, and they all reach the server through an encrypted connection, wherever they are. To everyone else, the server has nothing to talk to.
- The firewall (ufw) refuses everything that comes in from the internet and only lets traffic in that arrives over the private network.
- The hosting provider’s firewall in front of the server has no inbound rules either, so even a mistake on the server itself wouldn’t open a door.
- No public ports at all. No SSH, no web server, nothing listening on the public address. A port that doesn’t exist can’t be attacked, no matter which security hole turns up next.
- Only devices in active use stay in the private network, because every device in it can reach the server.
2. SSH: keys only, private address only
SSH is still there for the moments it’s needed, but it’s locked down:
- It listens only on the private network address, not on the public one.
- Passwords are off. Only keys get in, and root can’t log in at all.
- X11 and TCP forwarding are off, so a stolen login can’t be turned into a tunnel to somewhere else.
- On the Tailscale side, SSH asks for a fresh login confirmation regularly.
3. Terminals: sessions that keep running
For day-to-day work I don’t use plain SSH, but Rex, a terminal multiplexer. It keeps my sessions, tabs and panes on the server. When I connect from another device, everything is still there: the build that was running, the open editor, the scroll history.
Rex runs in login server mode: a small service on the server checks who is connecting and starts a session as that person’s own Linux account. It, too, is only reachable through the private network, via Tailscale Serve, and only my own Tailscale identity is allowed in.
Why a multiplexer is the heart of this setup, and how it lets me work from an iPhone and an iPad, is in Terminal multiplexers.
4. Updates: always the latest stable version
A development server is exactly where old versions shouldn’t pile up:
- Security updates install themselves every day (unattended-upgrades).
- Everything else stays on the latest stable release: the system packages, Node.js and npm, Bun, and the terminal server, each from its official package source.
- After updates, I check which programs are still running the old version and restart them. An update only protects you once the new code is actually running.
- Features nobody uses are off. One example is Multipath TCP, a kernel feature for devices with several connections at once, like a phone on Wi-Fi and mobile data. A server in a data centre has one connection, so it gains nothing from it. Turning it off removes code that could one day have a hole in it.
5. GitHub and deploys: narrow access
The server talks to GitHub with a fine-grained token that only reaches the repositories I work on there. Git runs over HTTPS with that token, not with a broad key that opens everything.
Deploying doesn’t happen from the server at all: Cloudflare is connected to the GitHub repository and publishes this site whenever something lands on main. So the server never needs credentials for the live site.
6. Set it up yourself: a prompt to copy
You can have an AI assistant with access to your server’s terminal guide you through the same setup. Copy this prompt, fill in what’s in square brackets, and paste it in. It explains every step before running it and doesn’t lock you out along the way.
You are helping me set up a secure development server. I'm logged in
over SSH to a fresh Ubuntu server (version: [e.g. 24.04]) at a cloud
provider ([provider name]). My Linux user is [username] and has sudo.
Goal: a server with NO public ports. I reach it only through a private
Tailscale network, from these devices: [list your laptop/phone/tablet].
Work through the steps below one at a time. For each step:
- explain in plain language what it does and why,
- show me the exact commands before running them,
- wait for my OK,
- check afterwards that it worked.
Never do anything that could lock me out before we've confirmed that
access over Tailscale works. If a step fails, stop and explain.
1. Update the system and enable automatic security updates
(unattended-upgrades).
2. Install Tailscale, join my tailnet, and confirm I can reach the
server over its Tailscale address from another device.
3. Harden SSH: listen only on the Tailscale addresses, keys only,
no passwords, no root login, no X11 and no TCP forwarding.
Keep my current session open while testing a second login.
4. Set up the firewall (ufw): deny all incoming traffic by default,
allow everything on the tailscale0 interface. Then show me that
nothing listens on the public address (ss -tulpn).
5. Tell me how to set my cloud provider's firewall to no inbound rules.
6. Optionally enable Tailscale SSH with check mode.
7. Install the Rex terminal multiplexer so my sessions survive
disconnects: Rex in login server mode, reachable only via
Tailscale Serve, with only my own Tailscale identity allowed in.
8. Install my dev tools from their official sources on the latest
stable version: [e.g. git, Node.js, Bun, GitHub CLI].
9. Set up GitHub access with a fine-grained token limited to the
repositories I need, using git over HTTPS.
10. Turn off kernel features a server doesn't need, starting with
Multipath TCP (net.mptcp.enabled = 0, saved in /etc/sysctl.d/).
11. Finish with a short checklist of what's set up, and the commands
I can run later to check that everything is still up to date.
Every setup is a little different: another provider, other tools, more people who need access. If you’d like help setting up a development environment like this for your team, get in touch.
