Incident timeline

    ← All articles

    Guide

    How to deploy a monorepo with multiple services (frontend, API and database)

    Deploy a monorepo as one connected unit: describe each service once, link them with generated URLs, and let dependencies decide the deploy order.

    · 3 min read

    In short

    • Describe every service once in a single prongo.yaml
    • depends_on sets the deploy order, {{ api.url }} links the services
    • pre_deploy runs migrations before the new version goes live

    A monorepo with a frontend, an API and a database is easy to run on your laptop and surprisingly awkward to deploy. Most hosting platforms assume one repository means one app. The moment you have three services that depend on each other, you are suddenly managing deploy order, internal URLs and shared secrets by hand.

    This guide shows the general approach, and then how it looks with a monorepo-aware platform where the whole repo is the unit of deployment.

    What makes a monorepo hard to deploy

    Three things go wrong again and again:

    1. Order. The API cannot start before its database is reachable, and the frontend should not go live before the API it calls. Starting everything at once (“it will retry”) works until it doesn’t.
    2. Discovery. The frontend needs the API’s address. In production that address is generated, so hardcoding http://localhost:8080 stops working the moment you leave your machine.
    3. Secrets. The same DATABASE_URL is needed by the API and a worker. Copy-pasting it into three dashboards is how credentials drift and leak.

    Everything below is about making those three problems disappear.

    Step 1: give every service a Dockerfile

    Each service becomes a container, built from its own folder. A typical layout:

    shop/
    ├── apps/
    │   ├── api/        # Dockerfile, listens on 8080
    │   └── web/        # Dockerfile, listens on 3000
    └── prongo.yaml

    The database does not need a Dockerfile. It is a ready-made image such as postgres:16.

    Step 2: describe the whole system in one file

    Instead of one config per service, write one file at the repo root:

    project: shop
    
    services:
      database:
        image: postgres:16
        port: 5432
        env:
          POSTGRES_DB: shop
    
      api:
        build: ./apps/api
        port: 8080
        depends_on: [database]
        pre_deploy: [npm run migrate]
    
      web:
        build: ./apps/web
        port: 3000
        depends_on: [api]
        env:
          API_URL: "{{ api.url }}"

    Three ideas carry the whole file:

    • depends_on declares order. The platform deploys database first, waits until it runs, then api, then web.
    • {{ api.url }} is service discovery. It is replaced with the real internal address at deploy time.
    • pre_deploy runs your migrations after the dependencies are up and before the new version receives traffic. See running migrations on every deploy for how to write them safely.

    The full list of fields is in the prongo.yaml reference.

    1 · database image: postgres:16 2 · api pre_deploy: migrate 3 · web API_URL = api.url depends_on depends_on
    Each service waits for the one it depends on, so the order comes from the config.

    Step 3: keep secrets out of the file

    The yaml lives in git, so passwords and tokens must not. Set them once in the dashboard and they are injected into the services as environment variables with the same name. More on this in sharing environment variables between services.

    Step 4: push

    With the file committed, a push builds every service, deploys them in dependency order, and starts them with zero downtime. If a pre_deploy command fails, the deploy fails and the previous version keeps running, so a broken migration does not take your site down.

    What this replaces

    Doing the same on Docker Compose gives you no readiness-aware ordering (depends_on only controls start order) and a single machine. Doing it on Kubernetes means separate Deployment, Service, Ingress and Secret manifests for every service. The side-by-side is on the comparison page.

    When this approach is not the right fit

    If your repo is one static site or one Next.js app, any host works and you do not need this. A monorepo platform pays off when you have two or more services that must be deployed together, which is exactly the case most hosts treat as an afterthought.

    Frequently asked questions

    Can you deploy a monorepo to one server?

    Yes. Build one container per service from its folder, run them together, and connect them over an internal network. The hard parts are deploy order, service discovery and shared secrets, which is what a monorepo-aware platform automates.

    Do I need Kubernetes to deploy a monorepo with several services?

    No. Kubernetes solves the problem but needs many manifests per service. A platform like Prongo runs on Kubernetes for you and reduces the setup to one prongo.yaml file.

    How do services in a monorepo find each other after deploying?

    Through generated internal URLs. In Prongo you write {{ api.url }} in another service's env and the real address is filled in at deploy time, so nothing is hardcoded.

    What order should services in a monorepo deploy in?

    Dependencies first: database, then the API that needs it, then the frontend that calls the API. Declaring depends_on lets the platform work this order out instead of you scripting it.