Mithril-Grade Project Management: Self-Hosted OpenProject

Host your own business-grade project management suite with OpenProject.
Server
Project Management
OpenProject
Author

Daniel Gerdesmann

Published

July 3, 2024

Modified

July 7, 2025

Changelog

2025/07/07: The OpenProject installation was originally included in the Part I (server-basics) post. However, this post became too long, so OpenProject got its own post. Updated the installation and upgrade process while at it.

A scene from the Council of Elrond in Rivendell - but with Dalmatians. Gandalf's famous advice is inscribed in an Elvish font at the bottom of the image: All we have to decide is what to do with the time that is given to us.

Motivation & Overview

Every now and then, a project comes along that’s slightly more involved than a shared to-do list can handle. When the road is long, the company varied, and the purpose evolving — clarity and structure become your quiet allies. One of the most powerful self-hostable project management software you can wield is OpenProject. Following is a glimpse into the many features of OpenProject, not the least of which is full control over your data, so even the most delicate and ambitious missions can stay on track:

  • Gantt charts
  • Kanban boards
  • Detailed work packages with activity tracking
  • User roles and team planner
  • Calendar
  • Forums, Wiki
  • Budgets, time, and cost overviews
  • Notifications
  • Integration with Nextcloud (files) and GitHub/GitLab

TL;DR: It includes just about everything you could hope to find in a FOSS project management suite (browse all features here).

If that wasn’t gloomy enough for you or you need some visuals, watch OpenProject’s ~1-minute promotional introduction.

The only two caveats I see are its resource usage — which might stretch the limits of a small VPS — and the initial complexity. The feature set is broad, and while powerful, it can feel overwhelming at first. It takes a bit of time to get up to speed and might be too much overhead for simpler projects. Consider whether you truly need full-fledged project management software, or if a notes app combined with a Kanban board like Nextcloud Deck might be enough.

📦 App Info

  • Logo:
  • Purpose: Business-grade project management platform for planning, tracking, and collaborating on complex projects
  • Deployment: Docker Compose (preferred), various other methods

🔍 Tech & Usage

  • Stack: Ruby on Rails (backend), Angular (frontend), PostgreSQL, Apache/NGINX
  • Memory: ~1.5–2.5 GB in idle (!)
  • Disk: ~1.5 GB (Docker image + minimal setup)
  • CPU: Low at idle; increases with active users and background jobs

📜 License & Community

  • License: Open Source (GNU GPL v3)
  • Pricing: Free self-hosted edition; paid cloud and enterprise plans
  • Type: Maintained by OpenProject GmbH with active open-source community
  • User Base: Rather large

🌐 Homepage · 💻 GitHub

Enough chatter, let’s set it up!

Installing OpenProject

We will install OpenProject the officially preferred way. You need Docker and a reverse proxy installed (here we use Nginx Proxy Manager). If you don’t know what I am talking about, the Part I (server-basics) post walks you through everything.

Step 1: Register a Subdomain

In the end, you want OpenProject served under a nice subdomain like projects.yourdomain.eu. So the first step is to register this domain with your registrar. Enter an A-record and point it to your server’s IP address. While this new subdomain is being registered, we can move on to installing OpenProject.

Step 2: Pull the Compose File

Logged into your server console? Great! The OpenProject developers maintain a Docker Compose file that defines everything OpenProject needs to run. We’ll pull it from their GitHub repository. Before running the command, check if the URL is still valid and whether a newer OpenProject version is available here.

Notice the openproject at the end of the command; it tells Git to create a directory called openproject and put all the files in there.

git clone https://github.com/opf/openproject-docker-compose.git --depth=1 --branch=stable/16 openproject
1
Clones the files into a new directory named openproject. Change the version in –branch=stable/16 if needed.

You will now have a new folder called openproject in your current directory. Move there with:

cd openproject

Step 3: Edit the Settings

We need to adjust some configuration files to make them work with our Nginx Proxy Manager setup. But instead of modifying the original files directly, we’ll create custom overrides. This way, if we pull the repository again to upgrade OpenProject, we won’t lose our changes.

Start by copying the example environment file to a new file called .env:

cp .env.example .env
1
cp is the standard copy command: first comes the source file, then the destination.

Now open the .env file with:

nano .env

You’ll see the following content. Use the annotations below to make the required changes:

##
# All environment variables defined here will only apply if you pass them
# to the OpenProject container in docker-compose.yml under x-op-app -> environment.
# For the examples here this is already the case.
#
# Please refer to our documentation to see all possible variables:
#   https://www.openproject.org/docs/installation-and-operations/configuration/environment/
#
TAG=15-slim
OPENPROJECT_HTTPS=false
OPENPROJECT_HOST__NAME=localhost
PORT=127.0.0.1:8080
OPENPROJECT_RAILS__RELATIVE__URL__ROOT=
IMAP_ENABLED=false
DATABASE_URL=postgres://postgres:some-long-strong-password@db/openproject?pool=20&encoding=unicode&reconnect=true
RAILS_MIN_THREADS=4
RAILS_MAX_THREADS=16
PGDATA="/var/lib/postgresql/data"
OPDATA="/var/openproject/assets"
POSTGRES_PASSWORD=some-long-strong-password
1
No changes here yet; we disable HTTPS for now because it will be handled later by Nginx Proxy Manager. We’ll re-enable it afterward.
2
Replace localhost with the subdomain you plan to use, e.g. openproject.yourdomain.com. This must match what you set in Nginx Proxy Manager.
3
Remove the IP part (127.0.0.1:) so it reads just PORT=8080. You can change the port to something else if 8080 is already in use (e.g. 8085). This is not needed if you follow Step 3.1.
4
Replace “some-long-strong-password” with a strong password. You’ll find a command below to generate one.
5
If the line isn’t present, add it manually. Type “POSTGRES_PASSWORD=” followed by the same password you use above. I use it to make sure this password is indeed used, not the default one.

Save the file with CTRL + S, then exit Nano with CTRL + X.

In case you want to generate a strong password, you can use this command:

openssl rand -base64 24
1
Generates a pseudo-random sequence of 32 characters (uppercase, lowercase, numbers, symbols).

Notice the OPDATA= variable above? That’s where OpenProject will store uploaded files and other data. We need to make sure this directory exists, and that your user has permission to write to it:

sudo mkdir -p /var/openproject/assets
sudo chown 1000:1000 -R /var/openproject/assets
1
Creates the directory if it doesn’t exist.
2
Sets ownership to user ID 1000, which usually corresponds to the first non-root user (i.e. your user). If you’re unsure, use id -u to get the UID of the current user.

3.1.: [Optional] Implement a Tighter Setup

Open to adding a step that makes your setup even more secure, and learning a thing or two along the way?

Cool, expand this!

Let’s take a look at the docker-compose.yml file, which defines how OpenProject runs:

networks:
  frontend:
  backend:

volumes:
  pgdata:
  opdata:

x-op-restart-policy: &restart_policy
  restart: unless-stopped
x-op-image: &image
  image: openproject/openproject:${TAG:-16-slim}
x-op-app: &app
  <<: [*image, *restart_policy]
  environment:
    OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}"
    OPENPROJECT_HOST__NAME: "${OPENPROJECT_HOST__NAME:-localhost:8080}"
    OPENPROJECT_HSTS: "${OPENPROJECT_HSTS:-true}"
    RAILS_CACHE_STORE: "memcache"
    OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211"
    OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    DATABASE_URL: "${DATABASE_URL:-postgres://postgres:p4ssw0rd@db/openproject?pool=20&encoding=unicode&reconnect=true}"
    RAILS_MIN_THREADS: ${RAILS_MIN_THREADS:-4}
    RAILS_MAX_THREADS: ${RAILS_MAX_THREADS:-16}
    # set to true to enable the email receiving feature. See ./docker/cron for more options
    IMAP_ENABLED: "${IMAP_ENABLED:-false}"
  volumes:
    - "${OPDATA:-opdata}:/var/openproject/assets"

services:
  db:
    image: postgres:13
    <<: *restart_policy
    stop_grace_period: "3s"
    volumes:
      - "${PGDATA:-pgdata}:/var/lib/postgresql/data"
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-p4ssw0rd}
      POSTGRES_DB: openproject
    networks:
      - backend

  cache:
    image: memcached
    <<: *restart_policy
    networks:
      - backend

  proxy:
    build:
      context: ./proxy
      args:
        APP_HOST: web
    image: openproject/proxy
    <<: *restart_policy
    ports:
      - "${PORT:-8080}:80"
    depends_on:
      - web
    networks:
      - frontend

  web:
    <<: *app
    command: "./docker/prod/web"
    networks:
      - frontend
      - backend
    depends_on:
      - db
      - cache
      - seeder
    labels:
      - autoheal=true
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}/health_checks/default"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 30s

  autoheal:
    image: willfarrell/autoheal:1.2.0
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    environment:
      AUTOHEAL_CONTAINER_LABEL: autoheal
      AUTOHEAL_START_PERIOD: 600
      AUTOHEAL_INTERVAL: 30

  worker:
    <<: *app
    command: "./docker/prod/worker"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  cron:
    <<: *app
    command: "./docker/prod/cron"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  seeder:
    <<: *app
    command: "./docker/prod/seeder"
    restart: on-failure
    networks:
      - backend

Phew, that’s a pretty advanced setup already! Multiple containers, volumes, and two Docker networks. But let’s focus on two things: the defined networks and the port exposure in the proxy service.

At the top, the file defines two Docker networks: frontend and backend. These control which containers can talk to each other. In this setup, background services like the database are on the backend, while user-facing services like the web interface and proxy are on the frontend. This separation reduces the attack surface and improves maintainability — only containers that need to talk to each other can do so.

Now look at the proxy container. It’s exposing port 8080 to the host system. That’s already fairly secure, but we can tighten it up further.

Instead of exposing port 8080 to the host, we can connect the proxy container to the same Docker network used by our reverse proxy (Nginx Proxy Manager). That way, NPM can communicate directly with the OpenProject proxy, without exposing extra ports to the host. This also means we don’t have to manually manage port numbers.

But again, we don’t want to edit docker-compose.yml directly, because updates might overwrite our changes. Instead, Docker provides a neat way to override parts of a Compose file using docker-compose.override.yml. Let’s use that to apply our adjustments:

Create and enter the file:

sudo nano docker-compose.override.yml

Paste the following:

services:
  proxy:
    networks:
      - npm-net
    ports: []

networks:
  npm-net:
    external: true
1
We’re attaching the proxy container to an additional network (npm-net); the same one used by Nginx Proxy Manager.
2
We clear the ports section entirely. This prevents Docker from exposing port 8080 to the host.
3
We declare that npm-net is an external network (i.e. it must already exist).

That’s it. When you start the stack in the next step, Docker will automatically merge this override into the main config.

Step 4: Start Your OpenProject Instance

You are ready to launch:

docker compose up -d --build --pull always && docker compose logs -f
1
The && docker compose logs -f part tells Docker to stream the logs right after launching. It’s helpful for spotting issues early. You can stay and watch the logs, or move on to the next step — press Ctrl + C to exit log view.

The installation takes around five minutes. Enough time to configure Nginx Proxy Manager.

Step 5: Add a New Entry in Nginx Proxy Manager

Go to your Nginx Proxy Manager interface at nproxy.yourdomain.com. Add a new proxy host.

  • Domain name: Enter the same domain you set for OPENPROJECT_HOST_NAME in your .env file.
  • Forward hostname / IP: Enter proxy if you followed step 3.1 (because NPM is connected to the same Docker network). Otherwise, enter your server’s local IP (127.0.0.1).
  • Forward port:
    • If you followed step 3.1: use port 80.
    • Otherwise: use port 8080, or whichever port you chose in .env.

Enable Block Common Exploits and Websocket Support.

Now test your setup. Open your browser and visit your OpenProject domain (e.g. http://openproject.yourdomain.com). You should see the login screen or setup wizard.

If it loads correctly, go back to Nginx Proxy Manager and edit the OpenProject entry. Go to the SSL tab, request a certificate, enable Force SSL, HTTP/2 Support, and HSTS. Agree to the terms and click save.

Step 6: Finalize Your Setup

Navigate to openproject.yourdomain.com again and check if it redirects to HTTPS.

Log in to OpenProject using the default admin credentials (both username and password: admin). Be sure to change these credentials right away and store them somewhere secure.

Welcome to your own powerful project management suite! Nice work.

OpenProject home screen showing a warning about https at the bottom

You’ll probably see a yellow warning about an “HTTPS mode setup mismatch” at the bottom of the page. That happens because we disabled HTTPS in the .env file, but are now accessing the site over an encrypted connection.

Let’s fix that. On your server, open the .env file again:

nano .env

Change OPENPROJECT_HTTPS=false to =true. That’s it. Save (CTRL + S) and exit (CTRL + X). Run:

docker compose up -d
1
Updates your running stack with the new HTTPS setting.

That’s it. Happy exploring, and thank you for following along — dot by dot.

If you use Nextcloud, GitHub, or GitLab, don’t forget that you can integrate them with OpenProject.

Upgrading OpenProject

When OpenProject releases a new version, upgrading is straightforward. Just follow their official upgrade guide.

It includes making a backup. It is a good idea before upgrading, but also good practice on its own.

Giving Back

Great product, friendly FOSS attitude — chapeau to the OpenProject team! If you want to give back, here are a few ideas:

  • Star the project on GitHub

  • Promote OpenProject, especially in enterprise settings

  • Join discussions or contribute to the codebase

Of course, tossing something evil into the appropriate volcano is also helpful.