Skip to main content
Back to Projects
Screenshot of Dockerized Turborepo Pipeline

Dockerized Turborepo Pipeline

Open sourceLearning project · DevOps

A hands-on project for learning CI/CD and containerisation end to end. It packages each service of a small Turborepo monorepo as its own Docker image, then uses GitHub Actions to build the images, push them to Docker Hub and deploy them over SSH to a Linux VM, where Nginx serves each one on its own subdomain over HTTPS. The apps are deliberately minimal; the pipeline and the infrastructure are the project.

GitHub Actions pipelines, one per service
3
commit-tagged images on Docker Hub
:<sha>
containers on one private Docker network
4

Tech stack

  • Docker
  • Docker Compose
  • GitHub Actions
  • Docker Hub
  • Nginx
  • Let's Encrypt (Certbot)
  • Linux VM (SSH)
  • Turborepo
  • pnpm
  • PostgreSQL
  • Prisma

Overview

The goal was to learn how code gets from a git push to a running server without a hosting platform doing the work. To have something realistic to ship, the repository is a Turborepo monorepo with three minimal sample services (a Next.js page, an Express API with sign-up and sign-in, and a WebSocket server) that share one PostgreSQL database through a Prisma package. The services exist to be containerised and deployed; they are not the focus.

Everything around them was set up by hand: a Dockerfile for each service, Docker Compose files for local and production runs, a GitHub Actions workflow per service that builds and pushes an image to Docker Hub, and an SSH deploy step that pulls the new image onto a Linux VM and recreates the container. On the VM, Nginx routes a subdomain to each container and Certbot provides the HTTPS certificates.

What I set up

  • A Dockerfile for each service (web, HTTP API and WebSocket server), all built from one shared pnpm workspace
  • Docker Compose files for local development and for production, with every container on one private network
  • A PostgreSQL 17 container with a named volume, so data survives restarts and redeploys
  • One GitHub Actions workflow per service that runs only when that service, its Dockerfile or the shared packages change
  • Image publishing to Docker Hub with two tags: the commit SHA and latest
  • Automated deploys over SSH that pull the exact commit's image and recreate the container on the VM, with the Docker Hub and SSH credentials stored as GitHub Actions secrets
  • An Nginx reverse proxy with a subdomain per service, WebSocket upgrades and Let's Encrypt HTTPS through Certbot

Technical breakdown

The CI/CD pipeline

Each workflow checks out the repository, logs in to Docker Hub, builds the service's image with docker/build-push-action and pushes it as turbo-<service>:<sha> and :latest. A final step connects to the VM over SSH, pulls the SHA-tagged image, stops and removes the old container and starts the new one on the shared network. Path filters keep the three pipelines independent: a frontend change ships only the frontend, while a change under packages/** rebuilds all three services.

Containers and runtime

The Dockerfiles copy the workspace manifests and lockfile before the source, so the pnpm install layer stays cached until dependencies change. The Prisma client is generated at build time against a placeholder DATABASE_URL and the real one is supplied at run time. In Compose, services reach the database by name (postgres:5432) over the turbo-net network and wait for a pg_isready health check before starting, and the API runs prisma migrate deploy on startup so the schema is always current. Nginx forwards the Upgrade and Connection headers, so WebSocket traffic passes through the same proxy as HTTP.

Problems I worked through

  1. 01

    Rebuilding only what changed

    In a monorepo, a naive pipeline rebuilds and redeploys everything on every push. Filtering each workflow on its own app folder, its Dockerfile and packages/** made a frontend change ship only the frontend, while a change to the shared Prisma or UI package still rebuilds all three services.

  2. 02

    Knowing exactly what is deployed

    Deploying :latest makes it impossible to tell which commit is running. The deploy step pulls the image tagged with the commit SHA that triggered the workflow, so every container maps to one commit and rolling back means redeploying an earlier tag.

  3. 03

    Startup order and migrations

    Containers that start before Postgres is ready crash or fail their first queries. A pg_isready health check now gates the application containers, and running prisma migrate deploy in the API's start command keeps the schema current on every deploy without a manual step.

  4. 04

    Prisma inside Docker builds

    prisma generate needs a DATABASE_URL, but no database exists during docker build. A placeholder build argument lets the client generate, while the real connection string is only provided when the container runs.

  5. 05

    WebSockets behind a reverse proxy

    A plain reverse proxy breaks WebSocket connections at the upgrade handshake. Each Nginx server block uses HTTP/1.1 and forwards the Upgrade and Connection headers, so one proxy carries both HTTP and WebSocket traffic, with Certbot managing the certificates.

What I learned

  • How Docker layer caching works, and why manifests are copied before the source
  • The difference between build-time and run-time configuration, and how to keep secrets out of images
  • Container networking: services talk to each other by name on a shared network, not through localhost
  • Health checks, not sleeps, should decide startup order
  • Path filters keep monorepo pipelines fast and deploys small
  • Tag images by commit SHA; latest is a convenience, not a deploy target
  • How a reverse proxy, subdomains and TLS certificates fit together on a single VM

Next steps

  • Multi-stage Docker builds to shrink the images
  • Zero-downtime (blue-green) deploys instead of stop-then-start
  • Turbo remote caching to speed up builds
  • A staging environment ahead of production
  • Container monitoring and logs with Prometheus and Grafana
  • Infrastructure as code with Terraform

More projects

Screenshot of LighterPulse

Analytics hub for Lighter.xyz: dashboards, a block explorer, exchange stats and cross-venue funding comparison.

  • Next.js
  • TypeScript
  • Tailwind CSS
Live
Screenshot of Lighter Liquidations

Backend service

Lighter Liquidations

Streaming pipeline that ingests Lighter liquidations, dedupes replayed batches and serves a rolling 24h total.

  • Node.js
  • WebSockets (Lighter streams)
  • Upstash Redis
Open source
View all projects