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:
- A server with a fresh Dokku installation.
- A domain name (
app.example.comin this guide) pointed at your serverβs public IP address. - Git installed on your local machine.
- A project locally that matches the monorepo structure described below.
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:
- 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.
- 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.