This guide was last updated in June 2020. View the full changelog.

So, you followed the advice in my Linode Hosting Review and decided to host your website with Linode. Excellent choice!

But, if you’re new to the command line (perhaps you are familiar with CPanel or Plesk?) or you’ve never setup a server from scratch before, you may be wondering what dark magic vudu is required to get up and running.

Well, you’ve come to the right place!

The Command Line Is Hard (…at first)

I’ve set up at least five new servers with Linode and each time I complete the ritual, I learn new incantations that make the Linux angels sing. I’m pretty happy with my current recipe.

Setting up a new server can be confusing, so using a tutorial like this one is a good idea the first time you do it.

Tutorial: How To Set Up Your Linode

In this guide, I will demonstrate how to set up a fresh Ubuntu server from scratch, update everything, install essential software, lock down the server to make it more resilient against basic attacks and denial-of-service, improve server stability, setup automatic backups to another server, and finally install common software like Nginx, MySQL, Python, Node, etc.

A Note About This Guide

I originally compiled this guide as a .txt file of notes for myself, but decided to share it in case anyone finds it useful. If you’re looking for something straight from the horse’s mouth, Linode also offers guides that cover how to set up a new server.

Let’s get started!

Provision a New Linode

First, you need to provision a new Linode. Using Linode’s web UI, it’s quite easy. Select your desired Linode size. If you’re unsure, choose the smallest size. You can always resize it later. You also need to select a location for the server. I usually select “Fremont, CA” since that is closest to my location and the location of most of my users (United States, west coast).

Next, let’s install an OS. Select the “Rebuild” tab. Pick “Ubuntu 18.04 LTS” for the OS image. Use 256MB as the swap disk size (it is default). You’ll be asked to create a password for the root user.

After a few minutes, your server will be ready. Now, click “Boot” to get things started!

Next, let’s connect to the server.

Connecting to Your Server

First, open Terminal on your Mac. On Windows, you’ll want to use putty or install the Windows Subsystem for Linux to get a proper Terminal.

To connect to your server, type this into your terminal and hit Enter:

ssh root@<your server ip>

Of course, replace <your server ip> with your Linode’s actual IP address, which you can find on the “Remote Access” tab in the control panel.

This command launches the SSH program and asks it to connect to your server with the username root, which is the default Ubuntu user. You will be prompted for the root password you created earlier.

Basic Ubuntu Setup

To set up your new server, execute the following commands.

Set the hostname

Set the server hostname. Any name will do — just make it memorable. In this example, I chose “future”.

echo "future" > /etc/hostname
hostname -F /etc/hostname

Let’s verify that it was set correctly:

hostname

Set the fully-qualified domain name

Set the FQDN of the server by editing the /etc/hosts file:

vim /etc/hosts

Make sure the following line is in the /etc/hosts file (after anything that’s in there by default):

<your server ip>   future.<your domain>.net       future

It is useful if you add an A record that points from some domain you control (in this case I used “future.<your domain>.net”) to your server IP address. This way, you can easily reference the IP address of your server when you SSH into it, like so:

ssh future.<your domain>.net

If you’re curious, you can read more about the /etc/hosts file.

Set the time

Set the server timezone:

dpkg-reconfigure tzdata

Verify that the date is correct:

date

Update the server

Check for updates and install:

apt update
apt upgrade

Basic Security Setup

Create a new user

The root user has a lot of power on your server. It has the power to read, write, and execute any file on the server. It’s not advisable to use root for day-to-day server tasks. For those tasks, use a user account with normal permissions.

Add a new user:

adduser <your username>

Add the user to the sudo group:

usermod -a -G sudo <your username>

This allows you to perform actions that require root privilege by simply prepending the word sudo to the command. You may need to type your password to confirm your intentions.

Login with new user:

exit
ssh <your username>@<your server ip>

Set up SSH keys

SSH keys allow you to login to your server without a password. For this reason, you’ll want to set this up on your primary computer (definitely not a public or shared computer!). SSH keys are very convenient and don’t make your server any less secure.

If you’ve already generated SSH keys before (maybe for your GitHub account?), then you can skip the next step.

Generate SSH keys

Generate SSH keys with the following command:

(NOTE: Be sure to run this on your local computer – not your server!)

ssh-keygen -t rsa -b 4096 -C "<your email address>"

When prompted, just accept the default locations for the keyfiles. Also, you’ll want to choose a nice, strong password for your key. If you’re on Mac, you can save the password in your keychain so you won’t have to type it in repeatedly.

Now you should have two keyfiles, one public and one private, in the ~/.ssh folder.

If you want more information about SSH keys, GitHub has a great guide.

Copy the public key to server

Now, copy your public key to the server. This tells the server that it should allow anyone with your private key to access the server. This is why we set a password on the private key earlier.

From your local machine, run:

scp ~/.ssh/id_rsa.pub <your username>@<your server ip>:

On your Linode, run:

mkdir .ssh
mv id_rsa.pub .ssh/authorized_keys
chown -R <your username>:<your username> .ssh
chmod 700 .ssh
chmod 600 .ssh/authorized_keys

Disable remote root login and change the SSH port

Since all Ubuntu servers have a root user and most servers run SSH on port 22 (the default), criminals often try to guess the root password using automated attacks that try many thousands of passwords in a very short time. This is a common attack that nearly all servers will face.

We can make things substantially more difficult for automated attackers by preventing the root user from logging in over SSH and changing our SSH port to something less obvious. This will prevent the vast majority of automatic attacks.

Disable remote root login and change SSH port:

sudo vim /etc/ssh/sshd_config

Set “Port” to “444” and “PermitRootLogin” to “no”. Save the file and restart the SSH service:

sudo systemctl restart ssh

In this example, we changed the port to 444. So, now to connect to the server, we need to run:

ssh <your username>@future.<your domain>.net -p 444

Update: Somone posted this useful thought about choosing an SSH port on Hacker News:

Make sure your SSH port is below 1024 (but still not 22). Reason being if your Linode is ever compromised a bad user may be able to crash sshd and run their own rogue sshd as a non root user since your original port is configured >1024. (More info here)

Advanced Security Setup

Prevent repeated login attempts with Fail2Ban

Fail2Ban is a security tool to prevent repeated failed login attempts from attackers. It works by monitoring important services (like SSH) and blocking IP addresses which appear to be malicious (i.e. they are failing too many login attempts because they are guessing passwords).

Install Fail2Ban:

sudo apt install fail2ban

Configure Fail2Ban:

Setup configuration in a new file (will overwrite defaults in /etc/fail2ban/jail.conf):

sudo vim /etc/fail2ban/jail.local

Paste the following into /etc/fail2ban/jail.local:

[DEFAULT]
destemail = <your email address>
sendername = Fail2Ban
[sshd]
enabled = true
port = 444
[sshd-ddos]
enabled = true
port = 444

(Change the port number to match whatever you used as your SSH port).

Save the file and restart Fail2Ban to put the new rules into effect:

sudo systemctl restart fail2ban

Add a firewall

We’ll add an iptables firewall to the server that blocks all incoming and outgoing connections except for ones that we manually approve. This way, only the services we choose can communicate with the internet.

The firewall has no rules yet. Check it out:

sudo iptables -L
sudo ip6tables -L

Next, we’ll install a package which enables persistent firewall rules. This means that the firewall rules will get automatically applied at server startup:

sudo apt install iptables-persistent

When prompted, agree to have the current rules installed into /etc/iptables/rules.v4 and /etc/iptables/rules.v6.

Setup the IPv4 firewall rules in /etc/iptables/rules.v4:

sudo vim /etc/iptables/rules.v4

The following firewall rules will allow HTTP (80), HTTPS (443), SSH (444 (our custom SSH port)), ping, and some ports for testing. All other ports will be blocked.

Paste the following into /etc/iptables/rules.v4:

*filter

# Accept established inbound connections
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow loopback interface traffic
-A INPUT -i lo -j ACCEPT

# Reject non-loopback interface traffic to loopback IP addresses
-A INPUT ! -i lo -d 127.0.0.0/8 -j REJECT

# Allow outbound traffic
-A OUTPUT -j ACCEPT

# Allow HTTP and HTTPS traffic
-A INPUT -p tcp -m state --state NEW --dport 80 -j ACCEPT
-A INPUT -p tcp -m state --state NEW --dport 443 -j ACCEPT

# Allow SSH connections (Note: --dport should match the port in sshd_config)
-A INPUT -p tcp -m state --state NEW --dport 444 -j ACCEPT

# Allow ping
-A INPUT -p icmp -m state --state NEW -m icmp --icmp-type 8 -j ACCEPT

# Allow test ports
-A INPUT -p tcp -m state --state NEW --dport 8080:8090 -j ACCEPT

# Log denied connections
-A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables denied: " --log-level 7

# Reject all other inbound and forward traffic (default deny unless explicitly allowed)
-A INPUT -j REJECT
-A FORWARD -j REJECT

COMMIT

Setup the IPv6 firewall rules in /etc/iptables/rules.v6:

sudo vim /etc/iptables/rules.v6

Paste the following into /etc/iptables/rules.v6:

*filter

# Accept established inbound connections
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow loopback interface traffic
-A INPUT -i lo -j ACCEPT

# Reject non-loopback interface traffic to loopback IP addresses
-A INPUT ! -i lo -d ::1/128 -j REJECT

# Allow outbound traffic
-A OUTPUT -j ACCEPT

# Allow ping
-A INPUT -p icmpv6 -m state --state NEW -m icmpv6 --icmpv6-type 8 -j ACCEPT

# Log denied connections
-A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables denied: " --log-level 7

# Reject all other inbound and forward traffic (default deny unless explicitly allowed)
-A INPUT -j REJECT
-A FORWARD -j REJECT

COMMIT

Activate the firewall rules now:

sudo iptables-restore < /etc/iptables/rules.v4
sudo ip6tables-restore < /etc/iptables/rules.v6

Verify that the rules were installed correctly:

sudo iptables -L
sudo ip6tables -L

Restart the server and confirm that the rules are still in place.

sudo shutdown -r -h +0
ssh <your username>@future.<your domain>.net -p 444
sudo iptables -L
sudo ip6tables -L

Get an email anytime a user uses sudo

I like to get an email anytime someone uses sudo. This way, I have a “paper trail” of sorts, in case anything bad happens to my server. I use a Gmail filter to file these away and only look at them occasionally.

Create a new file for the sudo settings:

sudo vim /etc/sudoers.d/my_sudoers

Add this to the file:

Defaults    mail_always
Defaults    mailto="<your email address>"

Set permissions on the file:

sudo chmod 0440 /etc/sudoers.d/my_sudoers

This is isn’t mentioned anywhere on the web, as far as I know, but in order for the “mail on sudo use” feature to work, you need to install an MTA server. sendmail is a good choice:

sudo apt install sendmail

Now, you should get an email anytime someone uses sudo!

Improve Server Stability

VPS servers can run out of memory during traffic spikes or other system events. In this situation, the server might go into “swap hell”. It’s important to configure your applications so memory swapping does not occur.

Modern servers like Nginx or Node.js use a single process to handle multiple simulateous connections, so this is less of a problem than in the past.

For example, in Apache 2.2.x (quite old), the default settings allowed 150 clients to connect simultaneously. This was way too large a number for a typical small VPS server. Let’s do the math. Apache’s processes were typically ~25MB each. If the website got a temporary traffic spike and 150 processes launched, we’d need 3750MB of memory on the server. If we don’t have this much, then the OS will grind to a halt as it swaps memory to disk to make room for new processes, but then immediately swaps the stuff on disk back into memory. This is also known as “swap hell”.

No useful work gets done once “swap hell” occurs. The server can be stuck in this state for hours, even after the traffic rush has subsided. During this time, very few web requests will get serviced.

If you’re still using the ancient Apache 2.2.x for some reason, you could set MaxClients to something more reasonable like 20 or 30 clients. There are many other optimizations to make, too. Linode has a Library article with optimizations for various server types.

Newer version of Apache (2.4 and up) use an “event based mpm” instead of Apache 2.2 ineffecient “prefork” approach. This is far less of a problem with the improved approach.

And of course, servers like Nginx and Node.js handle thousands of connections without making a new process for each connection.

Reboot server on out-of-memory condition

In cases where something goes awry, it is good to automatically reboot your server when it runs out of memory. This will cause a minute or two of downtime, but it’s better than languishing in the swapping state for potentially hours or days.

You can leverage a couple kernel settings and Lassie to make this happen on Linode.

Adding the following two lines to your /etc/sysctl.conf will cause it to reboot after running out of memory:

vm.panic_on_oom=1
kernel.panic=10

The vm.panic_on_oom=1 line enables panic on OOM; the kernel.panic=10 line tells the kernel to reboot ten seconds after panicking.

Read more about rebooting when out of memory on Linode’s wiki.

Miscellaneous nice-to-haves

These next things are not required but are nice to do.

Set up reverse DNS

The reverse DNS system allows one to determine the domain name that lives at a given IP address. This is useful for network troubleshooting — (ping, traceroute, etc.), as well as email anti-spam measures (read more on Wikipedia).

It’s pretty easy to set up. From the Linode Manager, select your Linode, click on “Remote Access”, then click on “Reverse DNS” (under “Public IPs”). Type in your domain and that’s it!

Set up a private IP address

Private IPs are useful for communicating data on the Linode network, i.e. Linode to Linode. This is handy if you have multiple Linodes (say, one for your web server and one for your database). Private network traffic is more secure (only other Linode customers can see it, vs. the whole internet), faster (the traffic never has to leave the datacenter if both Linodes are in the same datacenter), and free (doesn’t count towards your monthly bandwidth quota).

I currently put my database server on its own Linode, so that I can scale it independently of my frontend servers and debug performance issues easier since the systems are isolated. This hasn’t been super-handy yet, but if one of my sites gets a huge traffic rush, I bet it will be immensely useful.

It’s easy to set up. On the Remote Access tab, click Add a Private IP.

Then, just restart the Linode and the new IP address will become available thanks to Linode Network Helper which automatically deposits a static networking configuration in to your Linode at boot.

Configuring your applications and your database to route traffic over the local network is another issue, not covered here.

Install Useful Server Software

At this point, you have a pretty nice server setup. Congrats! But, your server still doesn’t do anything useful. Let’s install some software.

Install a compiler

A compiler is often required to install Python packages and other software, so let’s just install one up-front.

sudo apt install build-essential

Install Nginx

sudo apt install nginx

Install Node.js

Follow the instructions to install the NodeSource Node.js PPA.

Install MySQL

Install MySQL:

sudo apt install mysql-server libmysqlclient-dev

Set root password when prompt asks you.

Verify that MySQL is running.

sudo netstat -tap | grep mysql

For connecting to MySQL, instead of the usual PHPMyAdmin, I now use Sequel Pro, a free app for Mac.

Improve MySQL security

Before using MySQL in production, you’ll want to improve your MySQL installation security. Run:

mysql_secure_installation

This will help you set a password for the root account, remove anonymous-user accounts, and remove the test database.

Keep your MySQL tables in tip-top shape

Over time your MySQL tables will get fragmented and queries will take longer to complete. You can keep your tables in top shape by regularly running OPTIMIZE TABLE on all your tables. But, since you’ll never remember to do this regularly, we should set up a cron job to do this.

Open up your crontab file:

crontab -e

Then, add the following line:

@weekly mysqlcheck -o --user=root --password=<your password here> -A

Also, you can try manually running the above command to verify that it works correctly.

Backup your MySQL databases

The excellent automysqlbackup utility can automatically make daily, weekly, and monthly backups of your MySQL database.

Install it:

sudo apt install automysqlbackup

Now, let’s configure it. Open the configuration file:

sudo vim /etc/default/automysqlbackup

By default, your database backups get stored in /var/lib/automysqlbackup which isn’t very intuitive. I recommend changing it to a folder within your home directory. To do this, find the line that begins with BACKUPDIR= and change it to BACKUPDIR="/home/<your username>/backups/mysql"

You also want to get an email if an error occurs, so you’ll know if automatic backups stop working for some reason. Find the line that begins with MAILADDR= and change it to MAILADDR="<your email address>".

Close and save the file. That’s it!

Setup Automatic Backups

Backups are really important. Linode offers a paid backup service that’s really convenient if you accidentally destroy something and need to restore your Linode quickly. It’s $5 per month for the smallest Linode. I enable it on all my Linodes.

If you want even more peace of mind (or don’t want to pay for Linode’s backup service) you can roll your own simple backup solution using rsync.

You will need access to another Linux server (maybe another Linode?) or a home server. I just installed Ubuntu on an old desktop computer to use as a backup server.

We’re going to create a weekly cronjob that backs up our Linode’s home directory to a backup server. I keep all the files that I would want to backup in my home folder, so this works for me.

Open your crontab:

crontab -e

Add this line to the file:

@weekly rsync -r -a -e "ssh -l <your username on backup server> -p <ssh port number of backup server>" --delete /home/<your username> <hostname or ip address of backup server>:/path/to/some/directory/on/backup/server

I recommend running the above command manually to make sure you have it right before adding it to your crontab file.

That’s it!

Linode rocks!

If, after reading this, you want to sign up for Linode, use this link and I’ll get a couple weeks of free hosting. If you prefer not to, here’s the plain link: Linode.com

Happy hacking!



You can discuss, upvote, or poke fun at this post over at Hacker News.

(If you liked this, you might like Freedom of Speech on the Internet.)

Thanks for reading! RSS Feed Icon

Feross Aboukhadijeh

I'm Feross, an entrepreneur, programmer, open source author, and mad scientist.

I maintain 100+ packages on npm. All my code is freely accessible on GitHub and funded by my supporters. I now offer a support contract for companies and teams.

I build innovative projects like WebTorrent, a streaming torrent client for the web, WebTorrent Desktop, a slick torrent app for Mac/Windows/Linux, and StandardJS, a JavaScript style guide, linter, and automatic code fixer.

I also work on fun projects like BitMidi, a free MIDI file database, Play, a music video app, and Study Notes, a study tool with college essay examples.

If you enjoyed this post, you should follow me on Twitter.

Or, sign up to get an email whenever I write a post: