Learning Linux: Setting Up My First VPS

The terminal used to intimidate me. That black screen with cryptic commands felt like something only "real" developers used. But when I decided to set up my first Virtual Private Server (VPS), I had no choice but to dive into Linux. What followed was one of the most empowering learning experiences of my development journey.

This post chronicles my transformation from a Windows user who barely knew what SSH meant to someone who now manages Linux servers with confidence. If you're hesitant about taking the Linux plunge, I hope my story encourages you to take that first step.

Why I Needed a VPS

As my development skills grew, I kept hitting limitations with local development. I wanted to:

  • Host projects that could be accessed from anywhere
  • Learn server administration and DevOps basics
  • Have a playground for experimenting with different technologies
  • Understand how real web applications are deployed
  • Set up services that need to run 24/7

After researching various providers, I chose Hetzner. Their prices were reasonable (starting at €3.79/month), they had data centers in Europe, and their documentation seemed beginner-friendly. Little did I know that clicking "Create Server" would begin an intense learning journey.

First Contact: SSH and Terminal Basics

When Hetzner sent me my server credentials, I stared at them confused. IP address? Root password? SSH? These terms were foreign to me. My first challenge was simply connecting to the server.

My First SSH Connectionbash
1# The command that started it all
2  ssh root@49.13.80.27
3  
4  # I was greeted with:
5  The authenticity of host '49.13.80.27' can't be established.
6  ED25519 key fingerprint is SHA256:...
7  Are you sure you want to continue connecting (yes/no)?
8  
9  # After typing 'yes' and entering the password:
10  Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-88-generic x86_64)
11  root@ubuntu-2gb-fsn1-2:~#

That moment when I saw the command prompt change to show I was on a different machine,it was magical. I was controlling a computer hundreds of miles away with just text commands. But the magic quickly turned to panic when I realized I didn't know any Linux commands!

Learning Essential Linux Commands

I spent the first week learning basic Linux commands. Each one felt like unlocking a new superpower:

Linux Commands That Changed My Lifebash
1# Navigation and file management
2  pwd                    # Where am I?
3  ls -la                 # What's here? (show hidden files too)
4  cd /var/log           # Go to log directory
5  mkdir projects         # Create a directory
6  rm -rf old_folder     # Remove directory (careful with this!)
7  
8  # File operations
9  touch newfile.txt      # Create empty file
10  nano newfile.txt       # Edit file (beginner-friendly editor)
11  cat file.txt          # Display file contents
12  grep "error" log.txt  # Search for text in files
13  chmod 755 script.sh   # Change file permissions
14  
15  # System information
16  df -h                 # Disk space usage
17  free -m               # Memory usage
18  top                   # Running processes (press 'q' to quit)
19  ps aux                # List all processes
20  uname -a              # System information
21  
22  # Package management (Ubuntu/Debian)
23  apt update            # Update package list
24  apt upgrade           # Upgrade installed packages
25  apt install nginx     # Install new software
26  apt remove nginx      # Remove software
27  
28  # Network commands
29  ip addr               # Show network interfaces
30  ping google.com       # Test connectivity
31  netstat -tlnp        # Show listening ports
32  curl ifconfig.me     # Get public IP address
33  
34  # User management
35  adduser nikolas       # Create new user
36  passwd nikolas        # Change password
37  su - nikolas          # Switch user
38  exit                  # Return to previous user

What amazed me was how logical everything was. Unlike GUI applications where features are hidden in menus, Linux commands do exactly what they say. cp copies, mv moves, rm removes. The simplicity was beautiful.

My First Big Mistake: Learning About Permissions

About two weeks in, I made a classic rookie mistake. I was trying to make a script executable and ran:

The Command That Taught Me Respectbash
1# DON'T DO THIS!
2  chmod 777 /
3  
4  # What I meant to do:
5  chmod 777 ./myscript.sh
6  
7  # The result: Permission denied errors everywhere!

I had accidentally changed permissions on the root directory, breaking numerous system functions. This mistake taught me two crucial lessons:

  • Always double-check commands, especially with sudo or as root
  • Understand what you're doing before executing commands

Thankfully, Hetzner had a rescue system that let me boot from a recovery image and fix the permissions. It was a stressful few hours, but I learned more about Linux permissions than any tutorial could have taught me.

Securing the Server: My Introduction to Linux Security

Once I could navigate Linux comfortably, I learned that having a server on the internet is like having a house with a thousand doors,you need to lock them properly. Here's what I learned about basic server security:

Essential Security Setupbash
1# 1. Create a non-root user
2  adduser nikolas
3  usermod -aG sudo nikolas
4  
5  # 2. Set up SSH key authentication
6  # On local machine:
7  ssh-keygen -t ed25519 -C "nikolas@email.com"
8  
9  # Copy public key to server:
10  ssh-copy-id nikolas@49.13.80.27
11  
12  # 3. Disable root login and password authentication
13  sudo nano /etc/ssh/sshd_config
14  # Set these values:
15  # PermitRootLogin no
16  # PasswordAuthentication no
17  # PubkeyAuthentication yes
18  
19  # Restart SSH service
20  sudo systemctl restart ssh
21  
22  # 4. Set up firewall
23  sudo ufw allow 22/tcp      # SSH
24  sudo ufw allow 80/tcp      # HTTP
25  sudo ufw allow 443/tcp     # HTTPS
26  sudo ufw enable
27  
28  # 5. Install fail2ban to prevent brute force attacks
29  sudo apt install fail2ban
30  sudo systemctl enable fail2ban
31  
32  # 6. Keep system updated
33  sudo apt update && sudo apt upgrade -y
34  
35  # 7. Set up automatic security updates
36  sudo apt install unattended-upgrades
37  sudo dpkg-reconfigure --priority=low unattended-upgrades

The difference in login attempts was immediate. Before securing the server, the logs showed hundreds of failed login attempts daily. After implementing these measures, my server became practically invisible to automated attacks.

Installing My First Services

With a secure server, I was ready to install actual services. I started with the classics:

Nginx Web Server

Setting Up Nginxbash
1# Install Nginx
2  sudo apt install nginx
3  
4  # Check if it's running
5  sudo systemctl status nginx
6  
7  # Enable it to start on boot
8  sudo systemctl enable nginx
9  
10  # Create a simple website
11  sudo mkdir -p /var/www/mysite
12  sudo nano /var/www/mysite/index.html
13  
14  # Configure Nginx
15  sudo nano /etc/nginx/sites-available/mysite
16  # Added this configuration:
17  server {
18      listen 80;
19      server_name _;
20      root /var/www/mysite;
21      index index.html;
22  }
23  
24  # Enable the site
25  sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
26  sudo nginx -t  # Test configuration
27  sudo systemctl reload nginx

Docker: A Game Changer

Docker was the technology that really excited me. The ability to run applications in containers, isolated from each other, felt like magic:

Docker Installation and First Containerbash
1# Install Docker
2  curl -fsSL https://get.docker.com -o get-docker.sh
3  sudo sh get-docker.sh
4  
5  # Add user to docker group (avoid using sudo)
6  sudo usermod -aG docker $USER
7  
8  # Test Docker
9  docker run hello-world
10  
11  # Run a real application
12  docker run -d -p 8080:80 --name my-nginx nginx
13  
14  # See running containers
15  docker ps
16  
17  # Check logs
18  docker logs my-nginx
19  
20  # Stop and remove container
21  docker stop my-nginx
22  docker rm my-nginx
23  
24  # Docker Compose for complex setups
25  sudo apt install docker-compose
26  
27  # Create docker-compose.yml
28  version: '3'
29  services:
30    web:
31      image: nginx
32      ports:
33        - "80:80"
34      volumes:
35        - ./website:/usr/share/nginx/html

Docker opened up a world of possibilities. Suddenly, I could run complex applications without worrying about dependencies or configuration conflicts. Each application lived in its own container, making the server much easier to manage.

Monitoring and Maintenance

As I added more services, I learned the importance of monitoring. A server is like a garden,it needs regular attention:

Server Monitoring Basicsbash
1# Check system resources
2  htop  # Interactive process viewer (install with: sudo apt install htop)
3  
4  # Monitor disk usage
5  df -h
6  du -sh /var/log/*  # Find large files in logs
7  
8  # Check service status
9  systemctl status nginx
10  systemctl status docker
11  
12  # View recent logs
13  journalctl -xe
14  journalctl -u nginx -f  # Follow nginx logs
15  
16  # Set up log rotation
17  sudo nano /etc/logrotate.d/myapp
18  
19  # Basic monitoring script
20  #!/bin/bash
21  echo "=== System Health Check ==="
22  echo "Memory Usage:"
23  free -m
24  echo -e "
25Disk Usage:"
26  df -h
27  echo -e "
28Top 5 CPU Processes:"
29  ps aux --sort=-%cpu | head -6
30  
31  # Make it executable and schedule with cron
32  chmod +x health_check.sh
33  crontab -e
34  # Add: 0 * * * * /home/nikolas/health_check.sh

Backup Strategies I Learned the Hard Way

Three months into my VPS journey, I accidentally deleted an important configuration file. That's when I learned the critical importance of backups:

My Backup Approachbash
1# Simple backup script
2  #!/bin/bash
3  BACKUP_DIR="/home/nikolas/backups"
4  DATE=$(date +%Y%m%d_%H%M%S)
5  
6  # Create backup directory
7  mkdir -p $BACKUP_DIR
8  
9  # Backup important files
10  tar -czf $BACKUP_DIR/config_$DATE.tar.gz /etc/nginx /etc/ssh
11  tar -czf $BACKUP_DIR/data_$DATE.tar.gz /var/www
12  
13  # Backup Docker volumes
14  docker run --rm     -v myapp_data:/data     -v $BACKUP_DIR:/backup     alpine tar czf /backup/docker_data_$DATE.tar.gz /data
15  
16  # Keep only last 7 days of backups
17  find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
18  
19  # Sync to remote location (optional)
20  # rsync -avz $BACKUP_DIR/ user@backup-server:/backups/

Resource Management: Learning to Optimize

With a 2GB RAM server, I quickly learned about resource constraints. Here's how I optimized my setup:

  • Swap space: Added 2GB swap for memory overflow
  • Service tuning: Adjusted Nginx worker processes
  • Container limits: Set memory limits on Docker containers
  • Log rotation: Prevented logs from filling the disk
  • Monitoring: Set up alerts for high resource usage

Automation: Making Life Easier

As I became more comfortable, I started automating repetitive tasks:

Automation Scriptsbash
1# Update script
2  #!/bin/bash
3  echo "Starting system update..."
4  sudo apt update
5  sudo apt upgrade -y
6  sudo apt autoremove -y
7  sudo apt autoclean
8  echo "Update complete!"
9  
10  # Docker cleanup script
11  #!/bin/bash
12  echo "Cleaning up Docker..."
13  docker system prune -af --volumes
14  echo "Docker cleanup complete!"
15  
16  # Service health check
17  #!/bin/bash
18  services=("nginx" "docker" "ssh")
19  for service in "${services[@]}"
20  do
21      if systemctl is-active --quiet $service; then
22          echo "$service is running"
23      else
24          echo "$service is not running! Attempting restart..."
25          sudo systemctl restart $service
26      fi
27  done

Lessons Learned and Best Practices

After months of managing my VPS, here are the key lessons I've learned:

  • Start small: Don't try to learn everything at once
  • Document everything: Keep notes on what you install and configure
  • Test in Docker first: Containers are easier to clean up than system-wide installs
  • Automate early: If you do something twice, write a script
  • Monitor proactively: It's better to prevent problems than fix them
  • Keep it simple: Complex setups are harder to maintain
  • Learn from mistakes: Every error is a learning opportunity

Resources That Helped Me

For anyone starting their Linux journey, these resources were invaluable:

  • Linux Journey: Free online course for beginners
  • DigitalOcean Tutorials: Excellent step-by-step guides
  • The Linux Command Line book: Comprehensive reference
  • r/linuxquestions: Reddit community for help
  • Arch Wiki: Detailed documentation (works for other distros too)

My Journey with Tailscale and Headscale

As I got comfortable with basic server administration, I wanted to explore modern VPN solutions. This led me to Tailscale and eventually to self-hosting my own control server with Headscale.

Starting with Tailscale

My First Tailscale Experiencebash
1# Installing Tailscale was surprisingly simple
2  curl -fsSL https://tailscale.com/install.sh | sh
3  
4  # Starting Tailscale
5  sudo tailscale up
6  
7  # The magic - instant connectivity
8  tailscale status
9  # My laptop could now SSH to my VPS using:
10  ssh root@vps-hostname
11  
12  # No port forwarding, no firewall rules, it just worked!
13  
14  # Checking my Tailscale IP
15  ip addr show tailscale0
16  # 100.101.102.103 - my unique Tailscale IP
17  
18  # The revelation: I could access my VPS from anywhere
19  # Coffee shop WiFi? No problem
20  # Behind strict corporate firewall? Still works
21  # Mobile hotspot? Yep!

Moving to Headscale for Self-Hosting

While Tailscale was amazing, I wanted to understand the technology better and have full control over my infrastructure. Enter Headscale:

Self-Hosting with Headscalebash
1# Setting up Headscale on my Hetzner VPS
2  cd /tmp
3  wget https://github.com/juanfont/headscale/releases/latest/download/headscale_0.22.3_linux_amd64.deb
4  sudo dpkg -i headscale_0.22.3_linux_amd64.deb
5  
6  # Configuring Headscale
7  sudo nano /etc/headscale/config.yaml
8  
9  # Key configuration changes I made:
10  server_url: https://vpn.mydomain.com:8085
11  listen_addr: 0.0.0.0:8085
12  ip_prefixes:
13    - 100.64.0.0/10
14  
15  # Setting up PostgreSQL instead of SQLite
16  sudo apt install postgresql
17  sudo -u postgres createdb headscale
18  sudo -u postgres createuser headscale
19  
20  # Starting Headscale
21  sudo systemctl enable headscale
22  sudo systemctl start headscale
23  
24  # Creating my first user
25  sudo headscale users create nikolas
26  
27  # The moment of truth - connecting a client
28  tailscale up --login-server https://vpn.mydomain.com:8085
29  
30  # It failed! Time to debug...

Nginx and HTTPS Challenges

Getting HTTPS working properly with Headscale was my first major challenge:

Nginx Reverse Proxy Strugglesnginx
1# First attempt - didn't work
2  server {
3      listen 443 ssl;
4      server_name vpn.mydomain.com;
5      
6      location / {
7          proxy_pass http://localhost:8085;
8      }
9  }
10  
11  # After hours of debugging, this worked:
12  server {
13      listen 443 ssl http2;
14      server_name vpn.mydomain.com;
15      
16      ssl_certificate /etc/letsencrypt/live/vpn.mydomain.com/fullchain.pem;
17      ssl_certificate_key /etc/letsencrypt/live/vpn.mydomain.com/privkey.pem;
18      
19      location / {
20          proxy_pass http://localhost:8085;
21          proxy_http_version 1.1;
22          proxy_set_header Upgrade $http_upgrade;
23          proxy_set_header Connection "upgrade";
24          proxy_set_header Host $server_name;
25          proxy_buffering off;
26          proxy_set_header X-Real-IP $remote_addr;
27          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
28          proxy_set_header X-Forwarded-Proto $scheme;
29          proxy_redirect http:// https://;
30          proxy_read_timeout 86400;
31      }
32  }
33  
34  # The key was WebSocket support and proper headers!

DNS and DuckDNS Setup

Not having a domain initially, I discovered DuckDNS for free dynamic DNS:

Setting Up DuckDNSbash
1# Getting my free subdomain: projectvpn.duckdns.org
2  
3  # Creating update script
4  cat > ~/duckdns/duck.sh << 'EOF'
5  #!/bin/bash
6  echo url="https://www.duckdns.org/update?domains=projectvpn&token=MY_TOKEN&ip=" | curl -k -o ~/duckdns/duck.log -K -
7  EOF
8  
9  chmod 700 ~/duckdns/duck.sh
10  
11  # Adding to crontab for automatic updates
12  crontab -e
13  # Added: */5 * * * * ~/duckdns/duck.sh >/dev/null 2>&1
14  
15  # Getting Let's Encrypt certificate for DuckDNS domain
16  sudo certbot certonly --standalone -d projectvpn.duckdns.org
17  
18  # Now Headscale could use HTTPS!
19  server_url: https://projectvpn.duckdns.org

DERP Servers: The Deep Dive

Understanding DERP (Designated Encrypted Relay for Packets) servers became my next obsession. These servers handle connections when direct peer-to-peer isn't possible.

My DERP Learning Journey

Exploring DERP Serversbash
1# First, understanding what DERP does
2  tailscale netcheck
3  # Showed me which DERP regions were closest
4  
5  # Attempting to run my own DERP
6  git clone https://github.com/tailscale/tailscale.git
7  cd tailscale/cmd/derper
8  go build
9  
10  # First attempt - port conflict with Nginx
11  ./derper -hostname projectvpn.duckdns.org -a :443
12  
13  # Second attempt - different port
14  ./derper -hostname projectvpn.duckdns.org -a :8443 -c derper.crt -k derper.key
15  
16  # Testing DERP connectivity
17  curl https://projectvpn.duckdns.org:8443/derp/probe
18  
19  # Integrating with Headscale - in config.yaml:
20  derp:
21    server:
22      enabled: true
23      region_id: 999
24      region_code: "custom"
25      region_name: "My DERP"
26      stun_listen_addr: "0.0.0.0:3478"
27  
28  # But it didn't work as expected...

DERP Troubleshooting Saga

I spent days troubleshooting DERP issues. Here's what I learned:

DERP Debugging Journeybash
1# Problem 1: Certificate issues
2  # DERP clients expect valid HTTPS certificates
3  # Self-signed didn't work!
4  
5  # Solution: Use Let's Encrypt
6  ./derper -hostname projectvpn.duckdns.org -certmode letsencrypt
7  
8  # Problem 2: Firewall blocking STUN
9  # STUN needs UDP port 3478
10  sudo ufw allow 3478/udp
11  
12  # Problem 3: Nginx interference
13  # Can't proxy DERP easily - it needs WebSocket + HTTP/2
14  
15  # My working DERP behind Nginx (sort of):
16  location /derp {
17      proxy_pass http://127.0.0.1:8090;
18      proxy_http_version 1.1;
19      proxy_set_header Upgrade $http_upgrade;
20      proxy_set_header Connection "upgrade";
21      proxy_buffering off;
22  }
23  
24  # Monitoring DERP connections
25  sudo tcpdump -i any port 3478 or port 8443
26  
27  # Checking if clients use my DERP
28  tailscale status
29  # Look for "relay: self" vs "relay: fra" (Frankfurt)

Current Setup Reality

After all the experimentation, here's what actually works reliably:

What Actually Works in Productionyaml
1# Headscale config that works:
2  server_url: https://projectvpn.duckdns.org
3  listen_addr: 0.0.0.0:8085
4  
5  # Using Tailscale's DERP servers (reliable!)
6  derp:
7    server:
8      enabled: false  # Embedded DERP was unstable
9    urls:
10      - https://controlplane.tailscale.com/derpmap/default
11    auto_update_enabled: true
12  
13  # Backup everything!
14  backup_script: |
15    #!/bin/bash
16    # Backup Headscale database daily
17    systemctl stop headscale
18    cp /var/lib/headscale/db.sqlite /backup/headscale-$(date +%Y%m%d).db
19    systemctl start headscale
20    
21    # Keep last 7 days
22    find /backup -name "headscale-*.db" -mtime +7 -delete
23  
24  # Monitoring
25  prometheus_listen_addr: "127.0.0.1:9090"
26  
27  # Lessons learned:
28  # 1. Start simple - don't try custom DERP immediately
29  # 2. Tailscale's DERP servers are really good
30  # 3. Focus on getting basics working first
31  # 4. Document everything - you'll forget the details!

💬 Comments & Discussion

Share your thoughts, ask questions, or discuss this post. Comments are powered by GitHub Discussions.