Skip to main content

Install & Setup

Docker Compose

Run PE Community and its required services with persistent storage and a single HTTPS entry point.

Introduction

Use Docker Compose to run PE Community, PostgreSQL, Redis, background processing, and the HTTPS entry point as one managed deployment. Named volumes preserve application data when containers are recreated.

Before you start

  • Install Docker Engine and the Docker Compose plugin.
  • Copy and configure `.env` as described in Environment variables.
  • Create a DNS record for APP_DOMAIN when using automatic HTTPS.
  • Allow public traffic to ports 80 and 443.
  • Reserve enough disk space for PostgreSQL, uploaded files, Redis state, TLS data, and backups.

Production compose file

Use the repository `docker-compose.prod.yml` beside `.env`. It builds the API, worker, and web images from the checked-out source and starts the supporting services.

compose.ymlYAML
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: pe
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: pe_community
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U pe -d pe_community"]
      interval: 5s
      timeout: 5s
      retries: 20

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 20

  api:
    image: ghcr.io/pona-ekolo/pe-community-api:${PE_COMMUNITY_VERSION:?set PE_COMMUNITY_VERSION in .env}
    restart: unless-stopped
    env_file: .env
    environment:
      NODE_ENV: production
      API_PORT: "4000"
      DATABASE_URL: postgresql://pe:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/pe_community?schema=public
      REDIS_URL: redis://redis:6379
      WEB_ORIGIN: ${WEB_ORIGIN:?set WEB_ORIGIN in .env}
      API_PUBLIC_URL: ${API_PUBLIC_URL:-${WEB_ORIGIN:?set WEB_ORIGIN in .env}}
      UPLOADS_DIR: /app/uploads
    volumes:
      - uploads_data:/app/uploads
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    expose:
      - "4000"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:4000/health >/dev/null 2>&1"]
      interval: 10s
      timeout: 5s
      retries: 20

  worker:
    image: ghcr.io/pona-ekolo/pe-community-worker:${PE_COMMUNITY_VERSION:?set PE_COMMUNITY_VERSION in .env}
    restart: unless-stopped
    env_file: .env
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://pe:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/pe_community?schema=public
      REDIS_URL: redis://redis:6379
      UPLOADS_DIR: /app/uploads
    volumes:
      - uploads_data:/app/uploads
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  web:
    image: ghcr.io/pona-ekolo/pe-community-web:${PE_COMMUNITY_VERSION:?set PE_COMMUNITY_VERSION in .env}
    restart: unless-stopped
    env_file: .env
    environment:
      NODE_ENV: production
      WEB_PORT: "3000"
      INTERNAL_API_URL: ${INTERNAL_API_URL:-http://api:4000}
      NEXT_PUBLIC_API_URL: "${NEXT_PUBLIC_API_URL:-/api/v1}"
      NEXT_PUBLIC_REALTIME_ORIGIN: "${NEXT_PUBLIC_REALTIME_ORIGIN:-}"
    depends_on:
      api:
        condition: service_healthy
    expose:
      - "3000"

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    environment:
      APP_DOMAIN: "${APP_DOMAIN:-:80}"
    ports:
      - "${HTTP_PORT:-80}:80"
      - "${HTTPS_PORT:-443}:443"
      - "${HTTPS_PORT:-443}:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      web:
        condition: service_started
      api:
        condition: service_healthy

volumes:
  postgres_data:
  redis_data:
  uploads_data:
  caddy_data:
  caddy_config:

Caddyfile

The repository Caddyfile is mounted by the production Compose file. Route order is significant: realtime, API, and uploaded-file requests reach the application before the final interface route.

CaddyfileNginx
{$APP_DOMAIN::80} {
	encode zstd gzip

	handle /socket.io* {
		reverse_proxy api:4000
	}

	handle_path /api/v1/* {
		reverse_proxy api:4000
	}

	handle /uploads/* {
		reverse_proxy api:4000
	}

	handle {
		reverse_proxy web:3000
	}
}

Services

PostgreSQL, Redis, and the application request service have health checks. Startup dependencies wait for the required healthy services. Pending database migrations are applied by the application image before it begins serving requests.

ServiceRolePersistent data
postgresStores community and application data.postgres_data
redisSupports background jobs and temporary application state.redis_data
apiHandles requests, authentication, uploads, and realtime connections.uploads_data
workerProcesses background work such as email and notifications.Shared uploads and database records
webServes the PE Community application interface.None
caddyProvides the HTTPS entry point and routes application traffic.caddy_data, caddy_config

Networking and HTTPS

Only Caddy needs to be publicly reachable. PostgreSQL, Redis, the application request service, and the interface remain internal to the Compose deployment.

The standard deployment uses the same public origin for the interface, REST requests, uploads, and realtime connections. Published interface images are expected to use `/api/v1` and an empty explicit realtime origin. A custom browser endpoint requires an image built for that endpoint.

PortPurpose
80/tcpHTTP access, redirects, and certificate validation.
443/tcpHTTPS application traffic.
443/udpHTTP/3 application traffic.

Compose-specific environment values

Keep the full configuration in `.env` and use Environment variables as the reference. The values below control Compose identity, the public address, and same-origin browser routing.

.envEnvironment
COMPOSE_PROJECT_NAME=pe-community
POSTGRES_PASSWORD=<strong-url-safe-database-password>

APP_DOMAIN=community.example.com
WEB_ORIGIN=https://community.example.com

NEXT_PUBLIC_API_URL=/api/v1
NEXT_PUBLIC_REALTIME_ORIGIN=

Start and check status

Start the deployment, then confirm that all expected services are running. PostgreSQL, Redis, and the application request service should report healthy.

TerminalTerminal
docker compose -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml ps

View logs

Use combined logs for the full startup sequence, or follow one service while diagnosing a specific problem.

  • api: startup, database migrations, requests, uploads, and realtime errors.
  • worker: background jobs, notifications, and email delivery.
  • caddy: HTTPS, certificate, and routing errors.
TerminalTerminal
docker compose logs -f
docker compose logs -f api
docker compose logs -f worker
docker compose logs -f caddy

Restart and stop

Restart keeps the existing containers and volumes. Stop pauses containers. Down removes containers and the Compose network but retains named volumes unless you add `-v`.

TerminalTerminal
docker compose restart
docker compose restart worker
docker compose stop
docker compose down

Persistent data

VolumeContainsBackup priority
postgres_dataCommunity and application database.Critical
uploads_dataUploaded files and attachments.Critical
redis_dataPersistent queue and background-job state.Operational
caddy_dataTLS certificates and Caddy runtime state.Important
caddy_configCaddy configuration runtime state.Useful

Keep the project name stable

Set `COMPOSE_PROJECT_NAME=pe-community` before the first start and keep it stable. Docker Compose uses the project name to identify the deployment’s containers, network, and named volumes. Changing it can create a different volume set and make existing data appear missing.

Use `docker compose ps` and `docker volume ls` when you need to inspect the resources associated with a deployment.

Apply updates

After reviewing a source release, rebuild and recreate the application containers. Named volumes remain attached when the Compose project name is unchanged.

Back up the installation before a release that includes database changes. Database migrations run automatically during application startup.

TerminalTerminal
docker compose -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml ps

Security expectations

  • Expose only Caddy’s public ports during normal operation.
  • Keep PostgreSQL and Redis internal to the deployment.
  • Keep `.env` private and never commit secrets.
  • Do not mount the Docker daemon socket or grant privileged container access.
  • Treat named volumes and backups as sensitive data.

Troubleshooting

  • Containers do not start: run `docker compose ps` and `docker compose logs`, then check required values in `.env`.
  • The database is unavailable: inspect `docker compose logs postgres` and `docker compose logs api`, then check credentials and persistent storage.
  • Background jobs or email are not processed: inspect `docker compose logs worker` and `docker compose logs redis`.
  • HTTPS is unavailable: inspect `docker compose logs caddy`, then check DNS, APP_DOMAIN, firewall rules, and ports 80 and 443.
  • Realtime or chat does not connect: check Caddy and application logs, then inspect the browser’s WebSocket request.
  • Data appears missing after redeployment: confirm COMPOSE_PROJECT_NAME did not change and inspect existing named volumes.