Skip to content

Configuring Your Server

Approximate Time: 30 minutes (this guide) + 30 minutes (Backing Up Your Server)


This guide assumes you have already created a new server according to Renting a Server or similar.

As noted in that guide, the server should have the latest version of Debian and be reachable via SSH.

ssh root@YOUR-IP-HERE

Securing the Server

The first thing to do is to secure the server. It's good to do this first so you don't forget to come back to it once your server is actually hosting a site.

Additionally, a mistake here comes with a risk of locking yourself out. Do this early so that if you need to delete the server and restart, you can easily do so without losing work.

Create a New User

apt install sudo
adduser admin
usermod -aG sudo admin
mkdir -p /home/admin/.ssh
cp ~/.ssh/authorized_keys /home/admin/.ssh/
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh
chmod 600 /home/admin/.ssh/authorized_keys

This creates a new user named admin (you can change this, but be sure to update all commands below).

This command will prompt for a few things, you can skip all of them except for the password. Be sure that you remember this password!

It also makes it possible to log in with the same SSH key you used to log in as root.

It is recommended that you avoid using the root account, instead using sudo as needed to elevate privileges.

Disable root SSH and password login

cat > /etc/ssh/sshd_config.d/00-hardening.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
EOF
sshd -t && systemctl reload ssh

Bots are constantly trying to guess passwords for common account names on open SSH ports.

This adds a set of rules disabling dangerous logins: the root account and non-key based authentication.

Confirm Login Before Proceeding

Once root login is disabled, it will essential to confirm that you can login with the new account before proceeding.

Keep your current SSH window open, and open a new one.

Switch Users

Without closing the old SSH window. In a new window, run:

ssh admin@YOUR-IP-HERE

If this is unable to connect, carefully re-read prior steps and diagnose the error in your original SSH connection. Do not close the logged-in root session until this command succeeds.

Once you are logged in as admin in a second window, you can close the root SSH session window and proceed.

Enable fail2ban

sudo apt install fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

The final command should give a positive confirmation that fail2ban is enabled for SSH.

fail2ban helps deter brute force attacks. With the SSH changes above, this is mostly to reduce log noise.

Enable unattended-upgrades

sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

A prompt will appear, you should select (y)es.

This installs Debian security updates automatically, so known vulnerabilities get patched without you logging in.

Warning

This does not enable automatic reboots that are required for some updates. If you log in to the server and see a "reboot required" message you may want to manually reboot at a time that is convenient to you.

Configuring DNS

Before we proceed, we want to give this server a domain name. This will be required by the next step to enable HTTPS and make our site accesible in the browser.

This requires us to login to our domain registrar's website and access the DNS settings.

Porkbun's panel is here: https://porkbun.com/account/domainsSpeedy Under the domain you're using, click the button that says 'DNS'. This will open a side panel with a button that says '+ Add Record'.

Clicking this will reveal a form with a few fields:

  • Type: CNAME
  • Host: 'app' (or whatever subdomain you want to configure)
  • IPv4 Address: your server's IP
  • TTL: 600 (default)

This will have Porkbun publish a DNS record that will route requests to app.example.com to your IP.

Preparing to Run Applications

There are two common paths to handling long-running applications on the server. One of these, systemd, is already running on the server and manages existing long-running processes.It is possible to run your own application as a systemd process, and install packages like caddy, postgres, redis, etc. using apt, which will give them systemd services as well.

We'll use docker instead, which comes with a bit of overhead, but allows the same environment to be used on your dev machine and other servers.

Installing Docker

sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

(These commands come from https://docs.docker.com/engine/install/debian/#install-using-the-repository .)

Confirm that this worked by running:

sudo docker run hello-world

If this finishes without error, your server is ready to run docker images.

Running the Application Stack

To run your own applications, you'll need to first Dockerize Your Application.

In the example below we'll use Miniflux, an RSS reader, as a stand-in application.

We are going to create three files, I suggest you place them in ~/compose/ but any directory is fine.

compose.yml

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - miniflux

  miniflux:
    image: miniflux/miniflux:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
      RUN_MIGRATIONS: 1
      CREATE_ADMIN: 1
      ADMIN_USERNAME: admin
      ADMIN_PASSWORD: ${MINIFLUX_ADMIN_PASSWORD}
      BASE_URL: https://app.example.com # CHANGE

  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_USER: miniflux
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: miniflux
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux"]
      interval: 10s
      start_period: 30s

volumes:
  caddy_data:
  caddy_config:
  db_data:

Caddyfile

rss.example.com { # CHANGE
    reverse_proxy miniflux:8080
}

.env

POSTGRES_PASSWORD=change-me
MINIFLUX_ADMIN_PASSWORD=change-me-too
This configures three services:

  • Caddy, our reverse proxy. This handles all incoming HTTP requests and dispatches them based on the rules in Caddyfile. Our DNS settings from before ensure that requests for app.example.com are sent to our server, and this file tells Caddy what to do, in this case to allow them to be processed by Miniflux.

This is what makes it possible to host many apps on a single server. Each app would need another DNS CNAME entry and another entry in the Caddyfile.

  • Postgres, our database. Configured with a persistent data volume so that data survives the process being recreated.
  • Miniflux, an RSS reader. Used here as a stand-in for a simple dynamic application.

Managing Secrets

We created an .env file, which is where secrets are kept.

Remember it is important that you never commit secrets to git, as doing so is likely to lead to accidental breaches.

If you've committed a credential to git already, removing it from git is not enough; the credential itself needs to be disabled on the server since it will survive in history and likely has already been harvested by malicious bots.

Starting Docker

If you run sudo docker compose up -d all of the services in compose.yml should start.

sudo docker compose ps should show all services as running after a few seconds.

sudo docker compose logs -f will allow you to see the current logs to monitor for any issues. It isn't uncommon for this to be noisy at startup, but if things quiet down with no errors printed, the application is running.

Congratulations, you've deployed an app!

You should now be able to visit app.example.com and see your application running in the browser!

Configuring Backups

One final step before we can say that we're done setting up the server: we want to configure backups.

Maintaining the Server

Having an application server is not as painless as static hosting. It takes some routine maintenance to keep it secure and running. An hour or less a month is plenty for a simple server.

Security Updates

We configured unattended-upgrades above, so this is mostly taken care of.

As mentioned, the server will not automatically reboot. Keep an eye on messages to reboot the server and do this manually as needed.

Docker Image Updates

Occasionally, you may want to update the services running in docker.

To do this, run:

sudo docker compose pull
sudo docker compose up -d
sudo docker image prune -f

Once again, you can use sudo docker compose ps and/or docker compose logs -f to see if everything is working as intended.

This pulls the latest images, restarts the services that changed, and prunes unused images.

Warning

Updating pinned docker image versions (like postgres:17 to postgres:18) is out of scope for this guide and should only be done with great care.

You may want to try any version updates locally to ensure the new versions work as expected.

Next Steps

The final part of this process is Backing Up Your Server.