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 all2ssh root@49.13.80.27
34# 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)?
89 # 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 management2pwd# Where am I?3ls -la # What's here? (show hidden files too)4cd /var/log # Go to log directory5mkdir projects # Create a directory6rm -rf old_folder # Remove directory (careful with this!)78# File operations9touch newfile.txt # Create empty file10nano newfile.txt # Edit file (beginner-friendly editor)11cat file.txt # Display file contents12grep"error" log.txt # Search for text in files13chmod755 script.sh # Change file permissions1415# System information16df -h # Disk space usage17free -m # Memory usage18top# Running processes (press 'q' to quit)19ps aux # List all processes20uname -a # System information2122# Package management (Ubuntu/Debian)23apt update # Update package list24apt upgrade # Upgrade installed packages25aptinstall nginx # Install new software26apt remove nginx # Remove software2728# Network commands29ip addr # Show network interfaces30ping google.com # Test connectivity31netstat -tlnp # Show listening ports32curl ifconfig.me # Get public IP address3334# User management35 adduser nikolas # Create new user36passwd nikolas # Change password37su - nikolas # Switch user38exit# 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!2chmod777 /
34# What I meant to do:5chmod777 ./myscript.sh
67# 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 user2 adduser nikolas
3usermod -aG sudo nikolas
45# 2. Set up SSH key authentication6# On local machine:7 ssh-keygen -t ed25519 -C "nikolas@email.com"89# Copy public key to server:10 ssh-copy-id nikolas@49.13.80.27
1112# 3. Disable root login and password authentication13sudonano /etc/ssh/sshd_config
14# Set these values:15# PermitRootLogin no16# PasswordAuthentication no17# PubkeyAuthentication yes1819# Restart SSH service20sudo systemctl restart ssh2122# 4. Set up firewall23sudo ufw allow 22/tcp # SSH24sudo ufw allow 80/tcp # HTTP25sudo ufw allow 443/tcp # HTTPS26sudo ufw enable2728# 5. Install fail2ban to prevent brute force attacks29sudoaptinstall fail2ban
30sudo systemctl enable fail2ban
3132# 6. Keep system updated33sudoapt update &&sudoapt upgrade -y
3435# 7. Set up automatic security updates36sudoaptinstall unattended-upgrades
37sudo 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 Nginx2sudoaptinstall nginx
34# Check if it's running5sudo systemctl status nginx
67# Enable it to start on boot8sudo systemctl enable nginx
910# Create a simple website11sudomkdir -p /var/www/mysite
12sudonano /var/www/mysite/index.html
1314# Configure Nginx15sudonano /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}2324# Enable the site25sudoln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
26sudo nginx -t # Test configuration27sudo 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 Docker2curl -fsSL https://get.docker.com -o get-docker.sh
3sudosh get-docker.sh
45# Add user to docker group (avoid using sudo)6sudousermod -aG docker$USER78# Test Docker9docker run hello-world
1011# Run a real application12docker run -d -p 8080:80 --name my-nginx nginx
1314# See running containers15dockerps1617# Check logs18docker logs my-nginx
1920# Stop and remove container21docker stop my-nginx
22dockerrm my-nginx
2324# Docker Compose for complex setups25sudoaptinstalldocker-compose2627# Create docker-compose.yml28 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 resources2htop# Interactive process viewer (install with: sudo apt install htop)34# Monitor disk usage5df -h
6du -sh /var/log/* # Find large files in logs78# Check service status9 systemctl status nginx
10 systemctl status docker1112# View recent logs13 journalctl -xe
14 journalctl -u nginx -f # Follow nginx logs1516# Set up log rotation17sudonano /etc/logrotate.d/myapp
1819# Basic monitoring script20#!/bin/bash21echo"=== System Health Check ==="22echo"Memory Usage:"23free -m
24echo -e "
25Disk Usage:"26df -h
27echo -e "
28Top 5 CPU Processes:"29ps aux --sort=-%cpu |head -6
3031# Make it executable and schedule with cron32chmod +x health_check.sh
33crontab -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 script2#!/bin/bash3BACKUP_DIR="/home/nikolas/backups"4DATE=$(date +%Y%m%d_%H%M%S)56# Create backup directory7mkdir -p $BACKUP_DIR89# Backup important files10tar -czf $BACKUP_DIR/config_$DATE.tar.gz /etc/nginx /etc/ssh
11tar -czf $BACKUP_DIR/data_$DATE.tar.gz /var/www
1213# Backup Docker volumes14docker run --rm -v myapp_data:/data -v $BACKUP_DIR:/backup alpine tar czf /backup/docker_data_$DATE.tar.gz /data
1516# Keep only last 7 days of backups17find$BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
1819# 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 script2#!/bin/bash3echo"Starting system update..."4sudoapt update
5sudoapt upgrade -y
6sudoapt autoremove -y
7sudoapt autoclean
8echo"Update complete!"910# Docker cleanup script11#!/bin/bash12echo"Cleaning up Docker..."13docker system prune -af --volumes
14echo"Docker cleanup complete!"1516# Service health check17#!/bin/bash18services=("nginx""docker""ssh")19forservicein"${services[@]}"20do21if systemctl is-active --quiet $service;then22echo"$service is running"23else24echo"$service is not running! Attempting restart..."25sudo systemctl restart $service26fi27done
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:
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 simple2curl -fsSL https://tailscale.com/install.sh |sh34# Starting Tailscale5sudo tailscale up
67# The magic - instant connectivity8 tailscale status
9# My laptop could now SSH to my VPS using:10ssh root@vps-hostname
1112# No port forwarding, no firewall rules, it just worked!1314# Checking my Tailscale IP15ip addr show tailscale0
16# 100.101.102.103 - my unique Tailscale IP1718# The revelation: I could access my VPS from anywhere19# Coffee shop WiFi? No problem20# Behind strict corporate firewall? Still works21# 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 VPS2cd /tmp
3wget https://github.com/juanfont/headscale/releases/latest/download/headscale_0.22.3_linux_amd64.deb
4sudo dpkg -i headscale_0.22.3_linux_amd64.deb
56# Configuring Headscale7sudonano /etc/headscale/config.yaml
89# 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
1415# Setting up PostgreSQL instead of SQLite16sudoaptinstall postgresql
17sudo -u postgres createdb headscale
18sudo -u postgres createuser headscale
1920# Starting Headscale21sudo systemctl enable headscale
22sudo systemctl start headscale
2324# Creating my first user25sudo headscale users create nikolas
2627# The moment of truth - connecting a client28 tailscale up --login-server https://vpn.mydomain.com:8085
2930# 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 work2server{3listen443 ssl;4server_name vpn.mydomain.com;56location /{7proxy_pass http://localhost:8085;8}9}1011# After hours of debugging, this worked:12server{13listen443 ssl http2;14server_name vpn.mydomain.com;1516ssl_certificate /etc/letsencrypt/live/vpn.mydomain.com/fullchain.pem;17ssl_certificate_key /etc/letsencrypt/live/vpn.mydomain.com/privkey.pem;1819location /{20proxy_pass http://localhost:8085;21proxy_http_version 1.1;22proxy_set_header Upgrade $http_upgrade;23proxy_set_header Connection "upgrade";24proxy_set_header Host $server_name;25proxy_bufferingoff;26proxy_set_header X-Real-IP $remote_addr;27proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;28proxy_set_header X-Forwarded-Proto $scheme;29proxy_redirect http:// https://;30proxy_read_timeout86400;31}32}3334# 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.org23# Creating update script4cat> ~/duckdns/duck.sh <<'EOF'5#!/bin/bash6echourl="https://www.duckdns.org/update?domains=projectvpn&token=MY_TOKEN&ip="|curl -k -o ~/duckdns/duck.log -K -
7 EOF
89chmod700 ~/duckdns/duck.sh
1011# Adding to crontab for automatic updates12crontab -e
13# Added: */5 * * * * ~/duckdns/duck.sh >/dev/null 2>&11415# Getting Let's Encrypt certificate for DuckDNS domain16sudo certbot certonly --standalone -d projectvpn.duckdns.org
1718# 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 does2 tailscale netcheck
3# Showed me which DERP regions were closest45# Attempting to run my own DERP6git clone https://github.com/tailscale/tailscale.git
7cd tailscale/cmd/derper
8 go build
910# First attempt - port conflict with Nginx11 ./derper -hostname projectvpn.duckdns.org -a :443
1213# Second attempt - different port14 ./derper -hostname projectvpn.duckdns.org -a :8443 -c derper.crt -k derper.key
1516# Testing DERP connectivity17curl https://projectvpn.duckdns.org:8443/derp/probe
1819# Integrating with Headscale - in config.yaml:20 derp:
21 server:
22 enabled: true23 region_id: 99924 region_code: "custom"25 region_name: "My DERP"26 stun_listen_addr: "0.0.0.0:3478"2728# 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 issues2# DERP clients expect valid HTTPS certificates3# Self-signed didn't work!45# Solution: Use Let's Encrypt6 ./derper -hostname projectvpn.duckdns.org -certmode letsencrypt
78# Problem 2: Firewall blocking STUN9# STUN needs UDP port 347810sudo ufw allow 3478/udp
1112# Problem 3: Nginx interference13# Can't proxy DERP easily - it needs WebSocket + HTTP/21415# 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}2324# Monitoring DERP connections25sudo tcpdump -i any port 3478 or port 84432627# Checking if clients use my DERP28 tailscale status
29# Look for "relay: self" vs "relay: fra" (Frankfurt)
Current Setup Reality
After all the experimentation, here's what actually works reliably:
💬 Comments & Discussion
Share your thoughts, ask questions, or discuss this post. Comments are powered by GitHub Discussions.
💡 Tip: You need a GitHub account to comment. This helps reduce spam and keeps discussions high-quality.