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 update
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 is essential to confirm that you can log in 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, run in a new, second window:

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; kernel upgrades will require manual reboots.

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 accessible in the browser.

This requires us to log in 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: A
  • 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.

Create a directory with mkdir -p ~/compose, and then within it create three files:

Tip

If you aren't familiar with any CLI text editors, use nano <filename> to edit a file.

Alternatively, your text editor likely has a way to edit files on a remote server.

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

  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:18
    restart: unless-stopped
    environment:
      POSTGRES_USER: miniflux
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: miniflux
    volumes:
      - db_data:/var/lib/postgresql
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux"]
      interval: 10s
      start_period: 30s

volumes:
  caddy_data:
  caddy_config:
  db_data:

Caddyfile

app.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 forward them to Miniflux.

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

  • Postgres, our database. Configured with a persistent data volume so that data survives the container 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

From within ~/compose, run sudo docker compose up -d and 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!

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.

Tip

Set a reminder to ensure you spend a bit of time each month checking in on your server. Most of the time this will take less than 5 minutes, but it can help catch issues early.

Update Docker Engine

While most packages update automatically, Docker will not.

A periodic manual update can be done with:

sudo apt update && sudo apt upgrade

This will update Docker and any other system packages that are out of date.

Kernel Security Updates

As mentioned, the server will not automatically reboot. If there is a kernel (Linux) update, you will need to manually reboot the server.

Check if the file /var/run/reboot-required is present:

ls /var/run/reboot-required

If it isn't present an error message will be printed and no reboot is needed.

To reboot if it exists:

ls /var/run/reboot-required && sudo reboot

After a reboot, double check that all of your Docker containers come back up. You may need to manually start them with cd ~/compose && sudo docker compose up -d.

Docker Image Updates

Things running within docker need to be manually updated and restarted as well.

To do this, run:

cd ~/compose
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 sudo 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:18 to postgres:19) is out of scope for this guide and should only be done with great care.

You should also generally avoid using :latest for dependencies. Note that in the example above we use :latest only on the stand-in for your application.

It is recommended you try any major version updates locally to ensure the new versions work as expected.

Next Steps

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