In-Context Editing
Content creators edit directly on the live page, seeing changes instantly
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:
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
Deploying Apostrophe CMS on Klutch.sh provides several advantages:
Before deploying Apostrophe CMS on Klutch.sh, ensure you have:
Apostrophe CMS has the following requirements:
| Component | Version | Notes |
|---|---|---|
| Node.js | 20.x+ | Use LTS version for stability |
| MongoDB | 6.0+ | MongoDB Atlas recommended for cloud |
| npm | 10.x+ | Included with Node.js |
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└── .gitignoreIf you don’t have an existing Apostrophe project, create one using the CLI:
# Install the Apostrophe CLI globallynpm install -g @apostrophecms/cli
# Create a new projectapos create my-apostrophe-site
# Navigate to the projectcd my-apostrophe-site
# Install dependenciesnpm installAlternatively, use npx for a one-time project creation:
npx @apostrophecms/cli create my-apostrophe-sitecd my-apostrophe-sitenpm installKlutch.sh automatically detects Dockerfiles in your repository’s root directory. Create a Dockerfile optimized for Apostrophe CMS:
# Build stageFROM node:20-alpine AS builder
# Set working directoryWORKDIR /app
# Install build dependenciesRUN apk add --no-cache python3 make g++
# Copy package filesCOPY package*.json ./
# Install all dependencies (including dev for building)RUN npm ci
# Copy application sourceCOPY . .
# Build assets for productionENV NODE_ENV=productionRUN ./scripts/build-assets.sh
# Production stageFROM node:20-alpine
# Set labelsLABEL name="apostrophe-cms" \ description="Apostrophe CMS - Full-stack Node.js Content Management System" \ maintainer="Your Name"
# Create non-root user for securityRUN addgroup -g 1001 -S apostrophe && \ adduser -S -D -H -u 1001 -G apostrophe apostrophe
# Set working directoryWORKDIR /app
# Copy package filesCOPY package*.json ./
# Install production dependencies onlyENV NODE_ENV=productionRUN npm ci --only=production && npm cache clean --force
# Copy built assets from builder stageCOPY --from=builder /app/apos-build ./apos-buildCOPY --from=builder /app/public ./publicCOPY --from=builder /app/release-id ./release-id
# Copy application sourceCOPY --chown=apostrophe:apostrophe . .
# Create uploads directoryRUN mkdir -p /app/public/uploads && \ chown -R apostrophe:apostrophe /app
# Switch to non-root userUSER apostrophe
# Expose Apostrophe portEXPOSE 3000
# Health checkHEALTHCHECK --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 ApostropheCMD ["node", "app.js"]Create a script at scripts/build-assets.sh to handle asset compilation:
#!/bin/sh
# Generate unique release ID for cache bustingexport 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 runtimeecho $APOS_RELEASE_ID > ./release-id
# Build Apostrophe assetsnode app @apostrophecms/asset:buildMake the script executable:
chmod +x scripts/build-assets.shConfigure 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': {} }});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 existslet 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 }});Create a .dockerignore file to exclude unnecessary files:
# Dependenciesnode_modules/
# Build artifactsapos-build/data/
# Uploads (handled by persistent storage)public/uploads/
# Development files.git/.gitignore.env.env.local*.md
# IDE.vscode/.idea/
# DockerDockerfiledocker-compose*.yml.dockerignore
# Logs*.lognpm-debug.log*
# Testingtest/coverage/
# Local configlocal.jslocal.example.jsApostrophe requires MongoDB. We recommend using MongoDB Atlas for cloud deployments:
Create a MongoDB Atlas account
Navigate to MongoDB Atlas and create a free account.
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.
Create a database user
In the Security section, create a database user with read/write permissions.
Configure network access
In Network Access, add 0.0.0.0/0 to allow connections from any IP (required for cloud deployments).
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=majorityTest your Docker setup locally before deploying to Klutch.sh:
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 the Docker imagedocker build -t apostrophe-cms .
# Run with Docker Composedocker compose up -d
# Create admin userdocker exec -it apostrophe-cms node app @apostrophecms/user:add admin admin
# View logsdocker compose logs -f apostropheVisit http://localhost:3000 to see your site and http://localhost:3000/login to access the admin panel.
Push your code to GitHub
Initialize a Git repository and push to GitHub:
cd my-apostrophe-sitegit initgit add .git commit -m "Initial Apostrophe CMS setup"git remote add origin https://github.com/yourusername/my-apostrophe-site.gitgit push -u origin mainCreate 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”.
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.
Configure the deployment
Klutch.sh will automatically detect your Dockerfile. Configure the following settings:
Configure environment variables
Add the following environment variables in the Klutch.sh dashboard:
| Variable | Description | Example |
|---|---|---|
NODE_ENV | Node environment | production |
APOS_MONGODB_URI | MongoDB connection string | mongodb+srv://user:pass@cluster.mongodb.net/db |
APOS_SESSION_SECRET | Session encryption secret | A random 32+ character string |
APOS_SHORT_NAME | Your site’s short name | my-apostrophe-site |
PORT | Application port | 3000 |
Optional S3 storage variables:
| Variable | Description |
|---|---|
APOS_S3_BUCKET | S3 bucket name |
APOS_S3_REGION | AWS region |
APOS_S3_KEY | AWS access key |
APOS_S3_SECRET | AWS secret key |
APOS_S3_ENDPOINT | Custom S3 endpoint (for non-AWS) |
Add a persistent volume
To persist uploaded media files across deployments, add a persistent volume:
/app/public/uploadsThis ensures your uploaded images, documents, and media remain available after redeployments.
Deploy your application
Click the deploy button to start the build process. Klutch.sh will:
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:
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.
Access your Apostrophe site
Once deployed, your Apostrophe CMS will be available at:
https://your-app-name.klutch.shAccess the admin panel at:
https://your-app-name.klutch.sh/login| Variable | Required | Description | Default |
|---|---|---|---|
NODE_ENV | Yes | Set to production for production deployments | development |
APOS_MONGODB_URI | Yes | MongoDB connection string | - |
APOS_SESSION_SECRET | Yes | Secret for session encryption (32+ chars) | - |
APOS_SHORT_NAME | Yes | Unique identifier for your site | - |
PORT | No | HTTP port for the application | 3000 |
APOS_RELEASE_ID | No | Asset cache-busting ID (auto-generated) | - |
APOS_CLUSTER_PROCESSES | No | Number of cluster processes | 1 |
APOS_S3_BUCKET | No | S3 bucket for file uploads | - |
APOS_S3_REGION | No | AWS region for S3 | - |
APOS_S3_KEY | No | AWS access key ID | - |
APOS_S3_SECRET | No | AWS secret access key | - |
APOS_S3_ENDPOINT | No | Custom S3 endpoint URL | - |
Apostrophe’s module system allows you to extend functionality:
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'] } } }};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': {}}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>© {{ 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 %}Apostrophe provides a REST API for headless usage:
// Fetch all articlesconst response = await fetch('https://your-app-name.klutch.sh/api/v1/article');const articles = await response.json();
// Fetch single article by slugconst article = await fetch('https://your-app-name.klutch.sh/api/v1/article/my-article-slug');For protected endpoints, use API keys:
// modules/@apostrophecms/express/index.jsmodule.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}` }});To use a custom domain with your Apostrophe deployment:
Navigate to your project settings in the Klutch.sh dashboard
Add your custom domain (e.g., cms.yourdomain.com)
Configure DNS by adding a CNAME record pointing to your Klutch.sh app URL
Wait for SSL provisioning - Klutch.sh automatically provisions SSL certificates
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.
Problem: Application fails to connect to MongoDB.
Solution: Verify your MongoDB connection string:
// Test connectionconst { MongoClient } = require('mongodb');const client = new MongoClient(process.env.APOS_MONGODB_URI);await client.connect();console.log('Connected successfully');Problem: Assets fail to build during Docker image creation.
Solution: Ensure the build script has execute permissions:
chmod +x scripts/build-assets.shCheck that all dependencies are in dependencies, not devDependencies:
{ "dependencies": { "apostrophe": "^4.0.0", "sass": "^1.0.0" }}Problem: Users are logged out unexpectedly.
Solution: Ensure APOS_SESSION_SECRET is set and consistent across deployments:
'@apostrophecms/express': { options: { session: { secret: process.env.APOS_SESSION_SECRET, cookie: { secure: process.env.NODE_ENV === 'production' } } }}Problem: File uploads fail with permission errors.
Solution: Verify the uploads directory exists and has correct permissions in your Dockerfile:
RUN mkdir -p /app/public/uploads && \ chown -R apostrophe:apostrophe /app/public/uploadsFor S3 storage, verify IAM permissions include s3:PutObject, s3:GetObject, and s3:DeleteObject.
For production deployments with multiple CPU cores:
// Set environment variableAPOS_CLUSTER_PROCESSES=2Configure caching in your modules:
'@apostrophecms/page': { options: { cache: { page: { maxAge: 3600 // 1 hour } } }}Add indexes to frequently queried fields:
module.exports = { extend: '@apostrophecms/piece-type', options: { // ... }, extendMethods(self) { return { async afterConstruct(_super) { await _super(); await self.apos.doc.db.createIndex({ publishDate: -1 }); } }; }};You’ve successfully deployed Apostrophe CMS on Klutch.sh! Your content management system now provides:
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.