Setting up a new VPS for Go, Django, etc
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
- Pick the size and operating system
- Add an SSH key
- Assign an IP to the server and write it down
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
-
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
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.