Docs / CMS & Website Platforms / Deploy Tina CMS for Git-Based Content Management

Deploy Tina CMS for Git-Based Content Management

By Admin · Mar 15, 2026 · Updated Apr 24, 2026 · 264 views · 3 min read

Tina CMS is an open-source, Git-backed headless CMS that provides visual editing for Markdown, MDX, and JSON content. Unlike database-backed CMSs, Tina stores all content in your Git repository, giving you version history, branch-based workflows, and the ability to work offline. This guide covers self-hosting Tina CMS on your VPS.

Why Tina CMS?

  • Git-backed: Content stored as files in your repository — full version history
  • Visual editing: WYSIWYG editor overlaid on your actual website
  • Type-safe: Schema-defined content with TypeScript support
  • Framework agnostic: Works with Next.js, Astro, Hugo, and more
  • Self-hostable: Run your own Tina backend instead of using Tina Cloud

Set Up Tina with Next.js

# Add Tina to an existing Next.js project
cd /opt/my-website
npx @tinacms/cli@latest init

# This creates:
# tina/config.ts  — Tina schema configuration
# tina/__generated__/  — Generated types and queries

# Install dependencies
npm install tinacms @tinacms/cli

Define Your Content Schema

// tina/config.ts
import { defineConfig } from 'tinacms'

export default defineConfig({
  branch: process.env.TINA_BRANCH || 'main',
  clientId: process.env.TINA_CLIENT_ID || '',
  token: process.env.TINA_TOKEN || '',

  build: {
    outputFolder: 'admin',
    publicFolder: 'public',
  },

  media: {
    tina: {
      mediaRoot: 'uploads',
      publicFolder: 'public',
    },
  },

  schema: {
    collections: [
      {
        name: 'post',
        label: 'Blog Posts',
        path: 'content/posts',
        format: 'mdx',
        fields: [
          {
            type: 'string',
            name: 'title',
            label: 'Title',
            isTitle: true,
            required: true,
          },
          {
            type: 'datetime',
            name: 'publishedAt',
            label: 'Published Date',
            required: true,
          },
          {
            type: 'string',
            name: 'excerpt',
            label: 'Excerpt',
            ui: { component: 'textarea' },
          },
          {
            type: 'image',
            name: 'heroImage',
            label: 'Hero Image',
          },
          {
            type: 'string',
            name: 'tags',
            label: 'Tags',
            list: true,
          },
          {
            type: 'rich-text',
            name: 'body',
            label: 'Body',
            isBody: true,
            templates: [
              {
                name: 'Callout',
                label: 'Callout Box',
                fields: [
                  { name: 'type', label: 'Type', type: 'string',
                    options: ['info', 'warning', 'tip'] },
                  { name: 'text', label: 'Text', type: 'string' },
                ],
              },
            ],
          },
        ],
      },
      {
        name: 'page',
        label: 'Pages',
        path: 'content/pages',
        format: 'mdx',
        fields: [
          { type: 'string', name: 'title', label: 'Title', isTitle: true, required: true },
          { type: 'rich-text', name: 'body', label: 'Body', isBody: true },
        ],
      },
    ],
  },
})

Self-Hosted Tina Backend

# Instead of Tina Cloud, run your own backend
# tina/database.ts
import { createDatabase, createLocalDatabase } from '@tinacms/datalayer'
import { GitHubProvider } from 'tinacms-gitprovider-github'

export default createDatabase({
  gitProvider: new GitHubProvider({
    repo: process.env.GITHUB_REPO!,
    owner: process.env.GITHUB_OWNER!,
    token: process.env.GITHUB_TOKEN!,
    branch: process.env.GITHUB_BRANCH || 'main',
  }),
  // For local development without GitHub:
  // Use createLocalDatabase() instead
})

# Environment variables
cat >> .env  ({
    slug: edge?.node?._sys.filename,
  })) || []
}

export default async function PostPage({
  params,
}: { params: { slug: string } }) {
  const { data } = await client.queries.post({
    relativePath: `${params.slug}.mdx`,
  })

  return (
    <article>
      <h1>{data.post.title}</h1>
      <TinaMarkdown content={data.post.body} />
    </article>
  )
}

Deploy on Your VPS

# Build
npm run build

# systemd service
sudo cat > /etc/systemd/system/tina-site.service         

Was this article helpful?