Skip to content
Go back

A Guide to Dokku for Monorepo Applications

Published:

Dokku is a popular open-source Platform as a Service (PaaS) that lets you run Heroku-like deployments on your own server. Deploying a standard, single-service application is easy, but modern projects often require a monorepo structure with separated frontends and backends that need to communicate securely.

This guide shares a command-by-command walkthrough for deploying a React frontend and a FastAPI backend from a single Git repository, connected to a private PostgreSQL database instance. While FastAPI is used as an example here, this architecture is framework-agnostic and can support any backend (e.g., Django, Flask, Node.js, Go, etc.).

Prerequisites

To follow this guide, you will need:


Architecture and Strategy

We assume a monorepo layout where the two services live in dedicated sub-directories. The deployment-specific files (Dockerfile, app.json, etc.) should also be located within their respective service directories.

/
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ Dockerfile
β”‚   β”œβ”€β”€ package.json
β”‚   └── docker/
β”‚       └── nginx.conf
└── backend/
    β”œβ”€β”€ Dockerfile
    β”œβ”€β”€ app.json
    └── pyproject.toml

The Reverse Proxy

The core of this setup is the Nginx Reverse Proxy running inside the frontend container, that is, the browser only talks to the backend via the reverse proxy.

Public Internet
      β”‚
      β–Ό (app.example.com)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Dokku Host       β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ Frontend App    β”‚      β”‚ Backend App     β”‚
β”‚ β”‚ (Nginx)         │──────│ (FastAPI)       β”‚
β”‚ β”‚ serves React    β”‚      β”‚ (internal only) β”‚
β”‚ β”‚ proxies /api/*  β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                                  β”‚
β”‚                           β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           β”‚ Postgres DB    β”‚
β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When your React app calls fetch('/api/data'), the request goes to the Nginx server in the frontend-app. Nginx sees the /api path and forwards the request internally to the private backend-app.

This provides two benefits:

  1. Security: The backend API is not directly accessible to the public internet. It only responds to requests from the frontend app on the internal network.
  2. No CORS Headaches: From the browser’s perspective, the frontend assets and the API are served from the same domain. This means you don’t need to configure Cross-Origin Resource Sharing (CORS) on your backend.

Step 1: Install Plugins and Create Services

First, we’ll install the necessary Dokku plugins and create our database service.

# 1. Install the official PostgreSQL plugin
dokku plugin:install https://github.com/dokku/dokku-postgres.git

# 2. Install the Let's Encrypt plugin for free SSL/TLS
dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

# 3. Create the PostgreSQL database service
dokku postgres:create project-db

# 4. Optional: Verify the database service is running
dokku postgres:list

Step 2: Create and Configure Dokku Applications

Next, we create two distinct Dokku applications and tell them where to find their code within the monorepo.

# --- Frontend App ---

# 1. Create the public-facing frontend application
dokku apps:create frontend-app

# 2. Tell Dokku it will be built from the 'frontend' sub-directory
dokku builder:set frontend-app build-dir frontend


# --- Backend App ---

# 1. Create the private backend application
dokku apps:create backend-app

# 2. Tell Dokku it will be built from the 'backend' sub-directory
dokku builder:set backend-app build-dir backend

# 3. Link the DB to the backend app. This automatically sets the DATABASE_URL env var.
dokku postgres:link project-db backend-app

Step 3: Configure Networking and Ports

We create an internal network for our apps to communicate on and ensure the backend has no public-facing port.

# 1. Create a dedicated, isolated Docker network
dokku network:create app-network

# 2. Attach both services to this network so they can communicate
dokku network:set frontend-app attach-post-create app-network
dokku network:set backend-app attach-post-create app-network

# 3. Clear any default ports from the frontend app
dokku ports:clear frontend-app

# 4. Expose the frontend's Nginx server on port 80 for public web traffic
dokku ports:add frontend-app http:80:80

# 5. CRUCIAL: Clear all default ports from the backend app
# This makes the backend completely inaccessible from the public internet.
dokku ports:clear backend-app

By not adding a public port to the backend-app, we guarantee it is only accessible via the internal app-network.


Step 4: Set Environment Variables

The frontend requires no build-time environment variables, as the API URL is always /api.

For the backend, DATABASE_URL is set automatically by Dokku when you link the postgres service. You only need to add your application-specific secrets.

# Example: Set a secret key for your FastAPI application
dokku config:set backend-app SECRET_KEY="your-super-secret-key"

# Note: If your app requires a specific database driver scheme 
# (like postgresql+psycopg), you can see the connection details 
# with the following:
#   dokku postgres:info project-db
# and then construct and set the URL manually with:
#   dokku config:set backend-app DATABASE_URL=...

Step 5: Domain and SSL Configuration

Now, we point a public domain to our frontend-app and secure it with a free SSL certificate from Let’s Encrypt.

# 1. Add your custom domain to the frontend app
dokku domains:add frontend-app app.example.com

# 2. Set the email for certificate notifications
dokku letsencrypt:set frontend-app email your-email@example.com

# 3. Enable Let's Encrypt and install the certificate
dokku letsencrypt:enable frontend-app

# 4. Add the cron job for automatic certificate renewal
dokku letsencrypt:cron-job --add

Step 6: Configure Docker and Nginx

The deployment is orchestrated with Dockerfiles in each service directory and a custom nginx.conf for the frontend proxy.

Frontend Dockerfile

This uses a multi-stage build: one stage to build the React app, and a final, lightweight Nginx stage to serve the static files.

# File: frontend/Dockerfile
# Stage 1: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# No build-time env vars are needed since the API is always at /api
RUN npm run build

# -------------------------
# Stage 2: Serve with nginx
FROM nginx:alpine
# Copy our custom Nginx config
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets from the builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Frontend docker/nginx.conf

This config tells Nginx how to serve the React app and proxy API calls. The proxy_pass directive uses Dokku’s internal DNS to route traffic to the backend.

# File: frontend/docker/nginx.conf
server {
  listen 80;

  # React frontend
  location / {
    root   /usr/share/nginx/html;
    index  index.html;
    # Crucial for Single Page Applications (SPAs) like React. 
    # It ensures that any request not matching a static file is redirected
    # to index.html, allowing React Router to handle the client-side routing.
    try_files $uri $uri/ /index.html;
  }

  # FastAPI backend proxy
  location /api {
    # Proxy requests to the backend service.
    # Dokku creates an internal DNS entry for us: <app-name>.<proc-type>
    proxy_pass http://backend-app.web:8000;

    # These headers are important for passing client information to the backend
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

Backend Dockerfile

The backend uses uv for fast dependency management and granian for a high-performance ASGI server.

# File: backend/Dockerfile
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim

WORKDIR /app

RUN <<EOT
apt-get update -y && \
apt-get install -y --no-install-recommends \
    apt-transport-https \
    bash \
    libpq-dev \
    postgresql-client && \
apt-get autoremove -y && \
apt-get clean -y && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
EOT

# Install dependencies using Docker's cache for speed
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    uv sync --frozen --no-install-project

COPY . /app

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen

ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000

# Note: Migrations are run by Dokku's predeploy hook (see app.json)
CMD ["granian", "--interface", "asgi", "--host", "0.0.0.0", "--port", "8000", "--workers", "2", "app.main:app"]

Backend app.json

Dokku’s app.json support allows us to run database migrations before a new deployment goes live and to configure a health check.

File: backend/app.json

{
  "scripts": {
    "dokku": {
      "predeploy": "alembic upgrade head"
    }
  },
  "healthchecks": {
    "web": [
      {
        "type": "startup",
        "name": "web check",
        "description": "Checking if the app responds to the /health endpoint",
        "path": "/health",
        "attempts": 3
      }
    ]
  }
}

Step 7: Final Deployment

Finally, configure your local Git repository with remotes that point to your Dokku apps and deploy with git push.

# On your local machine:
# Replace <SERVER_IP> with your Dokku server's IP address
git remote add backend dokku@<SERVER_IP>:backend-app
git remote add frontend dokku@<SERVER_IP>:frontend-app

# Deploy the backend first. This prevents the frontend's Nginx proxy from
# immediately returning '502 Bad Gateway' errors for API requests if it
# starts before the backend is fully ready.
git push backend master

# Deploy the frontend
git push frontend master

Your site should now be live at https://app.example.com!


You have now successfully deployed a multi-service monorepo application from a single repository on Dokku. This guide demonstrates how to combine Dokku’s build-dir feature with private networking and a reverse proxy to create a secure and efficient deployment. This pattern is ideal for managing modern fullstack applications, giving you a clear template for your own monorepo projects.

For more details, check out the official Dokku documentation.



Next Post
Optimizing Django Docker Builds with Astral’s `uv`