A secure self-hosted server is not created by one command. It is maintained through layered controls, documented checks, and regular review. This self-hosted server hardening checklist walks through a fresh Ubuntu server, SSH access, firewall rules, unattended updates, Docker exposure, secrets, logging, backups, and recurring verification so you can reduce avoidable risk without losing track of how the system works.
Overview
Use this guide for a VPS, home server, or small homelab host that runs Docker containers and web applications. The examples assume Ubuntu with administrative access and a network path to the server. Adapt package names and service commands if you use another distribution.
Hardening is a process of reducing unnecessary access and limiting the damage caused by mistakes or compromised components. It does not make a server invulnerable. A practical baseline has several layers:
- A separate administrative user with carefully controlled privileges.
- SSH keys instead of routine password authentication.
- A default-deny firewall with only required services exposed.
- Prompt operating-system and application updates.
- Containers that expose only the ports they need.
- Protected secrets, tested backups, and useful security logs.
- A repeatable review after deployments, configuration changes, and incidents.
Before changing SSH or firewall settings, open a second SSH session and keep the first session available. Confirm that the new configuration works before closing either session. This simple precaution prevents many avoidable lockouts.
If you are still planning the host, the deployment workflow in Self-Hosting on a VPS: A Complete Docker Deployment Guide provides useful context for choosing a layout before applying these controls.
What to track
Identity and SSH access
Create a named administrator rather than using the root account for routine work. Add the user to the administrative group, install a public key, and verify key-based login from a separate terminal:
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo install -m 600 -o deploy -g deploy authorized_keys /home/deploy/.ssh/authorized_keys
ssh deploy@SERVER_IP
The final command assumes you have prepared an authorized_keys file. Do not disable password authentication until key login has been confirmed. Once you have a tested recovery path, review /etc/ssh/sshd_config or an included configuration file and consider settings such as PermitRootLogin no and PasswordAuthentication no. Validate before reloading:
sudo sshd -t
sudo systemctl reload ssh
Track who has SSH access, which keys are active, when keys were last reviewed, and whether any key belongs to a former user or device. Remove unused keys rather than allowing access to accumulate.
Firewall and exposed services
Start with an inventory. A typical web server may need SSH, HTTP, and HTTPS, while database ports, Docker APIs, dashboards, and internal application ports should usually remain private. With UFW, a basic policy might look like:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Do not copy these rules blindly if SSH uses a custom port, access is limited to a private network, or a provider requires another management path. Record every allowed port and its owner. Check listening services with:
sudo ss -tulpn
sudo systemctl --type=service --state=running
Docker deserves special attention: published container ports can be reachable in ways that do not match a simple UFW review, depending on the host's networking and Docker configuration. Prefer binding internal services to a private interface or to loopback, then expose public traffic through one maintained reverse proxy. Review the Docker firewall path and host rules rather than assuming an application is private because UFW is enabled.
Updates, containers, and privileges
Install security updates regularly and configure unattended security updates where that fits your change-management process. Review pending packages rather than treating automatic updates as a replacement for maintenance:
sudo apt update
apt list --upgradable
sudo unattended-upgrade --dry-run
Keep container images, compose files, and application dependencies under version control. Pin or constrain versions when reproducibility matters, and update deliberately after checking release notes and backup status. Avoid running containers as root when the application supports a non-root user. Do not mount the Docker socket into a container unless you understand that it provides highly privileged control over the host. Use read-only mounts where practical, drop unnecessary Linux capabilities, and avoid privileged containers.
For a multi-application server, separate networks and volumes by function. Keep databases off the public network, restrict administrative panels, and place secrets in a protected environment or secrets mechanism instead of committing them to Git or embedding them in published images.
Secrets, backups, and monitoring
Track API keys, database passwords, TLS private keys, recovery codes, and application encryption keys. Rotate credentials when a team member leaves, a device is lost, a secret appears in logs or source control, or a service is exposed unexpectedly. Restrict file permissions on environment files and back up secrets through a protected process, not by placing them in an unencrypted public repository.
A backup that has never been restored is only an assumption. Track backup completion, retention, off-host storage, encryption, and restore-test results. Include databases and important Docker volumes, not only compose files. The guide to Docker Backup Strategy for Self-Hosted Apps covers the distinction between backing up configuration and preserving usable application data.
Monitor disk usage, memory pressure, failed services, certificate expiry, authentication failures, backup status, and external uptime. Centralize logs when the environment justifies it, and keep enough history to investigate unusual activity. Fail2ban can help respond to repeated authentication attempts, but it is not a substitute for SSH keys, firewall rules, patching, or least privilege.
Cadence and checkpoints
A checklist is most useful when it has an owner and a schedule. Record the date, result, and follow-up action rather than marking a task complete without evidence.
After initial setup
- Confirm key-based SSH login from a clean client.
- Verify that root login and password authentication match your documented policy.
- List listening ports and compare them with the intended public surface.
- Test firewall behavior from an external network where possible.
- Check that databases and internal dashboards are not publicly reachable.
- Run a backup and perform a small restore test.
- Confirm logging, alert delivery, disk monitoring, and certificate renewal.
After every deployment or infrastructure change
Review the compose file or deployment manifest, image source, exposed ports, mounted paths, user identity, capabilities, environment variables, and network membership. Recheck reverse-proxy routing and TLS. If you use Traefik or another proxy, its routing configuration should be reviewed alongside the application rather than treated as an unrelated layer; see the Traefik Docker Compose Guide for a deployment-oriented reference.
Weekly or monthly
Review failed logins, service failures, backup results, disk growth, pending updates, unused users, stale SSH keys, and new public ports. Check the server from outside its local network if it is a home installation. Monthly is a reasonable default for a small, stable system; use a shorter interval for internet-facing or frequently changing infrastructure.
Quarterly
Perform a restore test, review administrator access, rotate appropriate credentials, remove abandoned containers and repositories, test recovery documentation, and reassess whether each public service still needs to be public. Keep a dated inventory so changes are visible over time.
How to interpret changes
Not every change is an incident, but every unexpected change deserves a question. A new listening port may be a legitimate deployment or an overlooked debug service. A spike in failed logins may be routine internet scanning, a misconfigured client, or a targeted attempt. The useful response is to correlate the event with deployment records, SSH logs, reverse-proxy logs, container activity, and provider notifications.
Prioritize findings by exposure and consequence. An internet-facing admin panel with a weak or shared credential deserves attention before a harmless package version difference. A database port exposed to the internet, an unknown SSH key, a leaked application secret, or a failed backup should be treated as urgent. Restrict access first, preserve relevant logs, rotate affected credentials, patch or remove the component, and investigate before simply deleting evidence.
Watch for gradual risk as well. Disk usage that rises every week can eventually stop logging or break a database. More containers can create a wider attack surface and make patching inconsistent. A growing list of firewall exceptions may indicate that the architecture needs a private network or a better reverse-proxy boundary. Security is partly about these operational trends, not only obvious alarms.
When to revisit
Revisit this self-hosted server hardening checklist at least monthly for an internet-facing host and quarterly for a stable, isolated homelab. Review it immediately after a new public application, domain, reverse proxy, VPN, administrator, cloud provider, or container platform is introduced. Also trigger an unscheduled review after a security advisory affecting your operating system or application stack, a suspected credential leak, an unexplained outage, or a restore failure.
Keep a small hardening record with the server purpose, owner, operating-system version, public services, SSH policy, backup location, monitoring destination, last restore test, and next review date. On each review, run the verification commands, compare the results with the previous record, and create a concrete remediation task for every exception.
Printable checkpoint: SSH access tested; root policy verified; keys reviewed; firewall rules reviewed; listening ports inventoried; Docker ports and privileges checked; updates applied or scheduled; secrets protected; backups completed; restore tested; logs and alerts working; certificates valid; public services still necessary; next review date recorded.
For a broader recurring routine, pair this article with the Self-Hosted Server Maintenance Checklist. Hardening is most effective when it becomes part of normal operations rather than a one-time task performed only after something goes wrong.