From 8aa912ce5a181a74dcfdaaff3d96f2957b554f0c Mon Sep 17 00:00:00 2001 From: rnsrk Date: Tue, 22 Apr 2025 22:46:08 +0200 Subject: [PATCH] Initial commit --- .env-example | 20 ++ .gitignore | 4 + README.md | 223 +++++++++++++++++++++++ control/Dockerfile | 11 ++ control/backup/entrypoint.sh | 13 ++ control/upgrade/entrypoint.sh | 8 + control/upgrade/scripts/00-db-upgrade.sh | 32 ++++ docker-compose.control.yml | 50 +++++ docker-compose.yml | 117 ++++++++++++ proxy/Caddyfile.template | 26 +++ proxy/Dockerfile | 8 + 11 files changed, 512 insertions(+) create mode 100644 .env-example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 control/Dockerfile create mode 100755 control/backup/entrypoint.sh create mode 100755 control/upgrade/entrypoint.sh create mode 100755 control/upgrade/scripts/00-db-upgrade.sh create mode 100644 docker-compose.control.yml create mode 100644 docker-compose.yml create mode 100644 proxy/Caddyfile.template create mode 100644 proxy/Dockerfile diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..cb62960 --- /dev/null +++ b/.env-example @@ -0,0 +1,20 @@ +## +# 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/ +# +OPENPROJECT_DOMAIN= +OPENPROJECT_TAG= +OPENPROJECT_HTTPS= +OPENPROJECT_HOST__NAME= +OPENPROJECT_PORT= +OPENPROJECT_IMAP_ENABLED=false +OPENPROJECT_POSTGRES_USER= +OPENPROJECT_POSTGRES_PASSWORD= +OPENPROJECT_POSTGRES_DB= +OPENPROJECT_RAILS_MIN_THREADS= +OPENPROJECT_RAILS_MAX_THREADS= +OPENPROJECT_OPDATA= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4284de --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env + +docker-compose.override.yml +backups/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6cc71a --- /dev/null +++ b/README.md @@ -0,0 +1,223 @@ +# OpenProject installation with Docker Compose + +This repository contains the installation method for OpenProject using Docker Compose. + + +> [!NOTE] +> Looking for the Kubernetes installation method? +> Please use the [OpenProject helm chart](https://charts.openproject.org) to install OpenProject on kubernetes. + +## Quick start + +First, you must clone the [openproject-deploy](https://github.com/opf/openproject-deploy/tree/stable/15/compose) repository: + +```shell +git clone https://github.com/opf/openproject-deploy --depth=1 --branch=stable/15 openproject +``` + +Copy the example `.env` file and edit any values you want to change: + +```shell +cp .env.example .env +vim .env +``` + +If you are using the default value of OPDATA that is used in the ```.env.example``` you need to make sure that the folder exist, and you have the right permissions: + +```shell +sudo mkdir -p /var/openproject/assets +sudo chown 1000:1000 -R /var/openproject/assets +``` + +Next you start up the containers in the background while making sure to pull the latest versions of all used images. + +```shell +OPENPROJECT_HTTPS=false docker compose up -d --build --pull always +``` + +After a while, OpenProject should be up and running on `http://localhost:8080`. The default username and password is login: `admin`, and password: `admin`. +The `OPENPROJECT_HTTPS=false` environment variable explicitly disables HTTPS mode for the first startup. Without this, OpenProject assumes it's running behind HTTPS in production by default. +We do strongly recommend you use OpenProject behind a TLS terminated proxy for production purposes and remove this flag before actually starting to use it. + +### Customization + +The `docker-compose.yml` file present in the repository can be adjusted to your convenience. But note that with each pull, it will be overwritten. +Best practice is to use the file `docker-compose.override.yml` for that case. +For instance you could mount specific configuration files, override environment variables, or switch off services you don't need. + +Please refer to the official [Docker Compose documentation](https://docs.docker.com/compose/extends/) for more details. + +### Troubleshooting + +**pull access denied for openproject/proxy, repository does not exist or may require 'docker login': denied: requested access to the resource is denied** + +If you encounter this after `docker compose up` this is merely a warning which can be ignored. + +If this happens during `docker compose pull` this is simply a warning as well. +But it will result in the command's exit code to be a failure even though all images are pulled. +To prevent this you can add the `--ignore-buildable` option, running `docker compose pull --ignore-buildable`. + +### HTTPS/SSL + +By default OpenProject starts with the HTTPS option **enabled**, but it **does not** handle SSL termination itself. This +is usually done separately via a [reverse proxy +setup](https://www.openproject.org/docs/installation-and-operations/installation/docker/#apache-reverse-proxy-setup). +Without this you will run into an `ERR_SSL_PROTOCOL_ERROR` when accessing OpenProject. + +See below how to disable HTTPS. + +Be aware that if you want to use the integrated Caddy proxy as a proxy with outbound connections, you need to rewrite the +`Caddyfile`. In the default state, it is configured to forward the `X-Forwarded-*` headers from the reverse proxy in +front of it and not setting them itself. This is considered a security flaw and should instead be solved by configuring +`trusted_proxies` inside the `Caddyfile`. For more information read +the [Caddy documentation](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy). + +### PORT + +By default the port is bound to `0.0.0.0` means access to OpenProject will be public. +See below how to change that. + +## Image configuration + +OpenProject publishes `slim` containers that you should be using for this compose setup. +Please see https://www.openproject.org/docs/installation-and-operations/installation/docker/#available-containers for more information on the containers and versions we push. + +## Configuration + +Environment variables can be added to `docker-compose.yml` under `x-op-app -> environment` to change +OpenProject's configuration. Some are already defined and can be changed via the environment. + +You can pass those variables directly when starting the stack as follows. + +``` +VARIABLE=value docker-compose up -d +``` + +You can also put those variables into an `.env` file in your current working +directory, and Docker Compose will pick it up automatically. See `.env.example` +for details. + +## HTTPS + +You can disable OpenProject's HTTPS option via: + +``` +OPENPROJECT_HTTPS=false +``` + +## PORT + +If you want to specify a different port, you can do so with: + +``` +PORT=4000 +``` + +If you don't want OpenProject to bind to `0.0.0.0` you can bind it to localhost only like this: + +``` +PORT=127.0.0.1:8080 +``` + +## TAG + +If you want to specify a custom tag for the OpenProject docker image, you can do so with: + +``` +TAG=my-docker-tag +``` + +## BIM edition + +In order to install or change to BIM inside a Docker environment, please navigate to the [Docker Installation for OpenProject BIM](https://www.openproject.org/docs/installation-and-operations/bim-edition/#docker-installation-openproject-bim) paragraph at the BIM edition documentation. + +## Upgrade + +Retrieve any changes from the `openproject-deploy` repository: + + git pull origin stable/15 + +Build the control plane: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml build + +Take a backup of your existing postgresql data and openproject assets: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml run backup + +Run the upgrade: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml run upgrade + +Relaunch the containers, ensure you are pulling to use the latest version of the Docker images: + + docker compose up -d --build --pull always + + + +## Backup + +Switch off your current installation: + + docker-compose down + +Build the control scripts: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml build + +Take a backup of your existing PostgreSQL data and OpenProject assets: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml run backup + +Restart your OpenProject installation + + docker-compose up -d + + + +## Uninstall + +If you want to stop the containers without removing them directly: + +```bash +docker-compose stop +``` + +You can remove the container stack with: + +```bash +docker-compose down +``` + +> [!NOTE] +> This will not remove your data which is persisted in named volumes, likely called `compose_opdata` (for attachments) and `compose_pgdata` (for the database). +> The exact name depends on the name of the directory where your `docker-compose.yml` and/or you `docker-compose.override.yml` files are stored (`compose` in this case). + +If you want to start from scratch and remove the existing data you will have to remove these volumes via +`docker volume rm compose_opdata compose_pgdata`. + +## Troubleshooting + +You can look at the logs with: + + docker-compose logs -n 1000 + +For the complete documentation, please refer to https://docs.openproject.org/installation-and-operations/. + +### Network issues + +If you're running into weird network issues and timeouts such as the one described in +[OP#42802](https://community.openproject.org/work_packages/42802), you might have success in remove the two separate +frontend and backend networks. This might be connected to using podman for orchestration, although we haven't been able +to confirm this. + +### SMTP setup fails: Network is unreachable. + +Make sure your container has DNS resolution to access external SMTP server when set up as described in +[OP#44515](https://community.openproject.org/work_packages/44515). + +```yml +worker: + dns: + - "Your DNS IP" # OR add a public DNS resolver like 8.8.8.8 +``` diff --git a/control/Dockerfile b/control/Dockerfile new file mode 100644 index 0000000..a90a259 --- /dev/null +++ b/control/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:12 + +RUN apt-get update -qq && apt-get install wget gnupg2 -y && rm -rf /var/lib/apt/lists/* +RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +RUN echo "deb http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list +RUN apt-get update -qq && apt-get install postgresql-9.6 postgresql-10 postgresql-13 -y && rm -rf /var/lib/apt/lists/* +RUN localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 + +ENV LANG en_US.utf8 + +ADD . /control diff --git a/control/backup/entrypoint.sh b/control/backup/entrypoint.sh new file mode 100755 index 0000000..f81abe2 --- /dev/null +++ b/control/backup/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +timestamp=$(date +%s) +mkdir -p /backups +cd /backups +filename="${timestamp}-pgdata.tar.gz" +echo "Backing up PostgreSQL data into backups/${filename}..." +tar czf "${filename}" -C "$PGDATA" . +filename="${timestamp}-opdata.tar.gz" +echo "Backing up OpenProject assets into backups/${filename}..." +tar czf "${filename}" -C "$OPDATA" . +echo "DONE" diff --git a/control/upgrade/entrypoint.sh b/control/upgrade/entrypoint.sh new file mode 100755 index 0000000..9cbc947 --- /dev/null +++ b/control/upgrade/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +set -o pipefail + +/control/upgrade/scripts/00-db-upgrade.sh + +echo "Please restart your installation by issuing the following command:" +echo " docker compose up -d --build --pull always" diff --git a/control/upgrade/scripts/00-db-upgrade.sh b/control/upgrade/scripts/00-db-upgrade.sh new file mode 100755 index 0000000..1e89bad --- /dev/null +++ b/control/upgrade/scripts/00-db-upgrade.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e +set -o pipefail + +CURRENT_PGVERSION="$(cat $PGDATA/PG_VERSION)" +NEW_PGVERSION="13" +PGWORKDIR=${PGWORKDIR:=/var/lib/postgresql/work} + +if [ ! "$CURRENT_PGVERSION" -lt "$NEW_PGVERSION" ]; then + echo "Current PG version is higher or equal to the PG version to be installed ($CURRENT_PGVERSION > $NEW_PGVERSION). Ignoring." + exit 0 +fi + +export PGBINOLD="/usr/lib/postgresql/$CURRENT_PGVERSION/bin" +export PGBINNEW="/usr/lib/postgresql/$NEW_PGVERSION/bin" +export PGDATAOLD="$PGDATA" +export PGDATANEW="$PGWORKDIR/datanew" + +rm -rf "$PGWORKDIR" && mkdir -p "$PGWORKDIR" "$PGDATANEW" +chown -R postgres.postgres "$PGDATA" "$PGWORKDIR" +cd "$PGWORKDIR" +# initialize new db +su -m postgres -c "$PGBINNEW/initdb --pgdata=$PGDATANEW --encoding=unicode --auth=trust" +echo "Performing a dry-run migration to PostgreSQL $NEW_PGVERSION..." +su -m postgres -c "$PGBINNEW/pg_upgrade -c" +echo "Performing the real migration to PostgreSQL $NEW_VERSION..." +su -m postgres -c "$PGBINNEW/pg_upgrade" +su -m postgres -c "rm -rf $PGDATAOLD/* && mv $PGDATANEW/* $PGDATAOLD/" +# as per docker hub documentation +su -m postgres -c "echo \"listen_addresses = '*'\" >> $PGDATAOLD/postgresql.conf" +su -m postgres -c "echo \"host all all all md5\" >> $PGDATAOLD/pg_hba.conf" +echo "DONE" diff --git a/docker-compose.control.yml b/docker-compose.control.yml new file mode 100644 index 0000000..947efef --- /dev/null +++ b/docker-compose.control.yml @@ -0,0 +1,50 @@ +version: "3.7" + +volumes: + pgupgrade: + +services: + db: + restart: "no" + entrypoint: ["echo", "disabled"] + upgrade: + restart: "no" + build: + context: ./control + environment: + PGDATA: /var/lib/postgresql/data + volumes: + - "${OPENPROJECT_PGDATA:-pgdata}:/var/lib/postgresql/data" + - "./control:/control" + entrypoint: ["/control/upgrade/entrypoint.sh"] + backup: + restart: "no" + build: + context: ./control + environment: + PGDATA: /var/lib/postgresql/data + OPDATA: /var/openproject/assets + volumes: + - "${OPENPROJECT_OPDATA:-opdata}:/var/openproject/assets" + - "./backups:/backups" + - "./control:/control" + entrypoint: ["/control/backup/entrypoint.sh"] + web: + restart: "no" + entrypoint: ["echo", "disabled"] + worker: + restart: "no" + entrypoint: ["echo", "disabled"] + cron: + restart: "no" + entrypoint: ["echo", "disabled"] + seeder: + restart: "no" + entrypoint: ["echo", "disabled"] + proxy: + restart: "no" + entrypoint: ["echo", "disabled"] + cache: + restart: "no" + entrypoint: ["echo", "disabled"] + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..304af67 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,117 @@ +networks: + database: + external: true + frontend: + backend: + traefik: + external: true +volumes: + opdata: + +x-op-restart-policy: &restart_policy + restart: unless-stopped +x-op-image: &image + image: openproject/openproject:${OPENPROJECT_TAG:-15-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: "postgres://${OPENPROJECT_DB_USER:-openproject}:${OPENPROJECT_DB_PASSWORD:-openproject}@postgres/openproject?pool=20&encoding=unicode&reconnect=true" + RAILS_MIN_THREADS: ${OPENPROJECT_RAILS_MIN_THREADS:-4} + RAILS_MAX_THREADS: ${OPENPROJECT_RAILS_MAX_THREADS:-16} + # set to true to enable the email receiving feature. See ./docker/cron for more options + IMAP_ENABLED: "${OPENPROJECT_IMAP_ENABLED:-false}" + volumes: + - "${OPENPROJECT_OPDATA:-opdata}:/var/openproject/assets" + +services: + cache: + image: memcached + networks: + - backend + - database + restart: unless-stopped + + proxy: + build: + context: ./proxy + args: + APP_HOST: web + image: openproject/proxy + depends_on: + - web + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.openproject.entrypoints=web,websecure + - traefik.http.routers.openproject.middlewares=https-redirect + - traefik.http.routers.openproject.tls=true + - traefik.http.routers.openproject.tls.certresolver=le + - traefik.http.routers.openproject.rule=Host(`${OPENPROJECT_DOMAIN}`) + - traefik.http.services.openproject.loadbalancer.server.port=80 + networks: + - traefik + - frontend + restart: unless-stopped + + web: + <<: *app + command: "./docker/prod/web" + networks: + - database + - frontend + - backend + depends_on: + - 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 + - database + depends_on: + - cache + - seeder + + cron: + <<: *app + command: "./docker/prod/cron" + networks: + - backend + - database + depends_on: + - cache + - seeder + + seeder: + <<: *app + command: "./docker/prod/seeder" + restart: on-failure + networks: + - backend + - database + diff --git a/proxy/Caddyfile.template b/proxy/Caddyfile.template new file mode 100644 index 0000000..6cc4730 --- /dev/null +++ b/proxy/Caddyfile.template @@ -0,0 +1,26 @@ +{ + # Global options + servers { + # Configure trusted proxies to correctly handle forwarded headers from Traefik + trusted_proxies static private_ranges + } +} + +:80 { + reverse_proxy * http://${APP_HOST}:8080 { + # The following directives are needed to make the proxy forward explicitly the X-Forwarded-* headers. If unset, + # Caddy will reset them. See: https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#defaults + # This is needed, if you are using a reverse proxy in front of the compose stack and Caddy is NOT your first + # point of contact. + # When using Caddy is reachable as a first point of contact, it is highly recommended to configure the server's + # global `trusted_proxies` directive. See: https://caddyserver.com/docs/caddyfile/options#trusted-proxies + + header_up X-Forwarded-Proto {header.X-Forwarded-Proto} + header_up X-Forwarded-For {header.X-Forwarded-For} + header_up X-Forwarded-Host {header.X-Forwarded-Host} + } + + file_server + + log +} diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..5a6591b --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,8 @@ +FROM caddy:2 + +COPY ./Caddyfile.template /etc/caddy/Caddyfile.template + +ARG APP_HOST +RUN sed 's|${APP_HOST}|'"$APP_HOST"'|g' /etc/caddy/Caddyfile.template > /etc/caddy/Caddyfile + +ENTRYPOINT ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]