How I Set Up My Private Cloud Server for Development

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.

Accessing Dev Server From Local

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>"

Shell Customization

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.

server zsh customized
“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.

Starting Development with Remote SSH

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.

Port Forwarding

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.

Port Forwarding in VS Code

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!

Containerize Everything — Docker

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.

Managing Docker Containers — Portainer

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. can help greatly with that.

Portainer is a UI-based container management software.

Portainer Dashboard

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.

Web Server & Reverse Proxy — Nginx

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. —> port 2000 —> port 3000 —> port 4000 —> port 5000

I hope you can see how easy it is to selectively deploy your docker containers and host them on different subdomains.

The Boring Stuff — Tech Stack

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

Closing Thoughts

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.

Irtiza Hafiz