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¶
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¶
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¶
.env¶
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 forapp.example.comare 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:
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:
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.