Nathan Clayton

Back to blog

From Idea to Production: Building a Self-Hosted Rails Portfolio on a VPS

I wanted to build a personal platform that felt simple, maintainable, and fully my own. What started as curiosity around Ruby on Rails became a self-hosted portfolio and blogging platform built with Rails 8, PostgreSQL, Hotwire, Tailwind, GitHub Actions, and a production deployment running entirely on a personal VPS.

Why I Started This Project

This project started with a few simple goals:

  • build a personal space on the internet again
  • explore Ruby on Rails and its developer experience
  • create something intentionally simple and maintainable
  • understand modern Rails deployment from end to end
  • build a platform that felt personal instead of overly corporate

I did not want to hide everything behind a platform-as-a-service or abstract away how the application actually runs in production. I wanted to understand the full lifecycle of the application — from local development to deployment infrastructure.

What started as a small experiment eventually became a full portfolio and blogging platform.


Choosing the Stack

The technology choices were intentionally straightforward.

Application Layer

The application itself is built with:

  • Ruby on Rails 8
  • PostgreSQL
  • Hotwire + Stimulus
  • Tailwind CSS
  • Redcarpet for Markdown rendering
  • Devise for authentication

Infrastructure Layer

For infrastructure and deployment:

  • Ubuntu VPS
  • Puma application server
  • Caddy reverse proxy with automatic HTTPS
  • systemd for service management
  • GitHub Actions for CI/CD

The goal was not to build the most complex architecture possible. The goal was to build something understandable, maintainable, and production-ready.


Building the Application

One of the things I appreciated most while building this project was how productive Rails feels when you stay close to its conventions.

The application started with the standard Rails defaults using PostgreSQL, and I intentionally avoided heavily customizing the framework early on.

Rails already provides strong foundations for:

  • authentication
  • database migrations
  • asset management
  • production configuration
  • health checks
  • application structure

Instead of spending time wiring together infrastructure and framework decisions, I could focus directly on building features.

What surprised me most was how quickly momentum builds once the foundations are in place. This entire experiment started on a Friday and by Sunday evening the application was deployed publicly on a VPS with automated deployments, authentication, an admin dashboard, and a functioning blog platform.


Designing the Site

The visual design of the site stayed intentionally minimal.

I wanted the platform to feel clean, technical, and readable without relying on heavy UI frameworks or excessive animation.

Tailwind CSS made it easy to create reusable styling patterns for:

  • cards
  • typography
  • sections
  • layouts
  • navigation components

The end result is a dark-themed portfolio and blog that feels lightweight and focused on content rather than decoration.


Building the Blog System

The blog platform itself became one of the most enjoyable parts of the project.

Posts are written entirely in Markdown and rendered with custom typography styling optimized for long-form technical writing.

The renderer supports:

  • headings
  • tables
  • blockquotes
  • code blocks
  • lists
  • inline formatting

The goal was to create a writing workflow that stays simple while still producing clean, readable technical articles.


Admin Features

The application also includes an authenticated admin experience powered by Devise.

Instead of wiring up email infrastructure immediately, contact submissions are stored internally through a lightweight inbox system inside the application itself.

The admin tooling supports:

  • publishing blog posts
  • reviewing contact submissions
  • managing content
  • authenticated administrative access

Keeping these tools built directly into the platform helped keep the project self-contained and easy to manage.


Development Environment

The project also became an opportunity to standardize a more Linux-native development workflow.

Development moved entirely into WSL, with Rails, PostgreSQL, and Ruby tooling all running inside Linux.

Most of my development happens on Windows while sitting at my desk, using WSL to provide a full Linux-based Ruby environment. The same workflow also transitions cleanly onto my laptop running Pop!_OS, which made it easy to keep a consistent development experience across both machines.

The environment includes:

  • WSL
  • Pop!_OS on laptop development
  • Cursor as the primary editor
  • mise for Ruby version management
  • PostgreSQL running locally in Linux

Once configured, the workflow felt extremely smooth and very close to the eventual production environment.


Automated Quality Checks

CI/CD and automated validation were added early in the project rather than being treated as an afterthought.

GitHub Actions was configured to automatically run:

  • RuboCop
  • Brakeman
  • Bundler Audit
  • Rails test suites

Adding these checks early helped establish a cleaner development workflow and made deployments significantly more reliable later on.


Planning for Production

Even though this started as a personal project, I wanted the production setup to feel structured and maintainable.

The deployment model intentionally stayed simple:

  • VPS
  • Rails
  • PostgreSQL
  • reverse proxy
  • automated deployments

The VPS layout was also designed so additional small applications and subdomains could eventually live alongside this project cleanly.

Environment variables and secrets were separated from the repository, and deployment access was isolated through dedicated Linux users and SSH keys.


Deploying to Production

The production environment runs on an Ubuntu VPS configured with:

  • PostgreSQL
  • Ruby
  • Caddy
  • fail2ban

Rails runs through Puma and is managed with systemd.

Caddy handles reverse proxy routing and automatic HTTPS certificates, which made securing the application surprisingly straightforward.

Deployments are fully automated through GitHub Actions using a push-to-deploy workflow tied to the main branch.

Each deployment handles:

  • pulling the latest code
  • installing dependencies
  • running database migrations
  • precompiling assets
  • restarting services
  • verifying application health

The result is a fully self-hosted Rails platform with automated production deployments running on a personal VPS.


Future Improvements

The platform is intentionally small right now, but there are still several areas I would like to expand over time.

One of the biggest missing pieces is media support for blog content. Right now the platform is heavily text-focused, but adding object storage for image uploads would make architecture diagrams, walkthroughs, and richer technical posts much easier to publish while still keeping the deployment self-hosted and lightweight.

Content organization is another area that will likely evolve as the number of posts grows. Features like:

  • tags and topic organization
  • related post recommendations
  • full-text search
  • improved content discovery

would make the blog easier to navigate long-term without dramatically increasing complexity.

I would also like to spend more time improving operational visibility around the platform itself. Adding lightweight observability through open-source tooling for metrics, logging, uptime monitoring, and error tracking feels like a perfect future weekend project.

For now though, keeping the platform simple is still the priority.


Final Result

What started as a simple Rails experiment evolved into:

  • a personal portfolio
  • a technical blog
  • an admin dashboard
  • a contact inbox
  • a production deployment pipeline
  • a self-hosted application platform

More than anything, the project reinforced how enjoyable it can be to build and operate a straightforward application stack from end to end.

Owning the entire platform — from the Rails application itself to the production infrastructure — made the project feel far more complete than just building another website.

Are you sure?