Case Study

From Code to Containers: Chronicle Homelab Deployment

Docker → Nginx → Node.js → MongoDB → Tailscale → Homelab

1. From Application to Deployment

Chronicle started as a full-stack blogging platform built with React, Node.js, Express, and MongoDB. The application follows a simple publishing model: anyone can read published articles, while only the authenticated Chronicle administrator can create, edit, publish, and delete articles.

Once the application was working locally, I wanted to go beyond application development and learn how to package, deploy, update, troubleshoot, and recover the entire system on a Linux homelab server.

Final deployment stack:

  • React frontend served by Nginx
  • Node.js + Express backend API
  • MongoDB running as a container
  • Docker Compose for service orchestration
  • GitHub Actions for CI/CD
  • GitHub Container Registry for application images
  • Tailscale for private server connectivity
  • Docker named volume for persistent MongoDB data

2. The Final Architecture

The deployment is defined by a Docker Compose configuration. The server runs three application services: the frontend, backend, and MongoDB.

                         Docker Compose

Browser
   │
   ▼
frontend
Nginx :80
   │
   │ /api/*
   ▼
backend
Node.js :5000
   │
   │ mongodb:27017
   ▼
mongodb
   │
   ▼
Docker named volume
ServiceTechnologyResponsibility
FrontendReact + NginxServes the frontend and proxies API requests
BackendNode.js + ExpressREST API, authentication, and application logic
DatabaseMongoDBApplication and credential data

3. Containerizing the Application

Instead of installing the application runtime and dependencies directly on the server, I created Docker images for the frontend and backend. MongoDB runs from the official MongoDB image.

Docker Compose defines how these services work together. The containers share a private Docker network and communicate using Compose service names instead of hard-coded container IP addresses.

The frontend can reach the backend through the internal hostname backend, while the backend reaches MongoDB through mongodb.

MongoDB uses a named Docker volume mounted at /data/db. This keeps database data independent from the lifecycle of the MongoDB container, allowing the container to be recreated without losing the stored data.

4. Nginx as the Application Entry Point

Nginx runs inside the frontend container and acts as the public entry point for Chronicle. It serves the compiled React application and forwards requests under /api/ to the backend service.

Browser
   │
   ▼
Nginx :80
   │
   ├── /       → React application
   │
   └── /api/*  → backend:5000

This gives the browser a single public entry point. The browser does not need to know the backend container's internal address, while Docker's internal DNS handles service discovery between containers.

5. CI/CD with GitHub Actions

After the Docker deployment was working, I changed the workflow so the server no longer needed the frontend and backend source directories. GitHub Actions became responsible for building and publishing the application images.

git push
   │
   ▼
GitHub Actions
   │
   ├── Build frontend image
   ├── Build backend image
   └── Publish images
          │
          ▼
        GHCR
          │
          ▼
    Homelab server
          │
          ├── docker compose pull
          └── docker compose up -d

The application images are published to GitHub Container Registry. During deployment, the server pulls the new images and recreates the services using the Docker Compose configuration already present on the server.

This separates the application source code from the deployment environment. The server needs the Compose configuration, environment configuration, Docker, and the required secrets, while the application itself is packaged inside the images.

6. Private Homelab Access

Chronicle is hosted on a Linux homelab server. Tailscale provides private connectivity between the server and authorized devices, allowing the application and server to be accessed without exposing the homelab directly to the public internet.

The private network also provides a secure path for deployment access, allowing the deployment workflow to connect to the server through SSH over the Tailscale network.

7. Admin Credential Initialization

Chronicle uses an admin-only publishing model. Public visitors can read published articles, while only the authenticated administrator can create, edit, publish, or delete articles.

The backend includes a credential-generation script that creates the administrator credential directly in MongoDB. The script accepts a username, password, and optional role, hashes the password with bcrypt, and stores the resulting credential.

node scripts/createCredential.js <username> <password> [role]

Because the script is included in the backend image, it can be executed inside the backend container on the server. The script can then communicate with MongoDB through the Docker Compose network using the MongoDB service name.

8. Debugging a Real Deployment Failure

One automated deployment exposed a Docker networking problem. The frontend container exited because Nginx could not resolve the backend service:

host not found in upstream "backend"

I inspected the actual Docker state using commands such as docker inspect, docker network inspect, and docker compose config.

The investigation showed that the backend was attached to the expected chronicle_default network, while the failed frontend container was not attached to a Docker network.

Recreating the frontend through Docker Compose attached it to the correct network. Docker's internal DNS could then resolve backend, Nginx started successfully, and the application became available.

9. Deployment and Recovery Testing

After deployment was working, I tested the system beyond a normal successful deployment. The server was rebooted to verify that Docker services could return to a working state without manually rebuilding the application.

The MongoDB named volume preserved database data across container recreation, while Docker and Compose restored the application services after the server came back online.

This validated an important part of the deployment: the system was not only able to run successfully, but could also recover after a server restart.

10. What I Learned

  • Docker images: package application dependencies and runtime requirements so the server does not need to reproduce the development environment.
  • Docker Compose: defines how containers, networks, volumes, and service dependencies work together.
  • Container networking: service names such as backend and mongodb can be resolved through Docker's internal DNS.
  • Persistent storage: databases need storage that is independent from the lifecycle of their containers.
  • CI/CD: application images can be built and published automatically, allowing the server to deploy new versions without copying source code manually.
  • Debugging: inspecting the actual container and network state is more reliable than assuming the current Docker state matches the intended Compose configuration.
  • Reliability: reboot and recovery testing can reveal deployment problems that are invisible during a normal successful deployment.

Chronicle started as a blogging application, but the deployment process became the more valuable part of the project. It provided a practical environment for understanding how application code, containers, networking, CI/CD, storage, and a Linux server fit together as one system.