Why cloud-init when you have hands
Manual SSH setup works right up until the second server. cloud-init runs your configuration on the first boot of a VPS — users, keys, packages — and the same file will bring up an identical server a year from now. Every major provider has a “user data” field right in the server creation form.
The minimal config: a dedicated deploy user with a key instead of a password, root login disabled, fresh packages.
#cloud-config
users:
- name: deploy
groups: [sudo, docker]
ssh_authorized_keys:
- ssh-ed25519 AAAA… you@laptop
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_pwauth: false
disable_root: true
package_update: true
package_upgrade: true
Docker: the official repo and log limits
Install Docker from the official repository, not from distro packages — those lag six months behind. And cap the logs immediately: a container writing to stdout with no limit will eat the whole disk in a couple of months. It is the single most common cause of “the server suddenly died” we get called about.
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
systemctl restart docker — already-running containers keep the old log driver until they are recreated.Firewall: three ports, no more
Only 22, 80 and 443 face the internet. Everything else — databases, metrics, admin panels — is reached over an SSH tunnel or VPN. The important nuance: Docker writes rules straight into iptables bypassing ufw, so a port published with -p 5432:5432 is open to the whole internet no matter what ufw says. Publish ports on 127.0.0.1 only.
$ ufw default deny incoming
$ ufw allow 22/tcp comment 'ssh'
$ ufw allow 80,443/tcp comment 'web'
$ ufw enable
# expose service ports on loopback only:
ports: ["127.0.0.1:5432:5432"]
Pre-production checklist
The server is ready when you can answer “yes” to every item — and point to where it lives in code:
No time to do it yourself?
We'll set up a Docker-ready VPS turnkey — from ~$95, with a cloud-init config that stays with you.