Stephen Gilmore

Setting up a new VPS for Go, Django, etc

June 24, 2023 #Django #Go #Linux #PostgreSQL #Python #VPS #Redis #Self-Hosting

I've done this a few times and taking this one to go a bit slower and clean up some notes.

Create the VPS instance

Set up an SSH alias on local computer

Execute nano ~/.ssh/config

In the file, add the following:

Host mysite
        HostName 123.12.12.1234
        User myuser

Save and now you'll be able to ssh in with ssh myuser@mysite instead of ssh myuser@123.12.12.1234

Login and update the instance

sudo apt-get update
sudo apt-get upgrade
sudo apt-get full-upgrade
sudo apt-get autoremove

Install and set up UFW firewall

Install:

# Install ufw
sudo apt intall ufw

# Allow ssh
sudo ufw allow ssh
sudo ufw allow "WWW Full" # or "WWW Secure" for https only

# Enable ufw
sudo ufw enable
sudo ufw reload
sudo ufw status

# optional - see a list of ufw apps
sudo ufw app list

Check and update SSH Permissions

# Open the SSH config file
sudo nano /etc/ssh/sshd_config
- PasswordAuthentication no To disable password authentication and require an SSH Key - PermitRootLogin no To disable root login (don't do until after setting up an alternate user)

There's a lot more available here in the article How to Harden OpenSSH

Then Restart the ssh service:

sudo systemctl restart sshd

Logout with logout and if you can ssh back in, you didn't lock yourself out!

Set up a non-root user

Create a new user with the adduser command.

adduser myuser

You will should be prompted to set up a passsword, fill that out and you can skip the other things.

Add the user to the sudo group with

usermod -aG sudo myuser

Copy the SSH key from the root user to your user

Create the folder for your user if it doesn't exist

mkdir /home/myuser/.ssh

Make the directory only executable by the user

chmod 700 /home/myuser/.ssh

Copy the authorized keys to the user

sudo cp /root/.ssh/authorized_keys /home/myuser/.ssh/authorized_keys

Change ownership to the user

sudo chown -R myuser:myuser /home/myuser/.ssh

Make it readable only by the current user

sudo chmod 600 /home/myuser/.ssh/authorized_keys

logout and try to ssh back in with the myuser user

Install rsync

rsync is very useful for pushing files up to the server.

sudo apt install rsync

Install caddy as a reverse proxy

We'll use this to route requests from the outside world to our static files and Django application. A few nice things about caddy: - Automatic certificates for https - Pretty easy to set up config and nice defaults like routing http to https

Following the stable release installation instructions here: Debian/Ubuntu/Raspbian instructions.

These were the instructions at the time I wrote this:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list

sudo apt update

sudo apt install caddy

Check the status of caddy:

sudo systemctl status caddy
# 'q' to exit

Serve a test page from Caddy

# Edit the caddy file
sudo nano /etc/caddy/Caddyfile

Add...

{
    auto_https off
}

:80 {
    respond "hello world"
}

Restart Caddy and check the status

sudo systemctl restart caddy
sudo systemctl status caddy

In another terminal, use curl to test if it's working:

> curl server.ip.addres.here
hello world

Install Docker

  1. Docker install using the repository

  2. Linux post-installation instructions.

    • Don't "Manage Docker as a non-root user"
    • Do Enable docker to boot with systemd
    • Do Configure logging (I typically use the "local" logging driver)

Install PostgreSQL

Digital Ocean has a pretty good guide.

Installation:

sudo apt update
sudo apt --yes install postgresql postgresql-contrib

# postgresql should be runnnig, but if not, start it
sudo systemctl start postgresql.service

Create a new postgres user:

# 1. Set your variables
USERNAME="my_app_user"
DB_NAME="my_app_db"

# 2. Create the user (role)
sudo -i -u postgres psql -c "CREATE ROLE ${USERNAME} WITH LOGIN;"

# 3. Set the user's password securely (this line will prompt you)
sudo -i -u postgres psql -c "\password ${USERNAME}"

# 4. Create the database and make the new user the owner
sudo -i -u postgres psql -c "CREATE DATABASE ${DB_NAME} WITH OWNER = ${USERNAME};"

## -- Situational or Optional things --

# Grant the user permissions on the public schema (often needed by frameworks)
sudo -i -u postgres psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${USERNAME};"

# Install 'citext' extension
sudo -i -u postgres psql -d ${DB_NAME} -c "CREATE EXTENSION IF NOT EXISTS citext;"

# Install 'hstore' extension
sudo -i -u postgres psql -d ${DB_NAME} -c "CREATE EXTENSION IF NOT EXISTS hstore;"

Install Fail2Ban

# Install fail2ban.
sudo apt --yes install fail2ban

# Create a fail2ban.local configuration file that overrides fail2ban.conf
sudo cp /etc/fail2ban/fail2ban.conf /etc/fail2ban/fail2ban.local
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Optional: golang-migrate

https://github.com/golang-migrate/migrate/blob/master/cmd/migrate/README.md

# Install the migrate CLI tool.
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz
mv migrate.linux-amd64 /usr/local/bin/migrate

# Check migrate installation
migrate -version

Some configuration examples

Serve a Django site from Caddy

sglmr.com {

        handle_path /media/* {
                root * /var/www/dg-media     
                file_server
        }

        handle_path /static/* {
                root * /var/www/dg-static     
                file_server {
                        precompressed br gzip
                }
        }

        handle {
                reverse_proxy 127.0.0.1:8000
        }

}

Install redis server for task queues and caching

Not needed if using redis through docker

Install redis to use as a task queue and cache for Django.

sudo apt install redis-server

Open the config file

sudo nano /etc/redis/redis.conf

Change the supervised setting to systemd

# change the supervised setting to `systemd`
supervised systemd

# uncomment the `save` setting to "" to disable persistence
# and also make sure appendonly is set to no
save ""
appendonly no

Restart redis to make sure the settings take effect:

sudo systemctl restart redis.service

Make sure redis is running:

sudo systemctl status redis

Test Redis

Open up the redis cli

redis-cli
Type ping and you should get a response PONG

Try adding a test record set test "Hello redis!". You should get an OK response.

Try to retrieve the value with get test. You should get the same Hello redis! message back.

Type exit to exit.

If trying to disable persistence, then we'll want to test that to. Restart your machine with sudo reboot now.

SSH back into the server. Check the redis status again

sudo systemctl status redis

Go back into the redis-cli

redis-cli

Try to get your test record again with get test. The response should be (nil).

We can also double check the config settings by using config get save and config get appendonly

A full script

#!/bin/bash

# This script configures a new Debian-based server.
# It should be run with sudo privileges.

# Ensure the script is run as root
if [ "$(id -u)" -ne 0 ]; then
  echo "This script must be run as root. Please use sudo." >&2
  exit 1
fi

set -e # Exit immediately if a command exits with a non-zero status.

# --- Variable Prompts ---
# Prompt for new user details
read -p "Enter the username for the new non-root user: " new_username
while true; do
  read -s -p "Enter the password for the new non-root user: " new_user_password
  echo
  read -s -p "Confirm the password: " new_user_password_confirm
  echo
  [ "$new_user_password" = "$new_user_password_confirm" ] && break
  echo "Passwords do not match. Please try again."
done

# --- System Configuration ---

echo "### Creating new user: ${new_username} ###"
# Create a new sudo user and hash the password
useradd -m -s /bin/bash -G sudo "$new_username"
echo "${new_username}:${new_user_password}" | chpasswd
echo "User ${new_username} created."

echo "### Setting up SSH for ${new_username} ###"
# Copy the root user's authorized_keys file to the new user.
# This assumes you have already SSH'd in as the root user.
if [ -f /root/.ssh/authorized_keys ]; then
    mkdir -p "/home/${new_username}/.ssh"
    cp /root/.ssh/authorized_keys "/home/${new_username}/.ssh/authorized_keys"
    chown -R "${new_username}:${new_username}" "/home/${new_username}/.ssh"
    chmod 700 "/home/${new_username}/.ssh"
    chmod 600 "/home/${new_username}/.ssh/authorized_keys"
    echo "SSH authorized key copied from root user."
else
    echo "Warning: /root/.ssh/authorized_keys not found. SSH key was not set for ${new_username}."
fi

echo "### Disabling SSH password authentication ###"
# Disable SSH password authentication for security
sed -i -E 's/^#?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
# Validate the sshd_config file syntax
sshd -t
# Restart SSH to apply changes
systemctl restart sshd
echo "SSH configuration hardened."

echo "### Updating system packages ###"
# Update apt cache and upgrade all packages
apt-get update
apt-get dist-upgrade -y

echo "### Installing required system packages ###"
# Install base packages
apt-get install -y ufw postgresql postgresql-contrib fail2ban curl gpg debian-keyring debian-archive-keyring apt-transport-https unattended-upgrades

echo "### Setting timezone to America/Los_Angeles ###"
timedatectl set-timezone America/Los_Angeles

# --- Caddy Installation ---
echo "### Installing Caddy Web Server ###"
install -d -m 0755 /etc/apt/keyrings
curl -fsSL "https://dl.cloudsmith.io/public/caddy/stable/gpg.key" | gpg --dearmor -o /etc/apt/keyrings/caddy-stable.gpg
chmod 0644 /etc/apt/keyrings/caddy-stable.gpg
echo "deb [signed-by=/etc/apt/keyrings/caddy-stable.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main" > /etc/apt/sources.list.d/caddy-stable.list
apt-get update
apt-get install -y caddy
echo "Caddy installed."

# --- Valkey Installation ---
echo "### Installing Valkey ###"
apt-get update
apt-get install -y valkey
systemctl start valkey
echo "Valkey installed and enabled."

# --- Security and Firewall ---
echo "### Configuring Firewall (UFW) ###"
ufw allow ssh
ufw allow 'WWW Full' # Allows both HTTP and HTTPS
ufw --force enable
echo "Firewall configured and enabled."

echo "### Starting and enabling Fail2Ban ###"
systemctl start fail2ban
systemctl enable fail2ban
echo "Fail2Ban service started."

# --- Install uv (Python Package Installer) ---
echo "### Installing uv for ${new_username} ###"
curl -LsSf https://astral.sh/uv/install.sh -o /tmp/install-uv.sh
chmod +x /tmp/install-uv.sh
# Run the installer as the new user
runuser -l "$new_username" -c "/tmp/install-uv.sh"
rm /tmp/install-uv.sh
echo "'uv' has been installed for ${new_username}."

# --- Finalization ---
echo
echo "✅ Server setup complete!"
echo "You should now be able to SSH into the server as '${new_username}' using your SSH key.