Setting Up a Production-Ready VPS from Scratch: A Comprehensive Guide

Md Saif ZamanMd Saif Zaman
9 min read

Table of Contents

  1. Introduction

  2. Domain Name Configuration

  3. SSH Hardening

  4. Docker Installation

  5. Docker Compose Installation

  6. Traefik Setup

  7. Running a Go App in Docker

  8. Watchtower Setup

  9. Monitoring with UptimeRobot

  10. Putting It All Together

  11. Conclusion

1. Introduction

Setting up a production-ready Virtual Private Server (VPS) involves multiple steps to ensure security, reliability, and ease of management. This guide will walk you through the process of configuring a VPS from scratch, covering everything from basic server setup to deploying containerized applications with automated updates and monitoring.

2. Domain Name Configuration

The first step in setting up your VPS is to configure a domain name. This involves:

  1. Purchasing a domain name from a registrar (e.g., GoDaddy, Namecheap, Google Domains).

  2. Pointing your domain’s DNS records to your VPS’s IP address.

  3. Setting up an A record for your root domain and www subdomain.

For example, if your domain is example.com and your VPS IP is 203.0.113.1, you'd create:

  • An A record for example.com pointing to 203.0.113.1

  • An A record for www.example.com pointing to 203.0.113.1

You can verify your DNS configuration using the dig command:

dig example.com
dig www.example.com

Look for the “ANSWER SECTION” in the output to confirm that your A records are set correctly. Allow some time for DNS propagation before proceeding to the next steps.

3. SSH Hardening

Securing SSH access to your VPS is crucial for maintaining a robust security posture. We’ll create a script that automates the process of hardening SSH, creating a new user, and setting up Fail2Ban.

Here’s the comprehensive SSH hardening script:

#!/bin/bash
# Automated SSH Hardening Script
# Run this script as root or with sudo privileges
# Function to print error messages
error_exit() {
echo "$1" >&2
exit 1
}
# Check if script is run as root
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root"
fi
# Prompt for new username
read -p "Enter new username: " NEW_USER
# Prompt for new SSH port
read -p "Enter new SSH port (between 1024 and 65535): " NEW_SSH_PORT
# Validate SSH port
if ! [[ "$NEW_SSH_PORT" =~ ^[0-9]+$ ]] || [ "$NEW_SSH_PORT" -le 1024 ] || [ "$NEW_SSH_PORT" -ge 65535 ]; then
error_exit "Invalid SSH port. Please choose a port between 1024 and 65535."
fi
# Create new user and add to sudo group
adduser $NEW_USER || error_exit "Failed to create new user"
usermod -aG sudo $NEW_USER || error_exit "Failed to add user to sudo group"
# Generate SSH key for the new user
su - $NEW_USER -c "ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ''" || error_exit "Failed to generate SSH key"
# Backup original sshd_config
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak || error_exit "Failed to backup sshd_config"

# Configure SSH
cat > /etc/ssh/sshd_config << EOF
Port $NEW_SSH_PORT
AddressFamily inet
Protocol 2
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
UsePrivilegeSeparation yes
KeyRegenerationInterval 3600
ServerKeyBits 1024
SyslogFacility AUTH
LogLevel VERBOSE
LoginGraceTime 30
PermitRootLogin no
StrictModes yes
RSAAuthentication yes
PubkeyAuthentication yes
IgnoreRhosts yes
RhostsRSAAuthentication no
HostbasedAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
PasswordAuthentication no
X11Forwarding no
X11DisplayOffset 10
PrintMotd no
PrintLastLog yes
TCPKeepAlive yes
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
UsePAM yes
AllowUsers $NEW_USER
MaxAuthTries 3
MaxSessions 2
ClientAliveInterval 300
ClientAliveCountMax 2
EOF

# Install and configure Fail2Ban
apt-get update && apt-get install -y fail2ban || error_exit "Failed to install Fail2Ban"
cat > /etc/fail2ban/jail.local << EOF
[sshd]
enabled = true
port = $NEW_SSH_PORT
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
EOF

# Restart SSH and Fail2Ban services
systemctl restart sshd || error_exit "Failed to restart SSH service"
systemctl restart fail2ban || error_exit "Failed to restart Fail2Ban service"
# Configure firewall (assuming UFW is installed)
ufw allow $NEW_SSH_PORT/tcp || error_exit "Failed to update firewall rules"
ufw enable || error_exit "Failed to enable firewall"
echo "SSH hardening completed successfully!"
echo "New user '$NEW_USER' created with sudo privileges."
echo "New SSH port: $NEW_SSH_PORT"
echo "Please make sure to update your SSH client configuration and reconnect using:"
echo "ssh -p $NEW_SSH_PORT $NEW_USER@your_server_ip"
echo "IMPORTANT: Keep your SSH keys safe and do not lose them!"

To use this script:

  1. Save it to a file, for example, ssh_hardening.sh.

  2. Make it executable: chmod +x ssh_hardening.sh

  3. Run it with sudo privileges: sudo ./ssh_hardening.sh

This script performs the following actions:

  1. Creates a new user with sudo privileges

  2. Generates an SSH key for the new user

  3. Configures SSH with hardened settings

  4. Installs and configures Fail2Ban

  5. Updates firewall rules

  6. Restarts necessary services

After running the script, you’ll need to exit your current SSH session and reconnect using the new port and username.

4. Docker Installation

Docker is a platform for developing, shipping, and running applications in containers. Here’s a script to automate the installation of Docker:

#!/bin/bash
# Update the apt package index
sudo apt-get update
# Install packages to allow apt to use a repository over HTTPS
sudo apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Set up the stable repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
# Add your user to the docker group
sudo usermod -aG docker $USER
echo "Docker installed successfully. Please log out and back in for group changes to take effect."

Save this script as docker_install.sh, make it executable with chmod +x docker_install.sh, and run it with ./docker_install.sh.

5. Docker Compose Installation

Docker Compose is a tool for defining and running multi-container Docker applications. Here’s a script to install Docker Compose:

#!/bin/bash
# Download the current stable release of Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# Apply executable permissions to the binary
sudo chmod +x /usr/local/bin/docker-compose
echo "Docker Compose installed successfully."

Save this script as docker_compose_install.sh, make it executable, and run it.

6. Traefik Setup

Traefik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. Here’s a script to set up Traefik:

#!/bin/bash
# Create necessary directories
mkdir -p traefik
cd traefik
# Create traefik.toml configuration file
cat > traefik.toml << 'EOT'
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.websecure]
address = ":443"
[api]
dashboard = true
[providers]
[providers.docker]
exposedByDefault = false
[certificatesResolvers.myresolver.acme]
email = "your-email@example.com"
storage = "acme.json"
[certificatesResolvers.myresolver.acme.tlsChallenge]
EOT
# Create Docker Compose file for Traefik
cat > docker-compose.yml << 'EOT'
version: '3'
services:
traefik:
image: traefik:v2.4
command:
- "--configfile=/etc/traefik/traefik.toml"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.toml:/etc/traefik/traefik.toml
- ./acme.json:/acme.json
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=myresolver"
volumes:
letsencrypt:
EOT
# Create empty acme.json file and set proper permissions
touch acme.json
chmod 600 acme.json
echo "Traefik setup completed. Please update the email in traefik.toml and domain in docker-compose.yml before starting."

Save this as traefik_setup.sh, make it executable, and run it. Remember to update the email address and domain name in the generated files before starting Traefik.

7. Running a Go App in Docker

To run a Go application in Docker, you need to create a Dockerfile. Here’s an example Dockerfile for a Go application:

# Build stage
FROM golang:1.20 AS builder
# Set the working directory
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download all dependencies
RUN go mod download
# Copy the source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Final stage
FROM alpine:latest
# Install ca-certificates
RUN apk --no-cache add ca-certificates
# Set the working directory
WORKDIR /root/
# Copy the binary from the build stage
COPY --from=builder /app/main .
# Expose the port the app runs on
EXPOSE 8080
# Command to run the application
CMD ["./main"]

This Dockerfile uses a multi-stage build to create a smaller final image. It also includes steps to cache dependencies, which can speed up subsequent builds.

To build and run this Docker image:

docker build -t myapp .
docker run -p 8080:8080 myapp

8. Watchtower Setup

Watchtower is a utility that automates the updating of Docker containers to the latest available image. Here’s a script to set up Watchtower:

#!/bin/bash
# Create watchtower directory
mkdir -p watchtower
cd watchtower
# Create Docker Compose file for Watchtower
cat > docker-compose.yml << 'EOT'
version: '3'
services:
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 30 --cleanup
labels:
- "com.centurylinklabs.watchtower.enable=true"
EOT
echo "Watchtower setup completed. You can customize the update interval in the docker-compose.yml file."

Save this as watchtower_setup.sh, make it executable, and run it. You can adjust the update interval by modifying the --interval parameter in the Docker Compose file.

9. Monitoring with UptimeRobot

UptimeRobot is a free external monitoring service that can check your website’s uptime every 5 minutes. Here’s a reminder script for setting up UptimeRobot:

#!/bin/bash
echo "UptimeRobot Setup Reminder:"
echo "1. Go to https://uptimerobot.com/ and sign up for an account if you haven't already."
echo "2. Log in to your UptimeRobot account."
echo "3. Click on 'Add New Monitor'."
echo "4. Choose 'HTTP(s)' as the monitor type."
echo "5. Enter a friendly name for your monitor."
echo "6. Enter the URL of your website or application."
echo "7. Set the monitoring interval (5 minutes is recommended for free accounts)."
echo "8. Click 'Create Monitor'."
echo ""
echo "Remember to create monitors for all critical endpoints of your application."

Save this as uptimerobot_setup.sh and run it as a reminder when you're ready to set up monitoring.

10. Putting It All Together

Now that we have all the individual components, let’s create a main script that runs all the setup scripts in the correct order:

#!/bin/bash
# Main script to run all setup scripts
# 1. SSH Hardening
./ssh_hardening.sh
# 2. Docker Installation
./docker_install.sh
# 3. Docker Compose Installation
./docker_compose_install.sh
# 4. Traefik Setup
./traefik_setup.sh
# 5. Watchtower Setup
./watchtower_setup.sh
# 6. UptimeRobot Setup (reminder)
./uptimerobot_setup.sh
echo "All setup scripts have been run. Please review the output of each script"

Demo docker_compose.yaml:

version: '3.8'

services:
# Watchtower for automated updates
watchtower:
image: containrrr/watchtower
command:
- "--label-enable"
- "--interval"
- "30"
- "--rolling-restart"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Mount the watchtower config file
configs:
- source: watchtower_config
target: /config.json

# Traefik as reverse proxy and load balancer
reverse-proxy:
image: traefik:v3.1
command:
- "--providers.docker"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=user@xyz.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock
# Mount the Traefik static config file
configs:
- source: traefik_config
target: /etc/traefik/traefik.toml

# Our Go application
your_app:
image: ghcr.io/dreamsofcode-io/your_app:prod
labels:
- "traefik.enable=true"
- "traefik.http.routers.your_app.rule=Host(`xyz.com`)"
- "traefik.http.routers.your_app.entrypoints=websecure"
- "traefik.http.routers.your_app.tls.certresolver=myresolver"
- "com.centurylinklabs.watchtower.enable=true"
secrets:
- db-password
environment:
- POSTGRES_HOST=db
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
- POSTGRES_USER=postgres
- POSTGRES_DB=your_app
- POSTGRES_PORT=5432
- POSTGRES_SSLMODE=disable
deploy:
mode: replicated
replicas: 3
restart: always
depends_on:
db:
condition: service_healthy

# PostgreSQL database
db:
image: postgres
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=your_app
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: [ "CMD", "pg_isready" ]
interval: 10s
timeout: 5s
retries: 5

volumes:
db-data:
letsencrypt:

secrets:
db-password:
file: db/password.txt

configs:
traefik_config:
file: ./traefik.toml
watchtower_config:
file: ./watchtower.json
0
Subscribe to my newsletter

Read articles from Md Saif Zaman directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Md Saif Zaman
Md Saif Zaman