My VPS was a mess. 25GB of storage, half of it node_modules. Builds failing because swap ran out. Apache configs I had to look up every time. This is how I fixed all of it in a weekend.
Why?
On the quest for upskilling and learning to do things the proper way this year, learning how to use Docker has been at the top of my list. I have owned a DigitalOcean droplet running Ubuntu for a couple of years now, mostly only containing running web applications.
This droplet managed multiple Node.js processes through PM2, using Apache as a reverse proxy. When deploying and managing these applications, a few pain points stood out:
- Application builds were often done on the server. With the lack of droplet memory, swap memory was required. On especially large projects, I still ran out of memory.
- I only have 25 GB of storage, which filled up quickly due to multiple projects containing a
node_modulesdirectory. - Deploy scripts were often messy, with a combination of
cd,mv, and PM2 commands. - Managing Apache configs was tedious at best, with a lot of directories and commands to remember. I utilized custom aliases to relieve some of the work.
While I went into this only wanting to learn Docker and deploy my first project doing so, I inadvertently solved all of my pain points.
The Migration
The first thing I did was install Docker, which was relatively straightforward. With this came learning Docker's terminology such as what containers and images are, what Dockerfile and docker-compose.yml are and how they work, and various commands to manage these containers.
Locally, I created a Dockerfile in my new project and configured Next.js to generate a standalone export. The most affordable way to get my built images was using GitHub's Packages feature. Using GitHub's Packages with Docker completely circumvents having to build projects on my server and provides an easy way to rollback to previous versions.
After publishing the image, running it on my server was incredibly simple with just two commands: docker compose pull and docker compose up -d.
While I initially installed Nginx as a service, I later installed Nginx Proxy Manager as a Docker container to manage my Nginx configs visually through a UI. It came with several benefits:
- Adding a new config is greatly simplified, replacing an old workflow which included cloning configs with file system actions and process restarts.
- I can easily tell which applications are running on which port.
- New SSL certificates can be requested in-app instead of using Certbot's CLI.
After adding all my existing live Apache configs into Nginx running on a temporary port, switching over was simple and reversible by changing the port Nginx listened on. After verifying all migrated proxies worked, Apache was removed from the server.
Due to the tight resource constraints, this droplet is primarily used for running web applications in production. However, I also decided to install two other "nice-to-have" services as Docker containers: Portainer and Uptime Kuma.
- Portainer gave me a GUI to quickly view my containers their statuses, as well as the ability to manage these containers without touching the terminal, a direct equivalent to using PM2.io.
- I also installed Uptime Kuma to publicly visually track which processes are running and their status. Interestingly, it provides the possibility of using Discord Webhooks for notifications, which is something I'll definitely set up in the future.
The result: build locally, push to GHCR, pull and run on the server in two commands, manage proxy configs in a snap. A much simpler and pain-free workflow.
What's next?
Docker does a lot more than what I've explored in my short two days of learning, but will play a part in some of my future plans:
- Learn what Redis is and how to use BullMQ with it, integrate with Docker.
- Utilize Infisical to manage secrets, instead of
.env. - Learn how to automate Docker deployments using GitHub actions.
- Migrate existing PM2 processes to their own Docker containers.
- Add Discord Webhook notifications from Uptime Kuma.