Skip to main content

Self-host Contember

caution

This is an advanced guide. You don't need this - skip the hassle and deploy to Contember Cloud. To complete this guide, you are expected to know how to run Docker containers in production.

What you'll need

  1. PostgreSQL database (e.g. AWS RDS for PostgreSQL, DigitalOcean Managed Database)
  2. S3-compatible storage for file uploads (optional if you don't need file upload capability; e.g. AWS S3, DigitalOcean Spaces, MinIO, CEPH, Zenko CloudServer)
  3. SMTP server to send emails from (optional)
  4. Some way to run Docker image.

Deploy Contember Engine

1. Setup PostgresSQL database

Required PostgresSQL version is 10 or newer.

Contember needs the following databases:

  • tenant (for information about users and their roles) and
  • content database for each deployed project.

Contember itself manages these databases and runs migrations upon startup. You don't need to create content database if the PostgresSQL user given to Contember has permission to create it on its own.

2. Setup S3 bucket

This bucket will be used to store user uploads. You will need a key and secret with permissions to create objects.

3. Setup SMTP server

Get SMTP credentials for your mailing server - used for example for password reset.

4. Run Contember Engine Docker container

Contember Engine is deployed as a Docker container. It's published on Docker Hub as contember/engine.

Configure the following environment variables:

NODE_ENV: "production"
CONTEMBER_PORT: "4000" # Port on which the service will be available
CONTEMBER_LOGIN_TOKEN: "..." # Login token used (20 random [0-9a-f] characters; can be generated by `openssl rand -hex 20` command)
CONTEMBER_ROOT_TOKEN: "..." # Root token used (20 random [0-9a-f] characters; can be generated by `openssl rand -hex 20` command)
CONTEMBER_ROOT_EMAIL: "[email protected]" # Superadmin user's e-mail
CONTEMBER_ROOT_PASSWORD: "my-secret-password" # Superadmin user's password

# Credentials for database:
DEFAULT_DB_HOST: "your-db-server.com"
DEFAULT_DB_PORT: "5432"
DEFAULT_DB_USER: "postgres"
DEFAULT_DB_PASSWORD: "postgres"
DEFAULT_DB_SSL: "true"
TENANT_DB_NAME: "tenant" # Name of tenant database - see first step
DEFAULT_DB_NAME: "my-project" # Optional - name of database to
# or BLOG_DB_NAME: "blog" where blog is slug of the project.

# S3 credentials - see step 2
DEFAULT_S3_REGION: "rgn1"
DEFAULT_S3_KEY: "..."
DEFAULT_S3_SECRET: "..."
DEFAULT_S3_ENDPOINT: "https://rgn1.yours3provider.com"
DEFAULT_S3_BUCKET: "your-bucket"

# SMTP credentials - see step 3
TENANT_MAILER_HOST: "send.your-smtp.com"
TENANT_MAILER_PORT: "2525"
TENANT_MAILER_FROM: "[email protected]"

HTTP server should start on the specified port. You can add a load-balancer in front of the app. The container is stateless and therefore horizontally scalable.

After the server is running Contember CLI can be used to set up other API tokens and invite users. Deploy your projects to this instance by running contember deploy blog --yes command with CONTEMBER_API_URL and CONTEMBER_API_TOKEN environment variables set. You can use either superadmin token or create token with deployer role (using contember tenant:create-api-key command).

Deploy Contember Interface

Contember Interface can be deployed as single page application. It's bundled using Vite.

You will need to modify admin/index.tsx and admin/vite.config.ts files based on following examples:

admin/index.tsx
import * as React from 'react'
import { ApplicationEntrypoint, LoginEntrypoint, Pages, runReactApp } from '@contember/admin'
import { Layout } from './components/Layout'
import '@contember/admin/index.css'

const apiBaseUrl = import.meta.env.VITE_CONTEMBER_ADMIN_API_BASE_URL as string
const loginToken = import.meta.env.VITE_CONTEMBER_ADMIN_LOGIN_TOKEN as string
const projectSlug = import.meta.env.VITE_CONTEMBER_ADMIN_PROJECT_NAME as string

if (window.location.pathname === '/') {
// Login page
runReactApp(
<LoginEntrypoint
apiBaseUrl={apiBaseUrl}
loginToken={loginToken}
projects={[projectSlug]}
formatProjectUrl={it => `/${it.slug}/`}
/>,
)
} else if (window.location.pathname.startsWith('/' + projectSlug)) {
// Project interface itself page
runReactApp(
<ApplicationEntrypoint
basePath={`/${projectSlug}/`}
apiBaseUrl={apiBaseUrl}
project={projectSlug}
stage="live"
children={<Pages layout={Layout} children={import.meta.glob('./pages/**/*.tsx')} />}
/>
)
} else {
// Page not found - redirect to login
window.location.href = '/'
}
admin/vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig(({ command }) => ({
base: '/',
}))

Furthermore your .env files (.env.development and .env.production) should contain referenced variables:

admin/.env.production
# URL where Engine is running
VITE_CONTEMBER_ADMIN_API_BASE_URL=http://localhost:1481
# Login token needed to perform login - configured in env variable of Engine
VITE_CONTEMBER_ADMIN_LOGIN_TOKEN=1111111111111111111111111111111111111111
# Your project's slug (used to connect to correct GraphQL endpoint)
VITE_CONTEMBER_ADMIN_PROJECT_NAME=headless-cms

After these changes you can run npm run build-admin command. It will create bundle in admin/dist folder, which you can deploy to any hosting, which needs to serve the index.html file for any request that doesn't match assets.