It’s been a month now since I have started developing and deploying apps in my private cloud server.
The process of configuring all aspects of the server and making development in it as seamless as my local environment has been immensely humbling. At the same time though, insanely rewarding.
I walked away from the process learning a ton about networking, HTTP, databases, and so much more.
In this blog post, I will go over the entire process, so that you can do something similar when you start self-hosting.
I own a Digital Ocean server with 2GB RAM, 50GB hard disk space, and running Ubuntu 23.10. It’s one of the cheapest servers that Digital Ocean offers, costing me around $11/month.
Given it’s a cloud server, my first big decision was regarding accessing the machine from my laptop.
I chose to use SSH Keys.
SSH Keys provide a more secure way of logging into a virtual private server with SSH than using a password alone. I also find it more convenient than the need to punch in my password every time I want to access my server.
Now, logging into the virtual private server from my local machine is as simple as:
ssh -A user@<ip_address>
I also created an alias to make the process even smoother.
alias server="ssh -A user@<ip_address>"
Before starting development on my private server, it was important for me to make the shell a lookalike of my MacBook.
Given it’s a headless server — without any UI — all my interactions with this machine happen through the Shell. So, spending an extra few hours here made sense to me.
I switched from Bash to ZSH and supercharged my terminal experience by using an open-source tool called Oh My Zsh.
“Oh My Zsh” lets you choose from hundreds of plugins and themes.A couple of plugins that I want to call your attention to:
Auto Suggestions — It suggests commands as you type based on history and completions.
Syntax Highlighting — Enables highlighting of commands while they are typed. Super helpful, particularly in catching syntax errors.
With that, my Terminal and Shell experience in my private server became almost identical to that of my local machine.
When developing on my local machine, Visual Studio Code with its myriad of extensions is essential for me.
Without it, my productivity plummets.
Because of that, having a similar setup in virtual my server was a no-brainer.
Enter a combination of extensions:
With these two enabled you will find it hard to distinguish between your local machine and remote server.
With a literal click, I can add/remove/edit any files on my cloud server, as well as code inside VS Code with all my extensions enabled.
For most of my web development, I stick to only 2 frameworks:
ReactJS or NextJS — everything frontend
FastAPI or Express — everything backend
If you ever developed using these frameworks (or any other similar ones), you will know that the most common workflow is to spin up local servers on your machine, and iteratively test your feature as you develop.
I have become so familiar with this flow that, I already have a couple of tabs on my Brave Browser with localhost:3000
and localhost:5000
open.
Now, here’s the issue.
When developing on my cloud server, I don’t have the luxury of a browser. Everything I do goes through a command prompt.
Say, in my cloud server, I am running my React app in port 3000. When I open localhost:3000
on my browser, I won’t see the React app. That’s because my React app is running on the cloud machine, NOT on my local computer’s “localhost”.
How do we solve this? Enter Remote Explorer’s Port Forwarding.
Through the SSN tunnel to your cloud server, your locally running VS Code can enable port forwarding. Functionally, that means your server’s port is directly mapped to your local machine’s port.
So, whatever is running in your server’s port 3000 (in the above example), you can access it on your local machine by visiting localhost:3000
. So powerful!
Currently, I am running 4 separate apps on my cloud server, with 3 more to be deployed by the end of April.
A couple of examples:
My website built with NextJS
Newsletter management service Listmonk
Web analytics service Umami
Credit Card tracking app (coming soon)
Net Worth tracking app (coming soon)
Each of these has its private databases. Some are exposed to the public internet, and some aren’t.
Managing all these can get complicated very quickly. That’s where Docker comes in.
With Docker, I can independently deploy each app with its database and environment-specific configurations. If they need to talk to each other, I can set up intermediary APIs (I already do that in some cases…more later).
Spinning up new services, web apps, or micro services, also becomes very easy with Docker.
One downside of using Docker is all interactions are through your terminal.
The Docker documentation is really good, and the commands are fairly self-explanatory. Yet, for the more visual of us, it can be challenging.
Portainer.io can help greatly with that.
Portainer is a UI-based container management software.
In this example screenshot you can see the following:
5 docker containers running
5 images downloaded
10 volumes in use
7 custom networks defined
When clicking on any of these options, you can see a much more detailed breakdown of your containers, images, volumes, and networks.
You can also start, stop, or remove any container through Portainer’s UI.
How am I locally accessing the Portainer container that’s running on my cloud server? SSH port forwarding, as mentioned above.
Now that you have seen how I run everything using docker, the next step is to explain how I can selectively expose apps and services to the public internet.
I use Nginx as my web server and reverse proxy.
Nginx gives me a ton of flexibility when it comes to hosting apps in different subdomains. Through a simple configuration file, I can route (or proxy pass) incoming traffic to different docker containers running on my cloud server.
Let's walk through an example set up below.
NextJS Server — running on port 2000
Umami Analytics Server — running on port 3000
Listmonk Server — running on port 4000
Portainer Server — running on port 5000
When we map these ports to subdomains, this is how it looks.
irtizahafiz.com —> port 2000
umami.irtizahafiz.com —> port 3000
newsletter.irtizahafiz.com —> port 4000
portainer.irtizahafiz.com —> port 5000
I hope you can see how easy it is to selectively deploy your docker containers and host them on different subdomains.
With all that, my cloud server can easily mimic the behavior of my local machine.
Apart from serving as my development hub, it can also serve as my production host where all my apps are deployed.
Now that you have learned about the more “fun things”, let’s talk about the tech stack I have installed on this machine.
Install NodeJS through NVM
Install Python3 (also keep Python 2.7 for backward compatibility purposes)
Install ReactJS and NextJS through NPM
Install Python FastAPI using Pip3
Install and locally run Postgres in a Docker container
With that, you know about the most crucial elements of my development workflow on my private virtual server.
In future blog posts, I plan on doing deep dives on many of these topics.
If you are still reading, I hope you found this valuable.