Skip to content

Deploying an Apostrophe CMS App

Introduction

Apostrophe CMS is a full-featured, open-source content management system built with Node.js and MongoDB. It combines powerful in-context editing with headless architecture capabilities, allowing content creators to edit directly on live pages while giving developers the flexibility to build with modern JavaScript.

Apostrophe CMS is ideal for:

  • Marketing websites - Build dynamic sites with in-context editing for marketing teams
  • Corporate portals - Enterprise-grade CMS with advanced permissions and workflow
  • Headless applications - Use as a backend for React, Vue, Astro, or other frontend frameworks
  • Multi-site platforms - Manage multiple websites from a single dashboard
  • Content-rich applications - Blogs, news sites, and media platforms with structured content

Key Features

In-Context Editing

Content creators edit directly on the live page, seeing changes instantly

Headless-Ready

Use any frontend framework while keeping the powerful admin experience

Developer-First

Built with Node.js and MongoDB for full-stack JavaScript development

Enterprise Features

Advanced permissions, workflow management, and automated translations

Why Deploy Apostrophe on Klutch.sh?

Deploying Apostrophe CMS on Klutch.sh provides several advantages:

  • Seamless Docker deployment - Push your Dockerfile and Klutch.sh handles the rest
  • Persistent storage - Media uploads and assets persist across deployments
  • MongoDB Atlas integration - Connect to managed MongoDB for reliable data storage
  • Automatic HTTPS - All connections are secured with SSL certificates
  • Custom domains - Use your own domain for a professional CMS URL
  • Scalable resources - Adjust compute resources based on traffic needs

Prerequisites

Before deploying Apostrophe CMS on Klutch.sh, ensure you have:


System Requirements

Apostrophe CMS has the following requirements:

ComponentVersionNotes
Node.js20.x+Use LTS version for stability
MongoDB6.0+MongoDB Atlas recommended for cloud
npm10.x+Included with Node.js

Project Structure

A typical Apostrophe CMS project has the following structure:

apostrophe-project/
├── Dockerfile
├── app.js
├── package.json
├── package-lock.json
├── modules/
│ ├── @apostrophecms/
│ │ ├── home-page/
│ │ ├── page-type/
│ │ └── rich-text-widget/
│ ├── article/
│ └── article-page/
├── views/
│ ├── layout.html
│ └── pages/
├── public/
│ └── (static assets)
├── scripts/
│ └── build-assets.sh
├── .dockerignore
└── .gitignore

Creating a New Apostrophe Project

If you don’t have an existing Apostrophe project, create one using the CLI:

Terminal window
# Install the Apostrophe CLI globally
npm install -g @apostrophecms/cli
# Create a new project
apos create my-apostrophe-site
# Navigate to the project
cd my-apostrophe-site
# Install dependencies
npm install

Alternatively, use npx for a one-time project creation:

Terminal window
npx @apostrophecms/cli create my-apostrophe-site
cd my-apostrophe-site
npm install

Creating the Dockerfile

Klutch.sh automatically detects Dockerfiles in your repository’s root directory. Create a Dockerfile optimized for Apostrophe CMS:

# Build stage
FROM node:20-alpine AS builder
# Set working directory
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache python3 make g++
# Copy package files
COPY package*.json ./
# Install all dependencies (including dev for building)
RUN npm ci
# Copy application source
COPY . .
# Build assets for production
ENV NODE_ENV=production
RUN ./scripts/build-assets.sh
# Production stage
FROM node:20-alpine
# Set labels
LABEL name="apostrophe-cms" \
description="Apostrophe CMS - Full-stack Node.js Content Management System" \
maintainer="Your Name"
# Create non-root user for security
RUN addgroup -g 1001 -S apostrophe && \
adduser -S -D -H -u 1001 -G apostrophe apostrophe
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install production dependencies only
ENV NODE_ENV=production
RUN npm ci --only=production && npm cache clean --force
# Copy built assets from builder stage
COPY --from=builder /app/apos-build ./apos-build
COPY --from=builder /app/public ./public
COPY --from=builder /app/release-id ./release-id
# Copy application source
COPY --chown=apostrophe:apostrophe . .
# Create uploads directory
RUN mkdir -p /app/public/uploads && \
chown -R apostrophe:apostrophe /app
# Switch to non-root user
USER apostrophe
# Expose Apostrophe port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/v1/@apostrophecms/doc/count', (r) => process.exit(r.statusCode === 200 || r.statusCode === 401 ? 0 : 1))" || exit 1
# Start Apostrophe
CMD ["node", "app.js"]

Build Assets Script

Create a script at scripts/build-assets.sh to handle asset compilation:

#!/bin/sh
# Generate unique release ID for cache busting
export APOS_RELEASE_ID=$(cat /dev/urandom | env LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
# Save release ID to file for runtime
echo $APOS_RELEASE_ID > ./release-id
# Build Apostrophe assets
node app @apostrophecms/asset:build

Make the script executable:

Terminal window
chmod +x scripts/build-assets.sh

Application Configuration

Main Application File (app.js)

Configure your app.js to read environment variables:

require('apostrophe')({
shortName: process.env.APOS_SHORT_NAME || 'my-apostrophe-site',
// MongoDB connection from environment
modules: {
'@apostrophecms/express': {
options: {
port: parseInt(process.env.PORT || '3000'),
session: {
secret: process.env.APOS_SESSION_SECRET || 'change-me-in-production'
}
}
},
// Upload storage configuration
'@apostrophecms/uploadfs': {
options: {
// For S3 storage (recommended for production)
...(process.env.APOS_S3_BUCKET && {
storage: 's3',
bucket: process.env.APOS_S3_BUCKET,
region: process.env.APOS_S3_REGION,
key: process.env.APOS_S3_KEY,
secret: process.env.APOS_S3_SECRET,
endpoint: process.env.APOS_S3_ENDPOINT
})
}
},
// Read release ID from file
'@apostrophecms/asset': {
options: {
// Release ID is read from file in production
}
},
// Your custom modules
'article': {},
'article-page': {}
}
});

Reading Release ID

Update your app.js to read the release ID from the file created during build:

const fs = require('fs');
const path = require('path');
// Read release ID from file if it exists
let releaseId;
const releaseIdPath = path.join(__dirname, 'release-id');
if (fs.existsSync(releaseIdPath)) {
releaseId = fs.readFileSync(releaseIdPath, 'utf8').trim();
}
require('apostrophe')({
shortName: process.env.APOS_SHORT_NAME || 'my-apostrophe-site',
// Set release ID for asset versioning
releaseId: releaseId || process.env.APOS_RELEASE_ID,
modules: {
'@apostrophecms/express': {
options: {
port: parseInt(process.env.PORT || '3000'),
session: {
secret: process.env.APOS_SESSION_SECRET
}
}
}
// ... rest of configuration
}
});

Docker Ignore File

Create a .dockerignore file to exclude unnecessary files:

# Dependencies
node_modules/
# Build artifacts
apos-build/
data/
# Uploads (handled by persistent storage)
public/uploads/
# Development files
.git/
.gitignore
.env
.env.local
*.md
# IDE
.vscode/
.idea/
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Logs
*.log
npm-debug.log*
# Testing
test/
coverage/
# Local config
local.js
local.example.js

Setting Up MongoDB Atlas

Apostrophe requires MongoDB. We recommend using MongoDB Atlas for cloud deployments:

  1. Create a MongoDB Atlas account

    Navigate to MongoDB Atlas and create a free account.

  2. Create a new cluster

    Follow the setup wizard to create a new cluster. The free M0 tier is sufficient for development and small production sites.

  3. Create a database user

    In the Security section, create a database user with read/write permissions.

  4. Configure network access

    In Network Access, add 0.0.0.0/0 to allow connections from any IP (required for cloud deployments).

  5. Get your connection string

    Click “Connect” on your cluster and select “Connect your application”. Copy the connection string:

    mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/<dbname>?retryWrites=true&w=majority

Local Development

Test your Docker setup locally before deploying to Klutch.sh:

Using Docker Compose

Create a docker-compose.yml for local development:

version: '3.8'
services:
mongodb:
image: mongo:7.0
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
environment:
MONGO_INITDB_DATABASE: apostrophe
apostrophe:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- APOS_MONGODB_URI=mongodb://mongodb:27017/apostrophe
- APOS_SESSION_SECRET=your-session-secret-change-in-production
- APOS_SHORT_NAME=my-apostrophe-site
depends_on:
- mongodb
volumes:
- uploads:/app/public/uploads
volumes:
mongodb_data:
uploads:

Build and Run Locally

Terminal window
# Build the Docker image
docker build -t apostrophe-cms .
# Run with Docker Compose
docker compose up -d
# Create admin user
docker exec -it apostrophe-cms node app @apostrophecms/user:add admin admin
# View logs
docker compose logs -f apostrophe

Visit http://localhost:3000 to see your site and http://localhost:3000/login to access the admin panel.


Deploying to Klutch.sh

  1. Push your code to GitHub

    Initialize a Git repository and push to GitHub:

    Terminal window
    cd my-apostrophe-site
    git init
    git add .
    git commit -m "Initial Apostrophe CMS setup"
    git remote add origin https://github.com/yourusername/my-apostrophe-site.git
    git push -u origin main
  2. Create a new project on Klutch.sh

    Navigate to klutch.sh/app and create a new project. Give your project a descriptive name like “apostrophe-cms” or “my-website”.

  3. Connect your GitHub repository

    Select GitHub as your git source and authorize Klutch.sh to access your repositories. Select the repository containing your Apostrophe project.

  4. Configure the deployment

    Klutch.sh will automatically detect your Dockerfile. Configure the following settings:

    • Traffic Type: HTTP
    • Internal Port: 3000
  5. Configure environment variables

    Add the following environment variables in the Klutch.sh dashboard:

    VariableDescriptionExample
    NODE_ENVNode environmentproduction
    APOS_MONGODB_URIMongoDB connection stringmongodb+srv://user:pass@cluster.mongodb.net/db
    APOS_SESSION_SECRETSession encryption secretA random 32+ character string
    APOS_SHORT_NAMEYour site’s short namemy-apostrophe-site
    PORTApplication port3000

    Optional S3 storage variables:

    VariableDescription
    APOS_S3_BUCKETS3 bucket name
    APOS_S3_REGIONAWS region
    APOS_S3_KEYAWS access key
    APOS_S3_SECRETAWS secret key
    APOS_S3_ENDPOINTCustom S3 endpoint (for non-AWS)
  6. Add a persistent volume

    To persist uploaded media files across deployments, add a persistent volume:

    • Mount Path: /app/public/uploads
    • Size: Choose based on your storage needs (e.g., 10 GB)

    This ensures your uploaded images, documents, and media remain available after redeployments.

  7. Deploy your application

    Click the deploy button to start the build process. Klutch.sh will:

    1. Clone your repository
    2. Build the Docker image (including asset compilation)
    3. Deploy the container
    4. Configure networking and SSL
  8. Create an admin user

    After deployment, you’ll need to create an admin user. You can do this by connecting to your deployed container or by using Apostrophe’s programmatic user creation.

    Add this to your app.js for first-run user creation:

    modules/setup/index.js
    module.exports = {
    handlers(self) {
    return {
    '@apostrophecms/doc-type:afterSave': {
    async createInitialAdmin(req) {
    // Only run once
    if (process.env.APOS_CREATE_ADMIN !== 'true') return;
    const userModule = self.apos.modules['@apostrophecms/user'];
    const existing = await userModule.find(req, { username: 'admin' }).toCount();
    if (existing === 0) {
    await userModule.insert(req, {
    username: 'admin',
    password: process.env.APOS_ADMIN_PASSWORD,
    role: 'admin'
    });
    console.log('Admin user created');
    }
    }
    }
    };
    }
    };

    Then set APOS_CREATE_ADMIN=true and APOS_ADMIN_PASSWORD=your-secure-password as environment variables for the first deployment, and remove them afterward.

  9. Access your Apostrophe site

    Once deployed, your Apostrophe CMS will be available at:

    https://your-app-name.klutch.sh

    Access the admin panel at:

    https://your-app-name.klutch.sh/login

Environment Variables Reference

VariableRequiredDescriptionDefault
NODE_ENVYesSet to production for production deploymentsdevelopment
APOS_MONGODB_URIYesMongoDB connection string-
APOS_SESSION_SECRETYesSecret for session encryption (32+ chars)-
APOS_SHORT_NAMEYesUnique identifier for your site-
PORTNoHTTP port for the application3000
APOS_RELEASE_IDNoAsset cache-busting ID (auto-generated)-
APOS_CLUSTER_PROCESSESNoNumber of cluster processes1
APOS_S3_BUCKETNoS3 bucket for file uploads-
APOS_S3_REGIONNoAWS region for S3-
APOS_S3_KEYNoAWS access key ID-
APOS_S3_SECRETNoAWS secret access key-
APOS_S3_ENDPOINTNoCustom S3 endpoint URL-

Creating Custom Modules

Apostrophe’s module system allows you to extend functionality:

Custom Piece Type (Article)

Create modules/article/index.js:

module.exports = {
extend: '@apostrophecms/piece-type',
options: {
label: 'Article',
pluralLabel: 'Articles'
},
fields: {
add: {
subtitle: {
type: 'string',
label: 'Subtitle'
},
body: {
type: 'area',
label: 'Body Content',
options: {
widgets: {
'@apostrophecms/rich-text': {
toolbar: ['bold', 'italic', 'link', 'bulletList', 'orderedList']
},
'@apostrophecms/image': {},
'@apostrophecms/video': {}
}
}
},
featuredImage: {
type: 'area',
label: 'Featured Image',
options: {
max: 1,
widgets: {
'@apostrophecms/image': {}
}
}
},
publishDate: {
type: 'date',
label: 'Publish Date'
}
},
group: {
basics: {
label: 'Basics',
fields: ['subtitle', 'body', 'featuredImage', 'publishDate']
}
}
}
};

Article Index Page

Create modules/article-page/index.js:

module.exports = {
extend: '@apostrophecms/piece-page-type',
options: {
label: 'Article Index',
pluralLabel: 'Article Indexes',
piecesFilters: [
{ name: 'publishDate' }
],
perPage: 10
}
};

Register modules in app.js:

modules: {
// ... other modules
'article': {},
'article-page': {}
}

Custom Page Templates

Create a custom layout at views/layout.html:

{% extends data.outerLayout %}
{% block beforeMain %}
<header class="site-header">
<nav class="site-nav">
<a href="/" class="site-logo">{{ data.global.siteTitle or 'My Site' }}</a>
{% for item in data.global._nav %}
<a href="{{ item._url }}">{{ item.title }}</a>
{% endfor %}
</nav>
</header>
{% endblock %}
{% block main %}
{% block content %}{% endblock %}
{% endblock %}
{% block afterMain %}
<footer class="site-footer">
<p>&copy; {{ data.global.siteTitle or 'My Site' }} {{ 'now' | date: 'YYYY' }}</p>
</footer>
{% endblock %}

Create a home page template at modules/@apostrophecms/home-page/views/page.html:

{% extends "layout.html" %}
{% block content %}
<main class="home-page">
<section class="hero">
{% area data.page, 'hero' %}
</section>
<section class="content">
{% area data.page, 'main' %}
</section>
</main>
{% endblock %}

Headless API Usage

Apostrophe provides a REST API for headless usage:

Fetch Pieces

// Fetch all articles
const response = await fetch('https://your-app-name.klutch.sh/api/v1/article');
const articles = await response.json();
// Fetch single article by slug
const article = await fetch('https://your-app-name.klutch.sh/api/v1/article/my-article-slug');

API Authentication

For protected endpoints, use API keys:

// modules/@apostrophecms/express/index.js
module.exports = {
options: {
apiKeys: {
[process.env.APOS_API_KEY]: {
role: 'admin'
}
}
}
};

Then include the key in requests:

const response = await fetch('https://your-app-name.klutch.sh/api/v1/article', {
headers: {
'Authorization': `ApiKey ${apiKey}`
}
});

Custom Domain Setup

To use a custom domain with your Apostrophe deployment:

  1. Navigate to your project settings in the Klutch.sh dashboard

  2. Add your custom domain (e.g., cms.yourdomain.com)

  3. Configure DNS by adding a CNAME record pointing to your Klutch.sh app URL

  4. Wait for SSL provisioning - Klutch.sh automatically provisions SSL certificates

  5. Update Apostrophe configuration to recognize the new domain:

    Add the baseUrl option to your app.js:

    require('apostrophe')({
    shortName: process.env.APOS_SHORT_NAME,
    baseUrl: process.env.APOS_BASE_URL || 'https://your-app-name.klutch.sh',
    // ... rest of configuration
    });

    Add APOS_BASE_URL=https://cms.yourdomain.com to your environment variables.


Troubleshooting

Common Issues

Problem: Application fails to connect to MongoDB.

Solution: Verify your MongoDB connection string:

  1. Ensure the username and password are URL-encoded
  2. Check that your IP is whitelisted in MongoDB Atlas
  3. Verify the database name in the connection string
// Test connection
const { MongoClient } = require('mongodb');
const client = new MongoClient(process.env.APOS_MONGODB_URI);
await client.connect();
console.log('Connected successfully');

Performance Optimization

Enable Cluster Mode

For production deployments with multiple CPU cores:

// Set environment variable
APOS_CLUSTER_PROCESSES=2

Enable Caching

Configure caching in your modules:

'@apostrophecms/page': {
options: {
cache: {
page: {
maxAge: 3600 // 1 hour
}
}
}
}

Optimize MongoDB Queries

Add indexes to frequently queried fields:

modules/article/index.js
module.exports = {
extend: '@apostrophecms/piece-type',
options: {
// ...
},
extendMethods(self) {
return {
async afterConstruct(_super) {
await _super();
await self.apos.doc.db.createIndex({ publishDate: -1 });
}
};
}
};

Further Reading


Summary

You’ve successfully deployed Apostrophe CMS on Klutch.sh! Your content management system now provides:

  • ✅ In-context editing for content creators
  • ✅ Modern Node.js architecture
  • ✅ MongoDB-powered data storage
  • ✅ Persistent media uploads
  • ✅ Headless API capabilities
  • ✅ Automatic HTTPS

Apostrophe CMS combines the best of traditional CMS features with modern headless architecture, giving you flexibility to build anything from simple marketing sites to complex enterprise applications.