feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
ARG PLANE_VERSION=v0.27.1
FROM --platform=$BUILDPLATFORM tonistiigi/binfmt AS binfmt
# **************************************************
# STAGE 0: Image Loading
# **************************************************
FROM node:22-alpine AS node
FROM artifacts.plane.so/makeplane/plane-frontend:${PLANE_VERSION} AS web-img
FROM artifacts.plane.so/makeplane/plane-backend:${PLANE_VERSION} AS backend-img
FROM artifacts.plane.so/makeplane/plane-space:${PLANE_VERSION} AS space-img
FROM artifacts.plane.so/makeplane/plane-admin:${PLANE_VERSION} AS admin-img
FROM artifacts.plane.so/makeplane/plane-live:${PLANE_VERSION} AS live-img
FROM artifacts.plane.so/makeplane/plane-proxy:${PLANE_VERSION} AS proxy-img
# **************************************************
# STAGE 1: Runner
# **************************************************
FROM python:3.12.10-alpine AS runner
WORKDIR /app
RUN apk add --no-cache \
"libpq" \
"libxslt" \
"xmlsec"
COPY --from=node /usr/lib /usr/lib
COPY --from=node /usr/local/lib /usr/local/lib
COPY --from=node /usr/local/include /usr/local/include
COPY --from=node /usr/local/bin /usr/local/bin
COPY --from=web-img /app /app/web
COPY --from=space-img /app /app/space
COPY --from=admin-img /app /app/admin
COPY --from=live-img /app /app/live
RUN rm -rf /app/web/apps/web/.next/cache && \
rm -rf /app/space/apps/space/.next/cache && \
rm -rf /app/admin/apps/admin/.next/cache
COPY --from=proxy-img /usr/bin/caddy /usr/bin/caddy
COPY dist/Caddyfile /app/proxy/Caddyfile
COPY --from=backend-img /code /app/backend
COPY --from=backend-img /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/
COPY --from=backend-img /usr/local/bin/ /usr/local/bin/
RUN apk add --no-cache nss-tools bash curl uuidgen ncdu vim
RUN pip install supervisor
RUN mkdir -p /etc/supervisor/conf.d
COPY start.sh /app/start.sh
COPY dist/plane.env /app/plane.env
COPY supervisor.conf /etc/supervisor/conf.d/supervisor.conf
RUN mkdir -p /app/logs/access && \
mkdir -p /app/logs/error && \
mkdir -p /app/data && \
chmod +x /app/start.sh
VOLUME ['/app/data', '/app/logs']
EXPOSE 80 443
CMD ["/app/start.sh"]

View File

@@ -0,0 +1,174 @@
# Plane Community All-In-One (AIO) Docker Image
The Plane Community All-In-One Docker image packages all Plane services into a single container for easy deployment and testing. This image includes web interface, API server, background workers, live server, and more.
## What's Included
The AIO image contains the following services:
- **Web App** (Port 3001): Main Plane web interface
- **Space** (Port 3002): Public project spaces
- **Admin** (Port 3003): Administrative interface
- **API Server** (Port 3004): Backend API
- **Live Server** (Port 3005): Real-time collaboration
- **Proxy** (Port 80, 443): Caddy reverse proxy
- **Worker & Beat**: Background task processing
## Prerequisites
### Required External Services
The AIO image requires these external services to be running:
- **PostgreSQL Database**: For data storage
- **Redis**: For caching and session management
- **RabbitMQ**: For message queuing
- **S3-Compatible Storage**: For file uploads (AWS S3 or MinIO)
### Required Environment Variables
You must provide these environment variables:
#### Core Configuration
- `DOMAIN_NAME`: Your domain name or IP address
- `DATABASE_URL`: PostgreSQL connection string
- `REDIS_URL`: Redis connection string
- `AMQP_URL`: RabbitMQ connection string
#### Storage Configuration
- `AWS_REGION`: AWS region (e.g., us-east-1)
- `AWS_ACCESS_KEY_ID`: S3 access key
- `AWS_SECRET_ACCESS_KEY`: S3 secret key
- `AWS_S3_BUCKET_NAME`: S3 bucket name
- `AWS_S3_ENDPOINT_URL`: S3 endpoint (optional, defaults to AWS)
## Quick Start
### Basic Usage
```bash
docker run --name plane-aio --rm -it \
-p 80:80 \
-e DOMAIN_NAME=your-domain.com \
-e DATABASE_URL=postgresql://user:pass@host:port/database \
-e REDIS_URL=redis://host:port \
-e AMQP_URL=amqp://user:pass@host:port/vhost \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY_ID=your-access-key \
-e AWS_SECRET_ACCESS_KEY=your-secret-key \
-e AWS_S3_BUCKET_NAME=your-bucket \
artifacts.plane.so/makeplane/plane-aio-community:latest
```
### Example with IP Address
```bash
MYIP=192.168.68.169
docker run --name myaio --rm -it \
-p 80:80 \
-e DOMAIN_NAME=${MYIP} \
-e DATABASE_URL=postgresql://plane:plane@${MYIP}:15432/plane \
-e REDIS_URL=redis://${MYIP}:16379 \
-e AMQP_URL=amqp://plane:plane@${MYIP}:15673/plane \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY_ID=5MV45J9NF5TEFZWYCRAX \
-e AWS_SECRET_ACCESS_KEY=7xMqAiAHsf2UUjMH+EwICXlyJL9TO30m8leEaDsL \
-e AWS_S3_BUCKET_NAME=plane-app \
-e AWS_S3_ENDPOINT_URL=http://${MYIP}:19000 \
-e FILE_SIZE_LIMIT=10485760 \
artifacts.plane.so/makeplane/plane-aio-community:latest
```
## Configuration Options
### Optional Environment Variables
#### Network & Protocol
- `SITE_ADDRESS`: Server bind address (default: `:80`)
#### Security & Secrets
- `SECRET_KEY`: Django secret key (default provided)
- `LIVE_SERVER_SECRET_KEY`: Live server secret (default provided)
#### File Handling
- `FILE_SIZE_LIMIT`: Maximum file upload size in bytes (default: `5242880` = 5MB)
#### API Configuration
- `API_KEY_RATE_LIMIT`: API key rate limit (default: `60/minute`)
## Port Mapping
The following ports are exposed:
- `80`: Main web interface (HTTP)
- `443`: HTTPS (if SSL configured)
## Volume Mounts
### Recommended Persistent Volumes
```bash
-v /path/to/logs:/app/logs \
-v /path/to/data:/app/data
```
## Building the Image
To build the AIO image yourself:
```bash
cd deployments/aio/community
IMAGE_NAME=myplane-aio ./build.sh --release=v0.27.1 [--platform=linux/amd64]
```
Available build options:
- `--release`: Plane version to build (required)
- `--image-name`: Custom image name (default: `plane-aio-community`)
## Troubleshooting
### Logs
All service logs are available in `/app/logs/`:
- Access logs: `/app/logs/access/`
- Error logs: `/app/logs/error/`
### Health Checks
The container runs multiple services managed by Supervisor. Check service status:
```bash
docker exec -it <container-name> supervisorctl status
```
### Common Issues
1. **Database Connection Failed**: Ensure PostgreSQL is accessible and credentials are correct
2. **Redis Connection Failed**: Verify Redis server is running and URL is correct
3. **File Upload Issues**: Check S3 credentials and bucket permissions
### Environment Validation
The container will validate required environment variables on startup and display helpful error messages if any are missing.
## Production Considerations
- Use proper SSL certificates for HTTPS
- Configure proper backup strategies for data
- Monitor resource usage and scale accordingly
- Use external load balancer for high availability
- Regularly update to latest versions
- Secure your environment variables and secrets
## Support
For issues and support, please refer to the official Plane documentation.

View File

@@ -0,0 +1,155 @@
#!/bin/bash
set -e
DIST_DIR=${DIST_DIR:-./dist}
CPU_ARCH=$(uname -m)
IMAGE_NAME=${IMAGE_NAME:-makeplane/plane-aio-community}
# loop though all flags and set the variables
for arg in "$@"; do
case $arg in
--release)
APP_RELEASE_VERSION="$2"
shift
shift
;;
--release=*)
APP_RELEASE_VERSION="${arg#*=}"
shift
;;
--image-name)
IMAGE_NAME="$2"
shift
shift
;;
--image-name=*)
IMAGE_NAME="${arg#*=}"
shift
;;
esac
done
if [ -z "$APP_RELEASE_VERSION" ]; then
echo ""
echo "Usage: "
echo " ./build.sh [flags]"
echo ""
echo "Flags:"
echo " --release=<APP_RELEASE_VERSION> required (e.g. v0.27.1)"
echo ""
echo "Example: ./build.sh --release=v0.27.1 --platform=linux/amd64"
exit 1
fi
# Install yq if not present
if ! command -v yq &> /dev/null; then
echo "Installing yq..."
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${CPU_ARCH}
sudo chmod +x /usr/local/bin/yq
fi
cd $(dirname "$0")
string_replace(){
local file="$1"
local search="$2"
local replace="$3"
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s|$search|$replace|g" "$file"
else
sed -i "s|$search|$replace|g" "$file"
fi
}
remove_line(){
local file="$1"
local line="$2"
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' '/'$line'/d' "$file"
else
sed -i '/'$line'/d' "$file"
fi
}
update_env_file(){
local file="$1"
local key="$2"
local value="$3"
# if key is in file, replace it
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' 's|^'$key'=.*|'$key'='$value'|' "$file"
else
sed -i 's|^'$key'=.*|'$key'='$value'|' "$file"
fi
# if key not in file, add it
if ! grep -q "^$key=" "$file"; then
echo "$key=$value" >> "$file"
fi
}
build_dist_files(){
cp ./variables.env $DIST_DIR/plane.env
cp ../../../apps/proxy/Caddyfile.ce $DIST_DIR/Caddyfile
echo "" >> $DIST_DIR/plane.env
echo "" >> $DIST_DIR/plane.env
# update the plane.env file with the APP_RELEASE_VERSION
update_env_file $DIST_DIR/plane.env "APP_RELEASE_VERSION" "$APP_RELEASE_VERSION"
update_env_file $DIST_DIR/plane.env "APP_RELEASE" "$APP_RELEASE_VERSION"
update_env_file $DIST_DIR/plane.env "APP_VERSION" "$APP_RELEASE_VERSION"
update_env_file $DIST_DIR/plane.env "API_BASE_URL" "http://localhost:3004"
update_env_file $DIST_DIR/plane.env "SITE_ADDRESS" ":80"
# remove this line containing `plane-minio:9000`
remove_line $DIST_DIR/Caddyfile "plane-minio:9000" ""
# in caddyfile, update `reverse_proxy /spaces/* space:3000` to `reverse_proxy /spaces/* space:3002`
string_replace $DIST_DIR/Caddyfile "web:3000" "localhost:3001"
string_replace $DIST_DIR/Caddyfile "space:3000" "localhost:3002"
string_replace $DIST_DIR/Caddyfile "admin:3000" "localhost:3003"
string_replace $DIST_DIR/Caddyfile "api:8000" "localhost:3004"
string_replace $DIST_DIR/Caddyfile "live:3000" "localhost:3005"
# print docker build command
echo "------------------------------------------------"
echo "Run the following command to build the image:"
echo "------------------------------------------------"
echo ""
echo "docker build -t $IMAGE_NAME \\"
echo " -f $(pwd)/Dockerfile \\"
echo " --build-arg PLANE_VERSION=$APP_RELEASE_VERSION \\"
echo " $(pwd)"
echo ""
echo "------------------------------------------------"
}
main(){
# check if the dist directory exists
echo ""
if [ -d "$DIST_DIR" ]; then
echo "Cleaning existing dist directory..."
rm -rf $DIST_DIR
fi
echo "Creating dist directory..."
mkdir -p $DIST_DIR
echo ""
build_dist_files
if [ $? -ne 0 ]; then
echo "Error: Failed to build docker image"
exit 1
fi
}
main "$@"

View File

@@ -0,0 +1,169 @@
#!/bin/bash -e
print_header(){
clear
echo "------------------------------------------------"
echo "Plane Community (All-In-One)"
echo "------------------------------------------------"
echo ""
echo "You are required to pass below environment variables to the script"
echo " DOMAIN_NAME, DATABASE_URL, REDIS_URL, AMQP_URL"
echo " AWS_REGION, AWS_ACCESS_KEY_ID"
echo " AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET_NAME"
echo ""
echo "Other optional environment variables: "
echo " SITE_ADDRESS (default: ':80')"
echo " FILE_SIZE_LIMIT (default: 5242880)"
echo " APP_PROTOCOL (http or https)"
echo " SECRET_KEY (default: 60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5)"
echo " LIVE_SERVER_SECRET_KEY (default: htbqvBJAgpm9bzvf3r4urJer0ENReatceh)"
echo ""
echo ""
}
check_required_env(){
echo "Checking required environment variables..."
local keys=("DOMAIN_NAME" "DATABASE_URL" "REDIS_URL" "AMQP_URL"
"AWS_REGION" "AWS_ACCESS_KEY_ID" "AWS_SECRET_ACCESS_KEY" "AWS_S3_BUCKET_NAME")
local missing_keys=()
# Check if the environment variable is set and not empty
for key in "${keys[@]}"; do
if [ -z "${!key}" ]; then
echo " ❌ '$key' is not set or is empty"
missing_keys+=("$key")
fi
done
if [ ${#missing_keys[@]} -gt 0 ]; then
echo ""
exit 1
fi
# add checkmark
echo "✅ Required environment variables are available"
echo ""
}
update_env_value(){
local key="$1"
local value="$2"
# check if the file exists
if [ ! -f "plane.env" ]; then
echo "plane.env file not found"
exit 1
fi
# check if the key exists and add it if it doesn't
if ! grep -q "^$key=.*" plane.env; then
echo "${key}=${value}" >> plane.env
return 0
fi
# if key and value are not empty, update the value
if [ -n "$key" ] && [ -n "$value" ]; then
sed -i "s|^$key=.*|$key=$value|" plane.env
return 0
fi
}
check_pre_requisites(){
check_required_env
# check if the file exists
if [ ! -f "plane.env" ]; then
echo "plane.env file not found"
exit 1
fi
# add a new line to the end of the file
echo "" >> plane.env
echo "" >> plane.env
echo "✅ Pre-requisites checked"
echo ""
}
validate_domain_name() {
local domain="$1"
# Check if it's an IP address first
if [[ "$domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "IP"
return 0
fi
# FQDN validation regex
local fqdn_regex='^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.?$'
if [[ "$domain" =~ $fqdn_regex ]]; then
# Additional checks
if [[ ${#domain} -le 253 ]] && [[ ! "$domain" =~ \.\. ]] && [[ ! "$domain" =~ ^- ]] && [[ ! "$domain" =~ -\. ]]; then
echo "FQDN"
return 0
fi
fi
echo "INVALID"
return 1
}
update_env_file(){
echo "Updating environment file..."
# check if DOMAIN_NAME is valid IP address
local domain_type=$(validate_domain_name "$DOMAIN_NAME")
if [ "$domain_type" == "INVALID" ]; then
echo "DOMAIN_NAME is not a valid FQDN or IP address"
exit 1
fi
local app_protocol=${APP_PROTOCOL:-http}
update_env_value "APP_PROTOCOL" "$app_protocol"
update_env_value "DOMAIN_NAME" "$DOMAIN_NAME"
update_env_value "APP_DOMAIN" "$DOMAIN_NAME"
if [ -n "$SITE_ADDRESS" ]; then
update_env_value "SITE_ADDRESS" "$SITE_ADDRESS"
else
update_env_value "SITE_ADDRESS" ":80"
fi
update_env_value "WEB_URL" "$app_protocol://$DOMAIN_NAME"
update_env_value "CORS_ALLOWED_ORIGINS" "http://$DOMAIN_NAME,https://$DOMAIN_NAME"
# update database url
update_env_value "DATABASE_URL" "$DATABASE_URL"
update_env_value "REDIS_URL" "$REDIS_URL"
update_env_value "AMQP_URL" "$AMQP_URL"
# update aws credentials
update_env_value "AWS_REGION" "$AWS_REGION"
update_env_value "AWS_ACCESS_KEY_ID" "$AWS_ACCESS_KEY_ID"
update_env_value "AWS_SECRET_ACCESS_KEY" "$AWS_SECRET_ACCESS_KEY"
update_env_value "AWS_S3_BUCKET_NAME" "$AWS_S3_BUCKET_NAME"
update_env_value "AWS_S3_ENDPOINT_URL" "${AWS_S3_ENDPOINT_URL:-https://s3.${AWS_REGION}.amazonaws.com}"
update_env_value "BUCKET_NAME" "$AWS_S3_BUCKET_NAME"
update_env_value "USE_MINIO" "0"
# Optional environment variables
update_env_value "SECRET_KEY" "${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}"
update_env_value "FILE_SIZE_LIMIT" "${FILE_SIZE_LIMIT:-5242880}"
update_env_value "LIVE_SERVER_SECRET_KEY" "${LIVE_SERVER_SECRET_KEY:-htbqvBJAgpm9bzvf3r4urJer0ENReatceh}"
update_env_value "API_KEY_RATE_LIMIT" "${API_KEY_RATE_LIMIT:-60/minute}"
echo "✅ Environment file updated"
echo ""
}
main(){
print_header
check_pre_requisites
update_env_file
# load plane.env as exported variables
export $(grep -v '^#' plane.env | xargs)
/usr/local/bin/supervisord -c /etc/supervisor/conf.d/supervisor.conf
}
main "$@"

View File

@@ -0,0 +1,123 @@
[supervisord]
user=root
nodaemon=true
stderr_logfile=/app/logs/error/supervisor.err.log
stdout_logfile=/app/logs/access/supervisor.log
[program:migrator]
directory=/app/backend
command=sh -c "./bin/docker-entrypoint-migrator.sh"
autostart=true
autorestart=unexpected
stdout_logfile=/app/logs/access/migrator.log
stderr_logfile=/app/logs/error/migrator.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
priority=10
[program:web]
command=sh -c "node /app/web/apps/web/server.js"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/web.log
stderr_logfile=/app/logs/error/web.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
environment=PORT=3001,HOSTNAME=0.0.0.0
priority=15
[program:space]
command=sh -c "node /app/space/apps/space/server.js"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/space.log
stderr_logfile=/app/logs/error/space.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
environment=PORT=3002,HOSTNAME=0.0.0.0
priority=15
[program:admin]
command=sh -c "node /app/admin/apps/admin/server.js"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/admin.log
stderr_logfile=/app/logs/error/admin.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
environment=PORT=3003,HOSTNAME=0.0.0.0
priority=15
[program:api]
directory=/app/backend
command=sh -c "./bin/docker-entrypoint-api.sh"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/api.log
stdout_logfile_maxbytes=0
stderr_logfile=/app/logs/error/api.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
environment=PORT=3004,HOSTNAME=0.0.0.0
priority=15
[program:worker]
directory=/app/backend
command=sh -c "./bin/docker-entrypoint-worker.sh"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/worker.log
stdout_logfile_maxbytes=0
stderr_logfile=/app/logs/error/worker.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
priority=20
[program:beat]
directory=/app/backend
command=sh -c "./bin/docker-entrypoint-beat.sh"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/beat.log
stdout_logfile_maxbytes=0
stderr_logfile=/app/logs/error/beat.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
priority=20
[program:live]
command=sh -c "node /app/live/apps/live/dist/start.js"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/live.log
stdout_logfile_maxbytes=0
stderr_logfile=/app/logs/error/live.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
environment=PORT=3005,HOSTNAME=0.0.0.0
priority=20
[program:proxy]
directory=/app/proxy
command=sh -c "caddy run --config /app/proxy/Caddyfile"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/proxy.log
stdout_logfile_maxbytes=0
stderr_logfile=/app/logs/error/proxy.err.log
# stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=5
priority=20

View File

@@ -0,0 +1,53 @@
APP_DOMAIN=localhost
APP_RELEASE=stable
# If SSL Cert to be generated, set CERT_EMAIl="email <EMAIL_ADDRESS>"
CERT_EMAIL=
CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
SITE_ADDRESS=:80
# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
# CERT_ACME_DNS="acme_dns <CERT_DNS_PROVIDER> <CERT_DNS_PROVIDER_API_KEY>"
CERT_ACME_DNS=
WEB_URL=http://localhost
DEBUG=0
CORS_ALLOWED_ORIGINS=http://localhost
API_BASE_URL=http://localhost:3004
#DB SETTINGS
DATABASE_URL=
# REDIS SETTINGS
REDIS_URL=
# RabbitMQ Settings
AMQP_URL=
# Secret Key
SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
# DATA STORE SETTINGS
USE_MINIO=0
AWS_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_ENDPOINT_URL=https://s3.amazonaws.com
AWS_S3_BUCKET_NAME=
BUCKET_NAME=
FILE_SIZE_LIMIT=5242880
# Gunicorn Workers
GUNICORN_WORKERS=1
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT=60/minute
# Live Server Secret Key
LIVE_SERVER_SECRET_KEY=htbqvBJAgpm9bzvf3r4urJer0ENReatceh

View File

@@ -0,0 +1,630 @@
# Self Hosting
In this guide, we will walk you through the process of setting up a self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization.
We will cover two main options for setting up your self-hosted environment: using a cloud server or using your desktop. For the cloud server, we will use an AWS EC2 instance. For the desktop, we will use Docker to create a local environment.
Let's get started!
## Setting up Docker Environment
<details>
<summary>Option 1 - Using Cloud Server</summary>
<p>Best way to start is to create EC2 machine on AWS. It must have minimum of 2vCPU and 4GB RAM.</p>
<p>Run the below command to install docker engine.</p>
`curl -fsSL https://get.docker.com | sh -`
</details>
---
<details>
<summary>Option 2 - Using Desktop</summary>
#### For Mac
<ol>
<li> Download Docker Desktop for Mac from the <a href="https://hub.docker.com/editions/community/docker-ce-desktop-mac/" target="_blank">Docker Hub</a>. </li>
<li> Double-click the downloaded `.dmg` file and drag the Docker app icon to the Applications folder. </li>
<li>Open Docker Desktop from the Applications folder. You might be asked to provide your system password to install additional software.</li>
</ol>
#### For Windows:
<ol>
<li>Download Docker Desktop for Windows from the <a href="https://hub.docker.com/editions/community/docker-ce-desktop-windows/" target="_blank">Docker Hub</a>.</li>
<li>Run the installer and follow the instructions. You might be asked to enable Hyper-V and "Containers" Windows features.</li>
<li>Open Docker Desktop. You might be asked to log out and log back in, or restart your machine, for changes to take effect.</li>
</ol>
After installation, you can verify the installation by opening a terminal (Command Prompt on Windows, Terminal app on Mac) and running the command `docker --version`. This should display the installed version of Docker.
</details>
---
## Installing Plane
Installing plane is a very easy and minimal step process.
### Prerequisite
- Docker installed and running
- OS with bash scripting enabled (Ubuntu, Linux AMI, macos). Windows systems need to have [gitbash](https://git-scm.com/download/win)
- User context used must have access to docker services. In most cases, use sudo su to switch as root user
- Use the terminal (or gitbash) window to run all the future steps
### Downloading Latest Release
```
mkdir plane-selfhost
cd plane-selfhost
```
#### For *Docker Compose* based setup
```
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh
chmod +x setup.sh
```
#### For *Docker Swarm* based setup
```
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/swarm.sh
chmod +x setup.sh
```
---
### Proceed with setup
Above steps will set you ready to install and start plane services.
Lets get started by running the `./setup.sh` command.
This will prompt you with the below options.
#### Docker Compose
```bash
Select an Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
4) Restart
5) Upgrade
6) View Logs
7) Backup Data
8) Exit
Action [2]: 1
```
For the 1st time setup, type "1" as action input.
This will create a folder `plane-app` and will download 2 files inside that
- `docker-compose.yaml`
- `plane.env`
Again the `options [1-8]` will be popped up, and this time hit `8` to exit.
#### Docker Swarm
```bash
Select an Action you want to perform:
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 1
```
For the 1st time setup, type "1" as action input.
This will create a create a folder `plane-app` and will download 2 files inside that
- `docker-compose.yaml`
- `plane.env`
Again the `options [1-7]` will be popped up, and this time hit `7` to exit.
---
### Continue with setup - Environment Settings
Before proceeding, we suggest used to review `.env` file and set the values.
Below are the most import keys you must refer to. _<span style="color: #fcba03">You can use any text editor to edit this file</span>_.
> `LISTEN_HTTP_PORT` - This is default set to `80`. Make sure the port you choose to use is not preoccupied. (e.g `LISTEN_HTTP_PORT=8080`)
> `WEB_URL` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with LISTEN_HTTP_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
> `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with LISTEN_HTTP_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
There are many other settings you can play with, but we suggest you configure `EMAIL SETTINGS` as it will enable you to invite your teammates onto the platform.
---
### Continue with setup - Start Server (Docker Compose)
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to start the services
```bash
Select a Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
4) Restart
5) Upgrade
6) View Logs
7) Backup Data
8) Exit
Action [2]: 2
```
Expect something like this.
![Downloading docker images](images/download.png)
Be patient as it might take sometime based on download speed and system configuration. If all goes well, you must see something like this
![Downloading completed](images/started.png)
This is the confirmation that all images were downloaded and the services are up & running.
You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
---
### Stopping the Server / Remove Stack
In case you want to make changes to `plane.env` variables, we suggest you to stop the services before doing that.
#### Docker Compose
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `3` to stop the services
```bash
Select a Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
4) Restart
5) Upgrade
6) View Logs
7) Backup Data
8) Exit
Action [2]: 3
```
If all goes well, you must see something like this
![Stop Services](images/stopped.png)
#### Docker Swarm
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to stop the services
```bash
Select an Action you want to perform:
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 2
```
If all goes well, you will see the confirmation from docker cli
---
### Restarting the Server / Redeploy Stack
In case you want to make changes to `plane.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with `RESTART` / `REDEPLOY` option.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `4` to restart the services
#### Docker Compose
```bash
Select a Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
4) Restart
5) Upgrade
6) View Logs
7) Backup Data
8) Exit
Action [2]: 4
```
If all goes well, you must see something like this
![Restart Services](images/restart.png)
#### Docker Swarm
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 4
```
If all goes well, you will see the confirmation from docker cli
---
### Upgrading Plane Version
It is always advised to keep Plane up to date with the latest release.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release.
#### Docker Compose
```bash
Select a Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
4) Restart
5) Upgrade
6) View Logs
7) Backup Data
8) Exit
Action [2]: 5
```
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`.
You must expect the below message
![Alt text](images/upgrade.png)
Once done, choose `8` to exit from prompt.
> It is very important for you to validate the `plane.env` for the new changes.
Once done with making changes in `plane.env` file, jump on to `Start Server`
#### Docker Swarm
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release.
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 5
```
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`.
Once done, choose `7` to exit from prompt.
> It is very important for you to validate the `plane.env` for the new changes.
Once done with making changes in `plane.env` file, jump on to `Redeploy Stack`
---
### View Logs
There would a time when you might want to check what is happening inside the API, Worker or any other container.
Lets again run the `./setup.sh` command. You will again be prompted with the below options.
This time select `6` to view logs.
#### Docker Compose
```bash
Select a Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
4) Restart
5) Upgrade
6) View Logs
7) Backup Data
8) Exit
Action [2]: 6
```
#### Docker Swarm
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 6
```
#### Service Menu Options for Logs
This will further open sub-menu with list of services
```bash
Select a Service you want to view the logs for:
1) Web
2) Space
3) API
4) Worker
5) Beat-Worker
6) Migrator
7) Proxy
8) Redis
9) Postgres
10) Minio
11) RabbitMQ
0) Back to Main Menu
Service: 3
```
Select any of the service to view the logs e.g. `3`. Expect something similar to this
```bash
api-1 | Waiting for database...
api-1 | Database available!
api-1 | Waiting for database migrations to complete...
api-1 | Waiting for database migrations to complete...
api-1 | Waiting for database migrations to complete...
api-1 | Waiting for database migrations to complete...
api-1 | Waiting for database migrations to complete...
api-1 | Waiting for database migrations to complete...
api-1 | Waiting for database migrations to complete...
api-1 | No migrations Pending. Starting processes ...
api-1 | Instance registered
api-1 | ENABLE_SIGNUP loaded with value from environment variable.
api-1 | ENABLE_EMAIL_PASSWORD loaded with value from environment variable.
api-1 | ENABLE_MAGIC_LINK_LOGIN loaded with value from environment variable.
api-1 | GOOGLE_CLIENT_ID loaded with value from environment variable.
api-1 | GITHUB_CLIENT_ID loaded with value from environment variable.
api-1 | GITHUB_CLIENT_SECRET loaded with value from environment variable.
api-1 | EMAIL_HOST loaded with value from environment variable.
api-1 | EMAIL_HOST_USER loaded with value from environment variable.
api-1 | EMAIL_HOST_PASSWORD loaded with value from environment variable.
api-1 | EMAIL_PORT loaded with value from environment variable.
api-1 | EMAIL_FROM loaded with value from environment variable.
api-1 | EMAIL_USE_TLS loaded with value from environment variable.
api-1 | EMAIL_USE_SSL loaded with value from environment variable.
api-1 | OPENAI_API_KEY loaded with value from environment variable.
api-1 | GPT_ENGINE loaded with value from environment variable.
api-1 | UNSPLASH_ACCESS_KEY loaded with value from environment variable.
api-1 | Checking bucket...
api-1 | Bucket 'uploads' does not exist. Creating bucket...
api-1 | Bucket 'uploads' created successfully.
api-1 | Public read access policy set for bucket 'uploads'.
api-1 | Cache Cleared
api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Starting gunicorn 21.2.0
api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker
api-1 | [2024-05-02 03:56:01 +0000] [25] [INFO] Booting worker with pid: 25
api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Started server process [25]
api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Waiting for application startup.
api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] ASGI 'lifespan' protocol appears unsupported.
api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Application startup complete.
```
To exit this, use `CTRL+C` and then you will land on to the main-menu with the list of actions.
Similarly, you can view the logs of other services.
---
### Backup Data (Docker Compose)
There would a time when you might want to backup your data from docker volumes to external storage like S3 or drives.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `7` to Backup the data.
```bash
Select a Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
4) Restart
5) Upgrade
6) View Logs
7) Backup Data
8) Exit
Action [2]: 7
```
In response, you can find the backup folder
```bash
Backing Up plane-app_pgdata
Backing Up plane-app_redisdata
Backing Up plane-app_uploads
Backup completed successfully. Backup files are stored in /....../plane-app/backup/20240502-1120
```
---
### Restore Data (Docker Compose)
When you want to restore the previously backed-up data, follow the instructions below.
1. Make sure that Plane-CE is installed, started, and then stopped. This ensures that the Docker volumes are created.
1. Download the restore script using the command below. We suggest downloading it in the same folder as `setup.sh`.
```bash
curl -fsSL -o restore.sh https://github.com/makeplane/plane/releases/latest/download/restore.sh
chmod +x restore.sh
```
1. Execute the command below to restore your data.
```bash
./restore.sh <path to backup folder containing *.tar.gz files>
```
As an example, for a backup folder `/opt/plane-selfhost/plane-app/backup/20240722-0914`, expect the response below:
```bash
--------------------------------------------
____ _ /////////
| _ \| | __ _ _ __ ___ /////////
| |_) | |/ _` | '_ \ / _ \ ///// /////
| __/| | (_| | | | | __/ ///// /////
|_| |_|\__,_|_| |_|\___| ////
////
--------------------------------------------
Project management tool from the future
--------------------------------------------
Found /opt/plane-selfhost/plane-app/backup/20240722-0914/pgdata.tar.gz
.....Restoring plane-app_pgdata
.....Successfully restored volume plane-app_pgdata from pgdata.tar.gz
Found /opt/plane-selfhost/plane-app/backup/20240722-0914/redisdata.tar.gz
.....Restoring plane-app_redisdata
.....Successfully restored volume plane-app_redisdata from redisdata.tar.gz
Found /opt/plane-selfhost/plane-app/backup/20240722-0914/uploads.tar.gz
.....Restoring plane-app_uploads
.....Successfully restored volume plane-app_uploads from uploads.tar.gz
Restore completed successfully.
```
1. Start the Plane instance using `./setup.sh start`.
---
### Restore for Commercial Air-Gapped (Docker Compose)
When you want to restore the previously backed-up data on Plane Commercial Air-Gapped version, follow the instructions below.
1. Download the restore script using the command below
```bash
curl -fsSL -o restore-airgapped.sh https://github.com/makeplane/plane/releases/latest/download/restore-airgapped.sh
chmod +x restore-airgapped.sh
```
1. Copy the backup folder and the `restore-airgapped.sh` to `Commercial Airgapped Edition` server
1. Make sure that Plane Commercial (Airgapped) is extracted and ready to get started. In case it is running, you would need to stop that.
1. Execute the command below to restore your data.
```bash
./restore-airgapped.sh <path to backup folder containing *.tar.gz files>
```
1. After restoration, you are ready to start Plane Commercial (Airgapped) will all your previously saved data.
---
<details>
<summary><h2>Upgrading from v0.13.2 to v0.14.x</h2></summary>
This is one time activity for users who are upgrading from v0.13.2 to v0.14.0
As there has been significant changes to Self Hosting process, this step mainly covers the data migration from current (v0.13.2) docker volumes from newly created volumes
> Before we begin with migration, make sure your v0.14.0 was started and then stopped. This is required to know the newly created docker volume names.
Begin with downloading the migration script using below command
```
curl -fsSL -o migrate.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/migration-0.13-0.14.sh
chmod +x migrate.sh
```
Now run the `./migrate.sh` command and expect the instructions as below
```
******************************************************************
This script is solely for the migration purpose only.
This is a 1 time migration of volume data from v0.13.2 => v0.14.x
Assumption:
1. Postgres data volume name ends with _pgdata
2. Minio data volume name ends with _uploads
3. Redis data volume name ends with _redisdata
Any changes to this script can break the migration.
Before you proceed, make sure you run the below command
to know the docker volumes
docker volume ls -q | grep -i "_pgdata"
docker volume ls -q | grep -i "_uploads"
docker volume ls -q | grep -i "_redisdata"
*******************************************************
Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata"
---------------------
plane-app_redisdata
v0132_redisdata
Provide the Source Volume Prefix :
```
**Open another terminal window**, and run the mentioned 3 command. This may be different for users who have changed the volume names in their previous setup (v0.13.2)
For every command you must see 2 records something like shown in above example of `redisdata`
To move forward, you would need PREFIX of old setup and new setup. As per above example, `v0132` is the prefix of v0.13.2 and `plane-app` is the prefix of v0.14.0 setup
**Back to original terminal window**, _Provide the Source Volume Prefix_ and hit ENTER.
Now you will be prompted to _Provide Destination Volume Prefix_. Provide the value and hit ENTER
```
Provide the Source Volume Prefix : v0132
Provide the Destination Volume Prefix : plane-app
```
In case the suffixes are wrong or the mentioned volumes are not found, you will receive the error shown below. The image below displays an error for source volumes.
![Migrate Error](images/migrate-error.png)
In case of successful migration, it will be a silent exit without error.
Now its time to restart v0.14.0 setup.
</details>

View File

@@ -0,0 +1,36 @@
services:
web:
image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest}
build:
context: ../../
dockerfile: apps/web/Dockerfile.web
space:
image: ${DOCKERHUB_USER:-local}/plane-space:${APP_RELEASE:-latest}
build:
context: ../../
dockerfile: apps/space/Dockerfile.space
admin:
image: ${DOCKERHUB_USER:-local}/plane-admin:${APP_RELEASE:-latest}
build:
context: ../../
dockerfile: apps/admin/Dockerfile.admin
live:
image: ${DOCKERHUB_USER:-local}/plane-live:${APP_RELEASE:-latest}
build:
context: ../../
dockerfile: apps/live/Dockerfile.live
api:
image: ${DOCKERHUB_USER:-local}/plane-backend:${APP_RELEASE:-latest}
build:
context: ../../apps/api
dockerfile: Dockerfile.api
proxy:
image: ${DOCKERHUB_USER:-local}/plane-proxy:${APP_RELEASE:-latest}
build:
context: ../../apps/proxy
dockerfile: Dockerfile.ce

View File

@@ -0,0 +1,255 @@
x-db-env: &db-env
PGHOST: ${PGHOST:-plane-db}
PGDATABASE: ${PGDATABASE:-plane}
POSTGRES_USER: ${POSTGRES_USER:-plane}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
POSTGRES_DB: ${POSTGRES_DB:-plane}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
PGDATA: ${PGDATA:-/var/lib/postgresql/data}
x-redis-env: &redis-env
REDIS_HOST: ${REDIS_HOST:-plane-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/}
x-minio-env: &minio-env
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
x-aws-s3-env: &aws-s3-env
AWS_REGION: ${AWS_REGION:-}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-secret-key}
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
x-proxy-env: &proxy-env
APP_DOMAIN: ${APP_DOMAIN:-localhost}
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
CERT_EMAIL: ${CERT_EMAIL}
CERT_ACME_CA: ${CERT_ACME_CA}
CERT_ACME_DNS: ${CERT_ACME_DNS}
LISTEN_HTTP_PORT: ${LISTEN_HTTP_PORT:-80}
LISTEN_HTTPS_PORT: ${LISTEN_HTTPS_PORT:-443}
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
SITE_ADDRESS: ${SITE_ADDRESS:-:80}
x-mq-env: &mq-env # RabbitMQ Settings
RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane}
x-live-env: &live-env
API_BASE_URL: ${API_BASE_URL:-http://api:8000}
LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW}
x-app-env: &app-env
WEB_URL: ${WEB_URL:-http://localhost}
DEBUG: ${DEBUG:-0}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
GUNICORN_WORKERS: 1
USE_MINIO: ${USE_MINIO:-1}
DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW}
services:
web:
image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-stable}
deploy:
replicas: ${WEB_REPLICAS:-1}
restart_policy:
condition: any
depends_on:
- api
- worker
space:
image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-stable}
deploy:
replicas: ${SPACE_REPLICAS:-1}
restart_policy:
condition: any
depends_on:
- api
- worker
- web
admin:
image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-stable}
deploy:
replicas: ${ADMIN_REPLICAS:-1}
restart_policy:
condition: any
depends_on:
- api
- web
live:
image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-stable}
environment:
<<: [*live-env]
deploy:
replicas: ${LIVE_REPLICAS:-1}
restart_policy:
condition: any
depends_on:
- api
- web
api:
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
restart_policy:
condition: any
volumes:
- logs_api:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
- plane-mq
worker:
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-worker.sh
deploy:
replicas: ${WORKER_REPLICAS:-1}
restart_policy:
condition: any
volumes:
- logs_worker:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
beat-worker:
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-beat.sh
deploy:
replicas: ${BEAT_WORKER_REPLICAS:-1}
restart_policy:
condition: any
volumes:
- logs_beat-worker:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
migrator:
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-migrator.sh
deploy:
replicas: 1
restart_policy:
condition: on-failure
volumes:
- logs_migrator:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
# Comment this if you already have a database running
plane-db:
image: postgres:15.7-alpine
command: postgres -c 'max_connections=1000'
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *db-env
volumes:
- pgdata:/var/lib/postgresql/data
plane-redis:
image: valkey/valkey:7.2.11-alpine
deploy:
replicas: 1
restart_policy:
condition: any
volumes:
- redisdata:/data
plane-mq:
image: rabbitmq:3.13.6-management-alpine
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *mq-env
volumes:
- rabbitmq_data:/var/lib/rabbitmq
# Comment this if you using any external s3 compatible storage
plane-minio:
image: minio/minio:latest
command: server /export --console-address ":9090"
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *minio-env
volumes:
- uploads:/export
# Comment this if you already have a reverse proxy running
proxy:
image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-stable}
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *proxy-env
ports:
- target: 80
published: ${LISTEN_HTTP_PORT:-80}
protocol: tcp
mode: host
- target: 443
published: ${LISTEN_HTTPS_PORT:-443}
protocol: tcp
mode: host
volumes:
- proxy_config:/config
- proxy_data:/data
depends_on:
- web
- api
- space
- admin
- live
volumes:
pgdata:
redisdata:
uploads:
logs_api:
logs_worker:
logs_beat-worker:
logs_migrator:
rabbitmq_data:
proxy_config:
proxy_data:

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,713 @@
#!/bin/bash
BRANCH=${BRANCH:-master}
SCRIPT_DIR=$PWD
SERVICE_FOLDER=plane-app
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE=stable
export DOCKERHUB_USER=artifacts.plane.so/makeplane
export PULL_POLICY=${PULL_POLICY:-if_not_present}
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deployments/cli/community"
CPU_ARCH=$(uname -m)
OS_NAME=$(uname)
UPPER_CPU_ARCH=$(tr '[:lower:]' '[:upper:]' <<< "$CPU_ARCH")
mkdir -p $PLANE_INSTALL_DIR/archive
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
function print_header() {
clear
cat <<"EOF"
--------------------------------------------
____ _ /////////
| _ \| | __ _ _ __ ___ /////////
| |_) | |/ _` | '_ \ / _ \ ///// /////
| __/| | (_| | | | | __/ ///// /////
|_| |_|\__,_|_| |_|\___| ////
////
--------------------------------------------
Project management tool from the future
--------------------------------------------
EOF
}
function spinner() {
local pid=$1
local delay=.5
local spinstr='|/-\'
if ! ps -p "$pid" > /dev/null; then
echo "Invalid PID: $pid"
return 1
fi
while ps -p "$pid" > /dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr" >&2
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b" >&2
done
printf " \b\b\b\b" >&2
}
function checkLatestRelease(){
echo "Checking for the latest release..." >&2
local latest_release=$(curl -sSL https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g')
if [ -z "$latest_release" ]; then
echo "Failed to check for the latest release. Exiting..." >&2
exit 1
fi
echo $latest_release
}
function initialize(){
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${UPPER_CPU_ARCH} support." >&2
if [ "$CUSTOM_BUILD" == "true" ]; then
echo "" >&2
echo "" >&2
echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
echo "build"
return 1
fi
local IMAGE_NAME=makeplane/plane-proxy
local IMAGE_TAG=${APP_RELEASE}
docker manifest inspect "${IMAGE_NAME}:${IMAGE_TAG}" | grep -q "\"architecture\": \"${CPU_ARCH}\"" &
local pid=$!
spinner "$pid"
echo "" >&2
wait "$pid"
if [ $? -eq 0 ]; then
echo "Plane supports ${CPU_ARCH}" >&2
echo "available"
return 0
else
echo "" >&2
echo "" >&2
echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
echo "" >&2
echo "build"
return 1
fi
}
function getEnvValue() {
local key=$1
local file=$2
if [ -z "$key" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
grep -q "^$key=" "$file"
if [ $? -eq 0 ]; then
local value
value=$(grep "^$key=" "$file" | cut -d'=' -f2)
echo "$value"
else
echo ""
fi
fi
}
function updateEnvFile() {
local key=$1
local value=$2
local file=$3
if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
echo "$key=$value" >> "$file"
return
else
if [ "$OS_NAME" == "Darwin" ]; then
value=$(echo "$value" | sed 's/|/\\|/g')
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
else
sed -i "s/^$key=.*/$key=$value/g" "$file"
fi
fi
else
echo "File not found: $file"
exit 1
fi
}
function updateCustomVariables(){
echo "Updating custom variables..." >&2
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
echo "Custom variables updated successfully" >&2
}
function syncEnvFile(){
echo "Syncing environment variables..." >&2
if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
updateCustomVariables
# READ keys of plane.env and update the values from plane.env.bak
while IFS= read -r line
do
# ignore is the line is empty or starts with #
if [ -z "$line" ] || [[ $line == \#* ]]; then
continue
fi
key=$(echo "$line" | cut -d'=' -f1)
value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
if [ -n "$value" ]; then
updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
fi
done < "$DOCKER_ENV_PATH"
fi
echo "Environment variables synced successfully" >&2
}
function buildYourOwnImage(){
echo "Building images locally..."
export DOCKERHUB_USER="myplane"
export APP_RELEASE="local"
export PULL_POLICY="never"
CUSTOM_BUILD="true"
# checkout the code to ~/tmp/plane folder and build the images
local PLANE_TEMP_CODE_DIR=~/tmp/plane
rm -rf $PLANE_TEMP_CODE_DIR
mkdir -p $PLANE_TEMP_CODE_DIR
REPO=https://github.com/$GH_REPO.git
git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
cp "$PLANE_TEMP_CODE_DIR/deployments/cli/community/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
cd "$PLANE_TEMP_CODE_DIR" || exit
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
if [ $? -ne 0 ]; then
echo "Build failed. Exiting..."
exit 1
fi
echo "Build completed successfully"
echo ""
echo "You can now start the services by running the command: ./setup.sh start"
echo ""
}
function install() {
echo "Begin Installing Plane"
echo ""
if [ "$APP_RELEASE" == "stable" ]; then
export APP_RELEASE=$(checkLatestRelease)
fi
local build_image=$(initialize)
if [ "$build_image" == "build" ]; then
# ask for confirmation to continue building the images
echo "Do you want to continue with building the Docker images locally?"
read -p "Continue? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Exiting..."
exit 0
fi
fi
if [ "$build_image" == "build" ]; then
download "true"
else
download "false"
fi
}
function download() {
local LOCAL_BUILD=$1
cd $SCRIPT_DIR
TS=$(date +%s)
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
then
mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
fi
RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
else
# Fallback to download from the raw github url
RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
else
echo "Failed to download docker-compose.yml. HTTP Status: $STATUS"
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml"
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
exit 1
fi
fi
RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
else
# Fallback to download from the raw github url
RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
else
echo "Failed to download variables.env. HTTP Status: $STATUS"
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env"
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
exit 1
fi
fi
if [ -f "$DOCKER_ENV_PATH" ];
then
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
fi
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
syncEnvFile
if [ "$LOCAL_BUILD" == "true" ]; then
export DOCKERHUB_USER="myplane"
export APP_RELEASE="local"
export PULL_POLICY="never"
CUSTOM_BUILD="true"
buildYourOwnImage
if [ $? -ne 0 ]; then
echo ""
echo "Build failed. Exiting..."
exit 1
fi
updateCustomVariables
else
CUSTOM_BUILD="false"
updateCustomVariables
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull --policy always"
if [ $? -ne 0 ]; then
echo ""
echo "Failed to pull the images. Exiting..."
exit 1
fi
fi
echo ""
echo "Most recent version of Plane is now available for you to use"
echo ""
echo "In case of 'Upgrade', please check the 'plane.env 'file for any new variables and update them accordingly"
echo ""
}
function startServices() {
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --pull if_not_present --quiet-pull"
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
if [ -n "$migrator_container_id" ]; then
local idx=0
while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
local message=">> Waiting for Data Migration to finish"
local dots=$(printf '%*s' $idx | tr ' ' '.')
echo -ne "\r$message$dots"
((idx++))
sleep 1
done
fi
printf "\r\033[K"
echo ""
echo " Data Migration completed successfully ✅"
# if migrator exit status is not 0, show error message and exit
if [ -n "$migrator_container_id" ]; then
local migrator_exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container_id)
if [ $migrator_exit_code -ne 0 ]; then
echo "Plane Server failed to start ❌"
# stopServices
echo
echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
echo "Stop the services by running the command: ./setup.sh stop"
exit 1
fi
fi
local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api")
# Verify container exists
if [ -z "$api_container_id" ]; then
echo " Error: API container not found. Please check if services are running."
exit 1
fi
local idx2=0
local api_ready=true # assume success, flip on timeout
local max_wait_time=300 # 5 minutes timeout
local start_time=$(date +%s)
echo " Waiting for API Service to be ready..."
while ! docker exec "$api_container_id" python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/', timeout=3)" > /dev/null 2>&1; do
local current_time=$(date +%s)
local elapsed_time=$((current_time - start_time))
if [ $elapsed_time -gt $max_wait_time ]; then
echo ""
echo " API Service health check timed out after 5 minutes"
echo " Checking if API container is still running..."
if docker ps | grep -q "$SERVICE_FOLDER-api"; then
echo " API container is running but did not pass the health-check. Continuing without marking it ready."
api_ready=false
break
else
echo " API container is not running. Please check logs."
exit 1
fi
fi
local message=">> Waiting for API Service to Start (${elapsed_time}s)"
local dots=$(printf '%*s' $idx2 | tr ' ' '.')
echo -ne "\r$message$dots"
((idx2++))
sleep 1
done
printf "\r\033[K"
if [ "$api_ready" = true ]; then
echo " API Service started successfully ✅"
else
echo " ⚠️ API Service did not respond to health-check please verify manually."
fi
source "${DOCKER_ENV_PATH}"
echo " Plane Server started successfully ✅"
echo ""
echo " You can access the application at $WEB_URL"
echo ""
}
function stopServices() {
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH down"
}
function restartServices() {
stopServices
startServices
}
function upgrade() {
local latest_release=$(checkLatestRelease)
echo ""
echo "Current release: $APP_RELEASE"
if [ "$latest_release" == "$APP_RELEASE" ]; then
echo ""
echo "You are already using the latest release"
exit 0
fi
echo "Latest release: $latest_release"
echo ""
# Check for confirmation to upgrade
echo "Do you want to upgrade to the latest release ($latest_release)?"
read -p "Continue? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Exiting..."
exit 0
fi
export APP_RELEASE=$latest_release
echo "Upgrading Plane to the latest release..."
echo ""
echo "***** STOPPING SERVICES ****"
stopServices
echo
echo "***** DOWNLOADING STABLE VERSION ****"
install
echo "***** PLEASE VALIDATE AND START SERVICES ****"
}
function viewSpecificLogs(){
local SERVICE_NAME=$1
if /bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH ps | grep -q '$SERVICE_NAME'"; then
echo "Service '$SERVICE_NAME' is running."
else
echo "Service '$SERVICE_NAME' is not running."
fi
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH logs -f $SERVICE_NAME"
}
function viewLogs(){
ARG_SERVICE_NAME=$2
if [ -z "$ARG_SERVICE_NAME" ];
then
echo
echo "Select a Service you want to view the logs for:"
echo " 1) Web"
echo " 2) Space"
echo " 3) API"
echo " 4) Worker"
echo " 5) Beat-Worker"
echo " 6) Migrator"
echo " 7) Proxy"
echo " 8) Redis"
echo " 9) Postgres"
echo " 10) Minio"
echo " 11) RabbitMQ"
echo " 0) Back to Main Menu"
echo
read -p "Service: " DOCKER_SERVICE_NAME
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do
echo "Invalid selection. Please enter a number between 0 and 11."
read -p "Service: " DOCKER_SERVICE_NAME
done
if [ -z "$DOCKER_SERVICE_NAME" ];
then
echo "INVALID SERVICE NAME SUPPLIED"
else
case $DOCKER_SERVICE_NAME in
1) viewSpecificLogs "web";;
2) viewSpecificLogs "space";;
3) viewSpecificLogs "api";;
4) viewSpecificLogs "worker";;
5) viewSpecificLogs "beat-worker";;
6) viewSpecificLogs "migrator";;
7) viewSpecificLogs "proxy";;
8) viewSpecificLogs "plane-redis";;
9) viewSpecificLogs "plane-db";;
10) viewSpecificLogs "plane-minio";;
11) viewSpecificLogs "plane-mq";;
0) askForAction;;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
fi
elif [ -n "$ARG_SERVICE_NAME" ];
then
ARG_SERVICE_NAME=$(echo "$ARG_SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
case $ARG_SERVICE_NAME in
web) viewSpecificLogs "web";;
space) viewSpecificLogs "space";;
api) viewSpecificLogs "api";;
worker) viewSpecificLogs "worker";;
beat-worker) viewSpecificLogs "beat-worker";;
migrator) viewSpecificLogs "migrator";;
proxy) viewSpecificLogs "proxy";;
redis) viewSpecificLogs "plane-redis";;
postgres) viewSpecificLogs "plane-db";;
minio) viewSpecificLogs "plane-minio";;
rabbitmq) viewSpecificLogs "plane-mq";;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
else
echo "INVALID SERVICE NAME SUPPLIED"
fi
}
function backup_container_dir() {
local BACKUP_FOLDER=$1
local CONTAINER_NAME=$2
local CONTAINER_DATA_DIR=$3
local SERVICE_FOLDER=$4
echo "Backing up $CONTAINER_NAME data..."
local CONTAINER_ID=$(/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH ps -q $CONTAINER_NAME")
if [ -z "$CONTAINER_ID" ]; then
echo "Error: $CONTAINER_NAME container not found. Make sure the services are running."
return 1
fi
# Create a temporary directory for the backup
mkdir -p "$BACKUP_FOLDER/$SERVICE_FOLDER"
# Copy the data directory from the running container
echo "Copying $CONTAINER_NAME data directory..."
docker cp -q "$CONTAINER_ID:$CONTAINER_DATA_DIR/." "$BACKUP_FOLDER/$SERVICE_FOLDER/"
local cp_status=$?
if [ $cp_status -ne 0 ]; then
echo "Error: Failed to copy $SERVICE_FOLDER data"
rm -rf $BACKUP_FOLDER/$SERVICE_FOLDER
return 1
fi
# Create tar.gz of the data
cd "$BACKUP_FOLDER"
tar -czf "${SERVICE_FOLDER}.tar.gz" "$SERVICE_FOLDER/"
local tar_status=$?
if [ $tar_status -eq 0 ]; then
rm -rf "$SERVICE_FOLDER/"
fi
cd - > /dev/null
if [ $tar_status -ne 0 ]; then
echo "Error: Failed to create tar archive"
return 1
fi
echo "Successfully backed up $SERVICE_FOLDER data"
}
function backupData() {
local datetime=$(date +"%Y%m%d-%H%M")
local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime
mkdir -p "$BACKUP_FOLDER"
# Check if docker-compose.yml exists
if [ ! -f "$DOCKER_FILE_PATH" ]; then
echo "Error: docker-compose.yml not found at $DOCKER_FILE_PATH"
exit 1
fi
backup_container_dir "$BACKUP_FOLDER" "plane-db" "/var/lib/postgresql/data" "pgdata" || exit 1
backup_container_dir "$BACKUP_FOLDER" "plane-minio" "/export" "uploads" || exit 1
backup_container_dir "$BACKUP_FOLDER" "plane-mq" "/var/lib/rabbitmq" "rabbitmq_data" || exit 1
backup_container_dir "$BACKUP_FOLDER" "plane-redis" "/data" "redisdata" || exit 1
echo ""
echo "Backup completed successfully. Backup files are stored in $BACKUP_FOLDER"
echo ""
}
function askForAction() {
local DEFAULT_ACTION=$1
if [ -z "$DEFAULT_ACTION" ];
then
echo
echo "Select a Action you want to perform:"
echo " 1) Install"
echo " 2) Start"
echo " 3) Stop"
echo " 4) Restart"
echo " 5) Upgrade"
echo " 6) View Logs"
echo " 7) Backup Data"
echo " 8) Exit"
echo
read -p "Action [2]: " ACTION
until [[ -z "$ACTION" || "$ACTION" =~ ^[1-8]$ ]]; do
echo "$ACTION: invalid selection."
read -p "Action [2]: " ACTION
done
if [ -z "$ACTION" ];
then
ACTION=2
fi
echo
fi
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ];
then
install
# askForAction
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ];
then
startServices
# askForAction
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ];
then
stopServices
# askForAction
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ];
then
restartServices
# askForAction
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ];
then
upgrade
# askForAction
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ];
then
viewLogs "$@"
askForAction
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ];
then
backupData
elif [ "$ACTION" == "8" ]
then
exit 0
else
echo "INVALID ACTION SUPPLIED"
fi
}
# if docker-compose is installed
if command -v docker-compose &> /dev/null
then
COMPOSE_CMD="docker-compose"
else
COMPOSE_CMD="docker compose"
fi
if [ "$CPU_ARCH" == "x86_64" ] || [ "$CPU_ARCH" == "amd64" ]; then
CPU_ARCH="amd64"
elif [ "$CPU_ARCH" == "aarch64" ] || [ "$CPU_ARCH" == "arm64" ]; then
CPU_ARCH="arm64"
fi
if [ -f "$DOCKER_ENV_PATH" ]; then
DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
PULL_POLICY=$(getEnvValue "PULL_POLICY" "$DOCKER_ENV_PATH")
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=artifacts.plane.so/makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
if [ -z "$APP_RELEASE" ]; then
APP_RELEASE=stable
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
fi
if [ -z "$PULL_POLICY" ]; then
PULL_POLICY=if_not_present
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
fi
if [ -z "$CUSTOM_BUILD" ]; then
CUSTOM_BUILD=false
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
fi
fi
print_header
askForAction "$@"

View File

@@ -0,0 +1,118 @@
#!/bin/bash
echo '
******************************************************************
This script is solely for the migration purpose only.
This is a 1 time migration of volume data from v0.13.2 => v0.14.x
Assumption:
1. Postgres data volume name ends with _pgdata
2. Minio data volume name ends with _uploads
3. Redis data volume name ends with _redisdata
Any changes to this script can break the migration.
Before you proceed, make sure you run the below command
to know the docker volumes
docker volume ls -q | grep -i "_pgdata"
docker volume ls -q | grep -i "_uploads"
docker volume ls -q | grep -i "_redisdata"
*******************************************************
'
DOWNLOAD_FOL=./download
rm -rf ${DOWNLOAD_FOL}
mkdir -p ${DOWNLOAD_FOL}
function volumeExists {
if [ "$(docker volume ls -f name=$1 | awk '{print $NF}' | grep -E '^'$1'$')" ]; then
return 0
else
return 1
fi
}
function readPrefixes(){
echo ''
echo 'Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata" '
echo '---------------------'
docker volume ls -q | grep -i "_redisdata"
echo ''
read -p "Provide the Source Volume Prefix : " SRC_VOL_PREFIX
until [ "$SRC_VOL_PREFIX" ]; do
read -p "Provide the Source Volume Prefix : " SRC_VOL_PREFIX
done
read -p "Provide the Destination Volume Prefix : " DEST_VOL_PREFIX
until [ "$DEST_VOL_PREFIX" ]; do
read -p "Provide the Source Volume Prefix : " DEST_VOL_PREFIX
done
echo ''
echo 'Prefix Provided '
echo " Source : ${SRC_VOL_PREFIX}"
echo " Destination : ${DEST_VOL_PREFIX}"
echo '---------------------------------------'
}
function migrate(){
SRC_VOLUME=${SRC_VOL_PREFIX}_${VOL_NAME_SUFFIX}
DEST_VOLUME=${DEST_VOL_PREFIX}_${VOL_NAME_SUFFIX}
if volumeExists $SRC_VOLUME; then
if volumeExists $DEST_VOLUME; then
GOOD_TO_GO=1
else
echo "Destination Volume '$DEST_VOLUME' does not exist"
echo ''
fi
else
echo "Source Volume '$SRC_VOLUME' does not exist"
echo ''
fi
if [ $GOOD_TO_GO = 1 ]; then
echo "MIGRATING ${VOL_NAME_SUFFIX} FROM ${SRC_VOLUME} => ${DEST_VOLUME}"
TEMP_CONTAINER=$(docker run -d -v $SRC_VOLUME:$CONTAINER_VOL_FOLDER busybox true)
docker cp -q $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX}
docker rm $TEMP_CONTAINER &> /dev/null
TEMP_CONTAINER=$(docker run -d -v $DEST_VOLUME:$CONTAINER_VOL_FOLDER busybox true)
if [ "$VOL_NAME_SUFFIX" = "pgdata" ]; then
docker cp -q ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX} $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER/_temp
docker run --rm -v $DEST_VOLUME:$CONTAINER_VOL_FOLDER \
-e DATA_FOLDER="${CONTAINER_VOL_FOLDER}" \
busybox /bin/sh -c 'cp -Rf $DATA_FOLDER/_temp/* $DATA_FOLDER '
else
docker cp -q ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX} $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER
fi
docker rm $TEMP_CONTAINER &> /dev/null
echo ''
fi
}
readPrefixes
# MIGRATE DB
CONTAINER_VOL_FOLDER=/var/lib/postgresql/data
VOL_NAME_SUFFIX=pgdata
migrate
# MIGRATE REDIS
CONTAINER_VOL_FOLDER=/data
VOL_NAME_SUFFIX=redisdata
migrate
# MIGRATE MINIO
CONTAINER_VOL_FOLDER=/export
VOL_NAME_SUFFIX=uploads
migrate

View File

@@ -0,0 +1,144 @@
#!/bin/bash
+set -euo pipefail
function print_header() {
clear
cat <<"EOF"
--------------------------------------------
____ _ /////////
| _ \| | __ _ _ __ ___ /////////
| |_) | |/ _` | '_ \ / _ \ ///// /////
| __/| | (_| | | | | __/ ///// /////
|_| |_|\__,_|_| |_|\___| ////
////
--------------------------------------------
Project management tool from the future
--------------------------------------------
EOF
}
function restoreData() {
echo ""
echo "****************************************************"
echo "We are about to restore your data from the backup files."
echo "****************************************************"
echo ""
# set the backup folder path
BACKUP_FOLDER=${1}
if [ -z "$BACKUP_FOLDER" ]; then
BACKUP_FOLDER="$PWD/backup"
read -p "Enter the backup folder path [$BACKUP_FOLDER]: " BACKUP_FOLDER
if [ -z "$BACKUP_FOLDER" ]; then
BACKUP_FOLDER="$PWD/backup"
fi
fi
# check if the backup folder exists
if [ ! -d "$BACKUP_FOLDER" ]; then
echo "Error: Backup folder not found at $BACKUP_FOLDER"
exit 1
fi
# check if there are any .tar.gz files in the backup folder
if ! ls "$BACKUP_FOLDER"/*.tar.gz 1> /dev/null 2>&1; then
echo "Error: Backup folder does not contain .tar.gz files"
exit 1
fi
echo ""
echo "Using backup folder: $BACKUP_FOLDER"
echo ""
# ask for current install path
AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped"
read -p "Enter the airgapped instance install path [$AIRGAPPED_INSTALL_PATH]: " AIRGAPPED_INSTALL_PATH
if [ -z "$AIRGAPPED_INSTALL_PATH" ]; then
AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped"
fi
# check if the airgapped instance install path exists
if [ ! -d "$AIRGAPPED_INSTALL_PATH" ]; then
echo "Error: Airgapped instance install path not found at $AIRGAPPED_INSTALL_PATH"
exit 1
fi
echo ""
echo "Using airgapped instance install path: $AIRGAPPED_INSTALL_PATH"
echo ""
# check if the docker-compose.yaml exists
if [ ! -f "$AIRGAPPED_INSTALL_PATH/docker-compose.yml" ]; then
echo "Error: docker-compose.yml not found at $AIRGAPPED_INSTALL_PATH/docker-compose.yml"
exit 1
fi
local dockerServiceStatus
if command -v jq &> /dev/null; then
dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped --format=json | jq -r .[0].Status)
else
dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped | grep -o "running" | head -n 1)
fi
if [[ $dockerServiceStatus == "running" ]]; then
echo "Plane Airgapped is running. Please STOP the Plane Airgapped before restoring data."
exit 1
fi
CURRENT_USER_ID=$(id -u)
CURRENT_GROUP_ID=$(id -g)
# if the data folder not exists, create it
if [ ! -d "$AIRGAPPED_INSTALL_PATH/data" ]; then
mkdir -p "$AIRGAPPED_INSTALL_PATH/data"
chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data"
fi
for BACKUP_FILE in "$BACKUP_FOLDER/*.tar.gz"; do
if [ -e "$BACKUP_FILE" ]; then
# get the basefilename without the extension
BASE_FILE_NAME=$(basename "$BACKUP_FILE" ".tar.gz")
# extract the restoreFile to the airgapped instance install path
echo "Restoring $BASE_FILE_NAME"
rm -rf "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" || true
tar -xvzf "$BACKUP_FILE" -C "$AIRGAPPED_INSTALL_PATH/data/"
if [ $? -ne 0 ]; then
echo "Error: Failed to extract $BACKUP_FILE"
exit 1
fi
chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME"
if [ $? -ne 0 ]; then
echo "Error: Failed to change ownership of $AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME"
exit 1
fi
else
echo "No .tar.gz files found in the current directory."
echo ""
echo "Please provide the path to the backup file."
echo ""
echo "Usage: $0 /path/to/backup"
exit 1
fi
done
echo ""
echo "Restore completed successfully."
echo ""
}
# if docker-compose is installed
if command -v docker-compose &> /dev/null
then
COMPOSE_CMD="docker-compose"
else
COMPOSE_CMD="docker compose"
fi
print_header
restoreData "$@"

View File

@@ -0,0 +1,123 @@
#!/bin/bash
function print_header() {
clear
cat <<"EOF"
--------------------------------------------
____ _ /////////
| _ \| | __ _ _ __ ___ /////////
| |_) | |/ _` | '_ \ / _ \ ///// /////
| __/| | (_| | | | | __/ ///// /////
|_| |_|\__,_|_| |_|\___| ////
////
--------------------------------------------
Project management tool from the future
--------------------------------------------
EOF
}
function restoreSingleVolume() {
selectedVolume=$1
backupFolder=$2
restoreFile=$3
docker volume rm "$selectedVolume" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Error: Failed to remove volume $selectedVolume"
echo ""
return 1
fi
docker volume create "$selectedVolume" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Error: Failed to create volume $selectedVolume"
echo ""
return 1
fi
docker run --rm \
-e TAR_NAME="$restoreFile" \
-v "$selectedVolume":"/vol" \
-v "$backupFolder":/backup \
busybox sh -c 'mkdir -p /restore && tar -xzf "/backup/${TAR_NAME}.tar.gz" -C /restore && mv /restore/${TAR_NAME}/* /vol'
if [ $? -ne 0 ]; then
echo "Error: Failed to restore volume ${selectedVolume} from ${restoreFile}.tar.gz"
echo ""
return 1
fi
echo ".....Successfully restored volume $selectedVolume from ${restoreFile}.tar.gz"
echo ""
}
function restoreData() {
print_header
local BACKUP_FOLDER=${1:-$PWD}
local dockerServiceStatus
dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-app --format=json | jq -r .[0].Status)
local dockerServicePrefix
dockerServicePrefix="running"
if [[ $dockerServiceStatus == $dockerServicePrefix* ]]; then
echo "Plane App is running. Please STOP the Plane App before restoring data."
exit 1
fi
local volume_suffix
volume_suffix="_pgdata|_redisdata|_uploads|_rabbitmq_data"
local volumes
volumes=$(docker volume ls -f "name=plane-app" --format "{{.Name}}" | grep -E "$volume_suffix")
# Check if there are any matching volumes
if [ -z "$volumes" ]; then
echo ".....No volumes found starting with 'plane-app'"
exit 1
fi
for BACKUP_FILE in $BACKUP_FOLDER/*.tar.gz; do
if [ -e "$BACKUP_FILE" ]; then
local restoreFileName
restoreFileName=$(basename "$BACKUP_FILE")
restoreFileName="${restoreFileName%.tar.gz}"
local restoreVolName
restoreVolName="plane-app_${restoreFileName}"
echo "Found $BACKUP_FILE"
local docVol
docVol=$(docker volume ls -f "name=$restoreVolName" --format "{{.Name}}" | grep -E "$volume_suffix")
if [ -z "$docVol" ]; then
echo "Skipping: No volume found with name $restoreVolName"
else
echo ".....Restoring $docVol"
restoreSingleVolume "$docVol" "$BACKUP_FOLDER" "$restoreFileName"
fi
else
echo "No .tar.gz files found in the current directory."
echo ""
echo "Please provide the path to the backup file."
echo ""
echo "Usage: ./restore.sh /path/to/backup"
exit 1
fi
done
echo ""
echo "Restore completed successfully."
echo ""
}
# if docker-compose is installed
if command -v docker-compose &> /dev/null
then
COMPOSE_CMD="docker-compose"
else
COMPOSE_CMD="docker compose"
fi
restoreData "$@"

View File

@@ -0,0 +1,82 @@
APP_DOMAIN=localhost
APP_RELEASE=stable
WEB_REPLICAS=1
SPACE_REPLICAS=1
ADMIN_REPLICAS=1
API_REPLICAS=1
WORKER_REPLICAS=1
BEAT_WORKER_REPLICAS=1
LIVE_REPLICAS=1
LISTEN_HTTP_PORT=80
LISTEN_HTTPS_PORT=443
WEB_URL=http://${APP_DOMAIN}
DEBUG=0
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
API_BASE_URL=http://api:8000
#DB SETTINGS
PGHOST=plane-db
PGDATABASE=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=plane
POSTGRES_DB=plane
POSTGRES_PORT=5432
PGDATA=/var/lib/postgresql/data
DATABASE_URL=
# REDIS SETTINGS
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=
# RabbitMQ Settings
RABBITMQ_HOST=plane-mq
RABBITMQ_PORT=5672
RABBITMQ_USER=plane
RABBITMQ_PASSWORD=plane
RABBITMQ_VHOST=plane
AMQP_URL=
# If SSL Cert to be generated, set CERT_EMAIl="email <EMAIL_ADDRESS>"
CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
TRUSTED_PROXIES=0.0.0.0/0
SITE_ADDRESS=:80
CERT_EMAIL=
# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
# CERT_ACME_DNS="acme_dns <CERT_DNS_PROVIDER> <CERT_DNS_PROVIDER_API_KEY>"
CERT_ACME_DNS=
# Secret Key
SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
# DATA STORE SETTINGS
USE_MINIO=1
AWS_REGION=
AWS_ACCESS_KEY_ID=access-key
AWS_SECRET_ACCESS_KEY=secret-key
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880
# Gunicorn Workers
GUNICORN_WORKERS=1
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
# DOCKER_PLATFORM=linux/amd64
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT=60/minute
# Live server environment variables
# WARNING: You must set a secure value for LIVE_SERVER_SECRET_KEY in production environments.
LIVE_SERVER_SECRET_KEY=

View File

@@ -0,0 +1,5 @@
# Helm Chart: Plane Community
Click on the below link to access the helm chart instructions.
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/makeplane)](https://artifacthub.io/packages/helm/makeplane/plane-ce)

View File

@@ -0,0 +1,611 @@
#!/bin/bash
BRANCH=${BRANCH:-master}
SERVICE_FOLDER=plane-app
SCRIPT_DIR=$PWD
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE="stable"
export DOCKERHUB_USER=artifacts.plane.so/makeplane
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deployments/cli/community"
OS_NAME=$(uname)
# Create necessary directories
mkdir -p $PLANE_INSTALL_DIR/archive
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yml
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
function print_header() {
clear
cat <<"EOF"
--------------------------------------------
____ _ /////////
| _ \| | __ _ _ __ ___ /////////
| |_) | |/ _` | '_ \ / _ \ ///// /////
| __/| | (_| | | | | __/ ///// /////
|_| |_|\__,_|_| |_|\___| ////
////
--------------------------------------------
Project management tool from the future
--------------------------------------------
EOF
}
function checkLatestRelease(){
echo "Checking for the latest release..." >&2
local latest_release=$(curl -s https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g')
if [ -z "$latest_release" ]; then
echo "Failed to check for the latest release. Exiting..." >&2
exit 1
fi
echo $latest_release
}
# Function to read stack name from env file
function readStackName() {
if [ -f "$DOCKER_ENV_PATH" ]; then
local saved_stack_name=$(grep "^STACK_NAME=" "$DOCKER_ENV_PATH" | cut -d'=' -f2)
if [ -n "$saved_stack_name" ]; then
stack_name=$saved_stack_name
return 1
fi
fi
return 0
}
# Function to get stack name (either from env or user input)
function getStackName() {
read -p "Enter stack name [plane]: " input_stack_name
if [ -z "$input_stack_name" ]; then
input_stack_name="plane"
fi
stack_name=$input_stack_name
updateEnvFile "STACK_NAME" "$stack_name" "$DOCKER_ENV_PATH"
echo "Using stack name: $stack_name"
}
function syncEnvFile(){
echo "Syncing environment variables..." >&2
if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
# READ keys of plane.env and update the values from plane.env.bak
while IFS= read -r line
do
# ignore if the line is empty or starts with #
if [ -z "$line" ] || [[ $line == \#* ]]; then
continue
fi
key=$(echo "$line" | cut -d'=' -f1)
value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
if [ -n "$value" ]; then
updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
fi
done < "$DOCKER_ENV_PATH"
value=$(getEnvValue "STACK_NAME" "$PLANE_INSTALL_DIR/plane.env.bak")
if [ -n "$value" ]; then
updateEnvFile "STACK_NAME" "$value" "$DOCKER_ENV_PATH"
fi
fi
echo "Environment variables synced successfully" >&2
rm -f $PLANE_INSTALL_DIR/plane.env.bak
}
function getEnvValue() {
local key=$1
local file=$2
if [ -z "$key" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
grep -q "^$key=" "$file"
if [ $? -eq 0 ]; then
local value
value=$(grep "^$key=" "$file" | cut -d'=' -f2)
echo "$value"
else
echo ""
fi
fi
}
function updateEnvFile() {
local key=$1
local value=$2
local file=$3
if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
echo "$key=$value" >> "$file"
return
else
if [ "$OS_NAME" == "Darwin" ]; then
value=$(echo "$value" | sed 's/|/\\|/g')
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
else
sed -i "s/^$key=.*/$key=$value/g" "$file"
fi
fi
else
echo "File not found: $file"
exit 1
fi
}
function download() {
cd $SCRIPT_DIR || exit 1
TS=$(date +%s)
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yml" ]; then
mv $PLANE_INSTALL_DIR/docker-compose.yml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml
fi
echo $RELEASE_DOWNLOAD_URL
echo $FALLBACK_DOWNLOAD_URL
echo $APP_RELEASE
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yml
else
# Fallback to download from the raw github url
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yml
else
echo "Failed to download docker-compose.yml. HTTP Status: $STATUS"
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml"
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml $PLANE_INSTALL_DIR/docker-compose.yml
exit 1
fi
fi
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
else
# Fallback to download from the raw github url
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
else
echo "Failed to download variables.env. HTTP Status: $STATUS"
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env"
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml $PLANE_INSTALL_DIR/docker-compose.yml
exit 1
fi
fi
if [ -f "$DOCKER_ENV_PATH" ];
then
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
fi
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
syncEnvFile
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
}
function deployStack() {
# Check if docker compose file and env file exist
if [ ! -f "$DOCKER_FILE_PATH" ] || [ ! -f "$DOCKER_ENV_PATH" ]; then
echo "Configuration files not found"
echo "Downloading it now......"
APP_RELEASE=$(checkLatestRelease)
download
fi
if [ -z "$stack_name" ]; then
getStackName
fi
echo "Starting ${stack_name} stack..."
# Pull envs
if [ -f "$DOCKER_ENV_PATH" ]; then
set -o allexport; source $DOCKER_ENV_PATH; set +o allexport;
else
echo "Environment file not found: $DOCKER_ENV_PATH"
exit 1
fi
# Deploy the stack
docker stack deploy -c $DOCKER_FILE_PATH $stack_name
echo "Waiting for services to be deployed..."
sleep 10
# Check migrator service
local migrator_service=$(docker service ls --filter name=${stack_name}_migrator -q)
if [ -n "$migrator_service" ]; then
echo ">> Waiting for Data Migration to finish"
while docker service ls --filter name=${stack_name}_migrator | grep -q "running"; do
echo -n "."
sleep 1
done
echo ""
# Get the most recent container for the migrator service
local migrator_container=$(docker ps -a --filter name=${stack_name}_migrator --latest -q)
if [ -n "$migrator_container" ]; then
# Get the exit code of the container
local exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container)
if [ "$exit_code" != "0" ]; then
echo "Server failed to start ❌"
echo "Migration failed with exit code: $exit_code"
echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
echo "Stop the services by running the command: ./swarm.sh stop"
exit 1
else
echo " Data Migration completed successfully ✅"
fi
else
echo "Warning: Could not find migrator container to check exit status"
fi
fi
# Check API service
local api_service=$(docker service ls --filter name=${stack_name}_api -q)
while docker service ls --filter name=${stack_name}_api | grep -q "running"; do
local running_container=$(docker ps --filter "name=${stack_name}_api" --filter "status=running" -q)
if [ -n "$running_container" ]; then
if docker container logs $running_container 2>/dev/null | grep -q "Application Startup Complete"; then
break
fi
fi
sleep 2
done
if [ -z "$api_service" ]; then
echo "Plane Server failed to start ❌"
echo "Please check the logs for the 'api' service and resolve the issue(s)."
echo "Stop the services by running the command: ./swarm.sh stop"
exit 1
fi
echo " Plane Server started successfully ✅"
echo ""
echo " You can access the application at $WEB_URL"
echo ""
}
function removeStack() {
if [ -z "$stack_name" ]; then
echo "Stack name not found"
exit 1
fi
echo "Removing ${stack_name} stack..."
docker stack rm "$stack_name"
echo "Waiting for services to be removed..."
while docker stack ls | grep -q "$stack_name"; do
sleep 1
done
sleep 20
echo "Services stopped successfully ✅"
}
function viewStatus() {
echo "Checking status of ${stack_name} stack..."
if [ -z "$stack_name" ]; then
echo "Stack name not found"
exit 1
fi
docker stack ps "$stack_name"
}
function redeployStack() {
removeStack
echo "ReDeploying ${stack_name} stack..."
deployStack
}
function upgrade() {
echo "Checking status of ${stack_name} stack..."
if [ -z "$stack_name" ]; then
echo "Stack name not found"
exit 1
fi
local latest_release=$(checkLatestRelease)
echo ""
echo "Current release: $APP_RELEASE"
if [ "$latest_release" == "$APP_RELEASE" ]; then
echo ""
echo "You are already using the latest release"
exit 0
fi
echo "Latest release: $latest_release"
echo ""
# Check for confirmation to upgrade
echo "Do you want to upgrade to the latest release ($latest_release)?"
read -p "Continue? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Exiting..."
exit 0
fi
export APP_RELEASE=$latest_release
# check if stack exists
echo "Upgrading ${stack_name} stack..."
# check env file and take backup
if [ -f "$DOCKER_ENV_PATH" ]; then
cp "$DOCKER_ENV_PATH" "${DOCKER_ENV_PATH}.bak"
fi
download
redeployStack
}
function viewSpecificLogs() {
local service=$1
# Input validation
if [ -z "$service" ]; then
echo "Error: Please specify a service name"
return 1
fi
# Main loop for service logs
while true; do
# Get all running containers for the service
local running_containers=$(docker ps --filter "name=${stack_name}_${service}" --filter "status=running" -q)
# If no running containers found, try service logs
if [ -z "$running_containers" ]; then
echo "No running containers found for ${stack_name}_${service}, checking service logs..."
if docker service inspect ${stack_name}_${service} >/dev/null 2>&1; then
echo "Press Ctrl+C or 'q' to exit logs"
docker service logs ${stack_name}_${service} -f
break
else
echo "Error: No running containers or services found for ${stack_name}_${service}"
return 1
fi
return
fi
# If multiple containers are running, let user choose
if [ $(echo "$running_containers" | grep -v '^$' | wc -l) -gt 1 ]; then
clear
echo "Multiple containers found for ${stack_name}_${service}:"
local i=1
# Use regular arrays instead of associative arrays
container_ids=()
container_names=()
while read -r container_id; do
if [ -n "$container_id" ]; then
local container_name=$(docker inspect --format '{{.Name}}' "$container_id" | sed 's/\///')
container_ids[$i]=$container_id
container_names[$i]=$container_name
echo "[$i] ${container_names[$i]} (${container_ids[$i]})"
i=$((i+1))
fi
done <<< "$running_containers"
echo -e "\nPlease select a container number:"
read -r selection
if [[ "$selection" =~ ^[0-9]+$ ]] && [ -n "${container_ids[$selection]}" ]; then
local selected_container=${container_ids[$selection]}
clear
echo "Showing logs for container: ${container_names[$selection]}"
echo "Press Ctrl+C or 'q' to return to container selection"
# Start watching logs in the background
docker container logs -f "$selected_container" &
local log_pid=$!
while true; do
read -r -n 1 input
if [[ $input == "q" ]]; then
kill $log_pid 2>/dev/null
wait $log_pid 2>/dev/null
break
fi
done
clear
else
echo "Error: Invalid selection"
sleep 2
fi
else
# Single container case
local container_name=$(docker inspect --format '{{.Name}}' "$running_containers" | sed 's/\///')
echo "Showing logs for container: $container_name"
echo "Press Ctrl+C or 'q' to exit logs"
docker container logs -f "$running_containers" &
local log_pid=$!
while true; do
read -r -n 1 input
if [[ $input == "q" ]]; then
kill $log_pid 2>/dev/null
wait $log_pid 2>/dev/null
break
fi
done
break
fi
done
}
function viewLogs(){
ARG_SERVICE_NAME=$2
if [ -z "$ARG_SERVICE_NAME" ];
then
echo
echo "Select a Service you want to view the logs for:"
echo " 1) Web"
echo " 2) Space"
echo " 3) API"
echo " 4) Worker"
echo " 5) Beat-Worker"
echo " 6) Migrator"
echo " 7) Proxy"
echo " 8) Redis"
echo " 9) Postgres"
echo " 10) Minio"
echo " 11) RabbitMQ"
echo " 0) Back to Main Menu"
echo
read -p "Service: " DOCKER_SERVICE_NAME
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do
echo "Invalid selection. Please enter a number between 0 and 11."
read -p "Service: " DOCKER_SERVICE_NAME
done
if [ -z "$DOCKER_SERVICE_NAME" ];
then
echo "INVALID SERVICE NAME SUPPLIED"
else
case $DOCKER_SERVICE_NAME in
1) viewSpecificLogs "web";;
2) viewSpecificLogs "space";;
3) viewSpecificLogs "api";;
4) viewSpecificLogs "worker";;
5) viewSpecificLogs "beat-worker";;
6) viewSpecificLogs "migrator";;
7) viewSpecificLogs "proxy";;
8) viewSpecificLogs "plane-redis";;
9) viewSpecificLogs "plane-db";;
10) viewSpecificLogs "plane-minio";;
11) viewSpecificLogs "plane-mq";;
0) askForAction;;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
fi
elif [ -n "$ARG_SERVICE_NAME" ];
then
ARG_SERVICE_NAME=$(echo "$ARG_SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
case $ARG_SERVICE_NAME in
web) viewSpecificLogs "web";;
space) viewSpecificLogs "space";;
api) viewSpecificLogs "api";;
worker) viewSpecificLogs "worker";;
beat-worker) viewSpecificLogs "beat-worker";;
migrator) viewSpecificLogs "migrator";;
proxy) viewSpecificLogs "proxy";;
redis) viewSpecificLogs "plane-redis";;
postgres) viewSpecificLogs "plane-db";;
minio) viewSpecificLogs "plane-minio";;
rabbitmq) viewSpecificLogs "plane-mq";;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
else
echo "INVALID SERVICE NAME SUPPLIED"
fi
}
function askForAction() {
# Rest of askForAction remains the same but use $stack_name instead of $STACK_NAME
local DEFAULT_ACTION=$1
if [ -z "$DEFAULT_ACTION" ]; then
echo
echo "Select an Action you want to perform:"
echo " 1) Deploy Stack"
echo " 2) Remove Stack"
echo " 3) View Stack Status"
echo " 4) Redeploy Stack"
echo " 5) Upgrade"
echo " 6) View Logs"
echo " 7) Exit"
echo
read -p "Action [3]: " ACTION
until [[ -z "$ACTION" || "$ACTION" =~ ^[1-6]$ ]]; do
echo "$ACTION: invalid selection."
read -p "Action [3]: " ACTION
done
if [ -z "$ACTION" ]; then
ACTION=3
fi
echo
fi
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "deploy" ]; then
deployStack
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "remove" ]; then
removeStack
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "status" ]; then
viewStatus
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "redeploy" ]; then
redeployStack
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]; then
upgrade
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ]; then
viewLogs "$@"
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "exit" ]; then
exit 0
else
echo "INVALID ACTION SUPPLIED"
fi
}
# Initialize stack name at script start
if [ -z "$stack_name" ]; then
readStackName
fi
# Sync environment variables
if [ -f "$DOCKER_ENV_PATH" ]; then
DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=artifacts.plane.so/makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
if [ -z "$APP_RELEASE" ]; then
APP_RELEASE=stable
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
fi
fi
# Main execution
print_header
askForAction "$@"