Directus CMS + Docker

GitHub

DockerHub

Docker

Dockerガイド

データベースにPostgreSQLを選択。

docker-compose.yml

version: "3.7"
services:
##### redis-server
  redis:
    container_name: redis
    image: redis:alpine
    volumes:
        - ./redis:/etc/redis
    # need to download default config file:redis.conf from https://redis.io/topics/config
    # In redis.conf : bind 172.18.0.2, protected-mode no, maxmemory 250mb, maxmemory-policy allkeys-lru
    command: ["redis-server", "/etc/redis/redis.conf"]
    restart: always
    networks: 
      proxy-tier:
        ipv4_address: 172.18.0.2
              
  postgres:
    image: postgres:alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_DATABASE}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    networks: 
      proxy-tier:
        ipv4_address: 172.22.0.3

  directus:
    container_name: directus
    image: directus/directus:10.9.0
    ports:
      - 8055:8055
    volumes:
      - ./uploads:/directus/uploads
      # If you want to load extensions from the host
      # - ./extensions:/directus/extensions
    depends_on:
      - redis
      - postgres
    environment:
      KEY: ${KEY}
      SECRET: ${SECRET}

      DB_CLIENT: ${DB_CLIENT}
      DB_HOST: ${DB_HOST}
      DB_PORT: ${DB_PORT}
      DB_DATABASE: ${DB_DATABASE}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}

      CACHE_ENABLED: "true"
      CACHE_STORE: "redis"
      REDIS: "redis://redis:6379"
      
      # Should create aclfile /etc/redis/users.acl
      #REDIS_USERNAME: ${REDIS_USERNAME}
      #REDIS_PASSWORD: ${REDIS_PASSWORD}

      ADMIN_EMAIL: ${ADMIN_EMAIL}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}

      CORS_ENABLED: true
      CORS_ORIGIN: true
      
    networks: 
      proxy-tier:
        ipv4_address: 172.18.0.4

      # Make sure to set this in production
      # (see https://docs.directus.io/self-hosted/config-options#general)
      # PUBLIC_URL: "https://directus.example.com"

    # Environment variables can also be defined in a file (for example `.env`):
    # env_file:
    #	  - .env

# For IPv4 only
# docker network create --gateway 172.18.0.1 --subnet 172.18.0.0/24 containers-network

networks:
  proxy-tier:
    name: containers-network
    external: true
    

.env

HOST="0.0.0.0"
PORT=8055

KEY="xxxx-xxxx-xxxx-xxxx-xxxx"
SECRET="xxxx-xxxx-xxxx-xxxx-xxxx"

DB_CLIENT="pg"
DB_HOST="postgres"
DB_PORT=5432
DB_DATABASE="directus"
DB_USER="directus"
DB_PASSWORD="xxxxxxxxxxxxxxxxxxxxxxxx"

ADMIN_EMAIL="[email protected]"
ADMIN_PASSWORD="xxxxxxxxxxxxxxxxxxxxxxxxx"

Database

Variable Description Default Value
DB_CLIENT Required. What database client to use. One of pg or postgres, mysql, oracledb, mssql, sqlite3, cockroachdb.
DB_HOST Database host. Required when using pg, mysql, oracledb, or mssql.
DB_PORT Database port. Required when using pg, mysql, oracledb, or mssql.
DB_DATABASE Database name. Required when using pg, mysql, oracledb, or mssql.
DB_USER Database user. Required when using pg, mysql, oracledb, or mssql.
DB_PASSWORD Database user’s password. Required when using pg, mysql, oracledb, or mssql.

Redis

Directus requires Redis for multi-container deployments. This ensures that things like caching, rate-limiting, and WebSockets work reliably across multiple containers of Directus.

Variable Description Default Value
REDIS Redis connection string, e.g., redis://user:[email protected]:6380/4. Using this will ignore the other Redis connection parameter environment variables
REDIS_HOST Hostname of the Redis instance, e.g., "127.0.0.1"
REDIS_PORT Port of the Redis instance, e.g., 6379
REDIS_USERNAME Username for your Redis instance, e.g., "default"
REDIS_PASSWORD Password for your Redis instance, e.g., "yourRedisPassword"

Universally Unique Identifiers (UUIDs)

uuidgenコマンド:Ubuntu22.04ではデフォルトでインストール済

$ uuidgen
e897956e-6a7b-4010-b7cc-6b3ce802916d

トラブルシュート

ログインエラー :データベースにMySQL(MariaDB)を選択した場合

directus:log

.......
.......
WARN: Some tables and columns do not match your database's default collation (utf8mb4_general_ci):
directus  | 		- Table "directus_webhooks": "utf8mb4_general_ci"
directus  | 		  - Column "headers": "utf8mb4_bin"
directus  | 		- Table "directus_files": "utf8mb4_general_ci"
directus  | 		  - Column "metadata": "utf8mb4_bin"
directus  | 		- Table "directus_fields": "utf8mb4_general_ci"
directus  | 		  - Column "options": "utf8mb4_bin"
directus  | 		  - Column "display_options": "utf8mb4_bin"
directus  | 		  - Column "translations": "utf8mb4_bin"
directus  | 		  - Column "conditions": "utf8mb4_bin"
directus  | 		  - Column "validation": "utf8mb4_bin"
.......
.....
directus  | [00:23:21] POST /auth/refresh 400 18ms
directus  | [00:23:21] GET /auth 200 21ms
directus  | [00:23:21] GET /server/info 200 26ms
directus  | [00:23:21] GET /translations?fields[]=language&fields[]=key&fields[]=value&filter[language][_eq]=en-US&limit=-1 403 19ms
directus  | [00:23:35] POST /auth/login 401 569ms
directus  | [00:25:56] POST /auth/login 401 534ms
directus  | [00:26:18] POST /auth/login 401 516ms
directus  | [00:29:22] GET /en/admin/login 404 18ms

MariaDBコンテナにコマンドオプションを追加。

docer-compose.yaml

  mariadb:
    container_name: mariadb
    restart: always
    image: mariadb:latest
    environment:
      MYSQL_USER: ${DB_USER}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
    volumes:
      - ./db:/var/lib/mysql
    command:
            [
                "--character-set-server=utf8mb4",
                "--collation-server=utf8mb4_bin",
            ]

mariadb Command-Line Client - MariaDB Knowledge Base


CORSエラー

ページのリンク先へのジャンプがリジェクトされるエラー。

CORS

directusコンテナの環境変数でCORSを有効にすること。

Variable Description Default Value
CORS_ENABLED Whether or not to enable the CORS headers. false
CORS_ORIGIN Value for the Access-Control-Allow-Origin header. Use true to match the Origin header, or provide a domain or a CSV of domains for specific access false
CORS_METHODS Value for the Access-Control-Allow-Methods header. GET,POST,PATCH,DELETE
CORS_ALLOWED_HEADERS Value for the Access-Control-Allow-Headers header. Content-Type,Authorization
CORS_EXPOSED_HEADERS Value for the Access-Control-Expose-Headers header. Content-Range
CORS_CREDENTIALS Whether or not to send the Access-Control-Allow-Credentials header. true
CORS_MAX_AGE Value for the Access-Control-Max-Age header. 18000

ロケール変更時エラー

setLocale(locale === ‘en’ ? ‘ja’ : ‘en’); の箇所について以下の警告

‘WritableComputedRef’ 型と ‘string’ 型が重複していないため、この比較は意図したとおりに表示されない可能性があります

はい、その警告は、locale 変数が WritableComputedRef<string> 型であるために発生します。

問題:

WritableComputedRef<string> 型は、値が変更可能な計算プロパティを表します。つまり、locale 変数の値は、コード実行中に変更される可能性があります。

一方、string 型は、文字列リテラルを表します。つまり、'en''ja' などの文字列は、コード実行中に変更されません。

解決策:

この警告を解決するには、locale 変数の値を比較する前に、明示的に文字列に変換する必要があります。

解決策 1:

`JavaScriptconst changeLocale = () => {
// 現在のロケールを取得
const currentLocale = locale.value;

// 現在のロケールが ‘en’ の場合は ‘ja’ に設定し、
// ‘ja’ の場合は ‘en’ に設定する
setLocale(currentLocale === ‘en’ ? ‘ja’ : ‘en’);
};`

Use code with caution.

content_copy

上記のように、locale.value を使用して、locale 変数の現在の値を取得することができます。

解決策 2:

JavaScriptconst changeLocale = () => { // 現在のロケールが 'en' の場合は 'ja' に設定し、 // 'ja' の場合は 'en' に設定する setLocale(locale === 'en' ? 'ja' : 'en', locale.value); };

Use code with caution.

content_copy

上記のように、setLocale() 関数の第 2 引数に locale.value を渡すことで、現在のロケールと比較することができます。

agency-os/pages/posts/index.vue

		const pagePromise = useDirectus(
			readSingleton('pages_blog', {
				// deep: {
				// 	translations: {
				// 		_filter: {
				// 			languages_code: { _eq: languageCode},
				// 		},
				// 	},
				// },
				fields: [
					'*',
					{
						seo: ['*'],
						featured_post: [
							'id',
							'slug',
							//'title',
							//'summary',
							'type',
							{ image: ['id', 'title', 'description'], author: ['name', 'image', 'job_title'], translations: ['languages_code', 'title', 'summary'] },
						],
					},
				],
			}),
		);

agency-os/components/post/PostCard.vue

<script setup lang="ts">
const locale = useI18n().locale;
const currentLocale = locale.value;

const translations = unref(props.post.translations);
const localeTranslation = translations?.find((translation) => translation.languages_code === currentLocale );
</script>
<template>
<div class="flex flex-col justify-between h-full gap-3">
			<div>
    			現在のロケール: <span :textContent="locale"></span>
  			</div>
			<NuxtLink class="space-y-4" :href="`/posts/${post.slug}`">
				<TypographyHeadline
					v-if="localeTranslation?.title"
					:content="localeTranslation.title"
					class="group-hover:text-primary"
					size="xs"
					as="h3"
				/>
				<VText text-color="light" class="line-clamp-2">
					{{ localeTranslation?.summary }}
				</VText>
			</NuxtLink>

			<Author v-if="post.author" size="sm" :author="post.author as Team" />
		</div>
</template>

フロントエンドテンプレート


AgencyOS(テンプレート)のインストール

テンプレートデータのインポート

注)事前に管理画面でTokenを作成

注)インポート先のURLとしてdirectusコンテナのアドレス:ポートを指定すること

Directusコンテナ内でAgencyOSのテンプレートデータをインポート(JSONフォーマットデータに対応したPostgreSQLが必須条件)

$ docker compose exec directus sh
/directus $ npx directus-template-cli@latest apply

Need to install the following packages:
[email protected]
Ok to proceed? (y) 
? What type of template would you like to apply? Official templates
? Select a template. AgencyOS
You selected AgencyOS
------------------
What is your Directus URL?: http://172.18.0.4:8055
What is your Directus Admin Token?: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
------------------
Loaded Schema
Applying template - AgencyOS... ⣽
Loaded Roles
Loaded Folders
Loaded Files
Loaded Users
Loaded Dashboards
Loaded Panels
Loaded Data
Loaded Flows
Loaded Operations
Loaded Presets
Loaded Settings
Applying template - AgencyOS... ⡿
Loaded Permissions
Applying template - AgencyOS... done
------------------
Template applied successfully.

テンプレートフロントエンド起動

ソースをダウンロード・ディレクトリへ移動

$ git clone https://github.com/directus-community/agency-os.git
$ cd agency-os

.envファイルを作成

.env

# Directus Setup
DIRECTUS_URL="https://your-instance.directus.app"
DIRECTUS_SERVER_TOKEN="your_directus_server_token_for_server_only_routes"
SITE_URL="http://localhost:3000"

pnpmを有効にして関連パッケージをダウンロード・テンプレートを起動

$ corepack enable pnpm
$ pnpm i
$ pnpm dev

注)directus側でCORSを有効にすること。

http://localhost:3000

コンテナとして起動する場合

Agency-OSはpnpmによりパッケージを管理しているため、pnpmがデフォルトで有効になるようにdebianベースのイメージファイルから新たなイメージを作成。

dockerfile

FROM debian:stable-slim
RUN apt-get update && apt-get install -y nano curl git
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
RUN corepack enable pnpm

新規イメージのビルド

$ docker build --no-cache -t debian:node20-slim .

注)Agency-OSの.envファイルのDIRECTUS_URLはホストマシンのローカルアドレス:ポート番号とすること(コンテナから見たlocalhostは、ホストマシンのlocalhostではないため)。

起動

$ docker run -it --rm --name my-running-script -p 3000:3000 -v ./agency-os:/app -w /app debian:node20-slim bash -c "pnpm i && pnpm dev"

docker-composeファイルに追加する場合は以下のセクションを追加

  agency-os:
    container_name: agency-os
    ### custom image created by dockerfile
    image: debian:node20-slim
    restart: unless-stopped
    working_dir: /app
    volumes:
      - ./agency-os:/app
    ports:
      - "3000:3000"
    entrypoint: ["/bin/sh", "-c", "sleep 30 && pnpm i && pnpm dev"]
    networks: 
      proxy-tier:
        ipv4_address: 172.18.0.5
    depends_on:
      - directus

注)バックエンドの起動待ちのためのsleepタイマー設定。カスタムイメージでentrypointを指定していないため、entrypointにより起動。

API

Request

RESTGraphQLSDK

GET /server/specs/oas

多言語対応

Directus側(バックエンド)とNuxt側(フロントエンド)で対応。

Directus

翻訳ページの設定

API

Nuxt

多言語対応パッケージのインストール

# pnpm add -D @nuxtjs/i18n

デフォルト言語を英語、英語と日本語に対応する場合の設定例
nuxt.config.ts

export default defineNuxtConfig({
    modules: ['@nuxtjs/i18n']

	// Internationalization - https://i18n.nuxtjs.org/
	i18n: {
		locales: [
			{
				code: 'en',
				name: 'EN',
				iso: 'en-US',
				file: 'en-US.js'
			},
		  	{
		    	code: 'ja',
		    	name: 'JP',
		    	iso: 'ja-JP',
				file: 'ja-JP.js'
		  	}
		],
		strategy: 'prefix_except_default',
		lazy: true,
		langDir: 'lang',
		defaultLocale: 'en', // default locale of your project for Nuxt pages and routings
		detectBrowserLanguage: {
			useCookie: false
		}
    },

ナビゲーションバーへ追加する言語切替スイッチコンポーネントを新規に作成

components/LangSwitcher.vue

<script setup lang="ts">
export interface DarkModeToggleProps {
	bg?: 'dark' | 'light';
}

withDefaults(defineProps<DarkModeToggleProps>(), {
	bg: 'light',
});

const colorMode = useColorMode();

const isDark = computed({
	get() {
		return colorMode.value === 'dark';
	},
	set() {
		colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';
	},
});

const { locale, locales, setLocale } = useI18n()
const switchLocalePath = useSwitchLocalePath()

const availableLocales = computed(() => {
  return locales.value.filter(i => i.code !== locale.value)
})
</script>

<template>
  <ClientOnly>
    <UButton
      v-for="locale in availableLocales"
      :key="locale.code"
      variant="ghost"
      class="text-white"
      @click.prevent.stop="setLocale(locale.code)"
      :icon="locale.code === 'en' ? 'flag:gb-4x3' : 'flag:jp-4x3'"
    > </UButton>
    <template #fallback>
      <div class="w-10 h-10" />
    </template>
  </ClientOnly>
</template>

ヘッダーテンプレートに<LangSwitcher>ブロック追加

components/navigation/TheHeader.vue

<template>
	<header class="relative w-full mx-auto space-y-4 md:flex md:items-center md:space-y-0 md:gap-x-4">
		<div class="flex items-center bg-gray-900 justify-between py-2 px-6 md:flex-1 rounded-card">
			<NuxtLink href="/" class="py-2">
				<Logo class="h-8 text-white" />
				<span class="sr-only">{{ title }}</span>
			</NuxtLink>
			<nav class="hidden md:flex md:space-x-4 lg:space-x-6" aria-label="Global">
				<NavigationMenuItem v-for="item in navigation?.items" :key="item.id" :item="item" />
			</nav>
			<div class="flex items-center justify-end flex-shrink-0 space-x-2">
				<DarkModeToggle class="hidden text-gray-200 md:block hover:text-gray-400" bg="dark" />
			</div>
			<div class="flex items-center justify-end flex-shrink-0 space-x-2">
				<LangSwitcher class="text-gray-200 md:block hover:text-gray-400" bg="dark" />
			</div>			

			<!-- div class="hidden h-full gap-4 md:flex">
				<UButton to="/contact-us" color="primary" size="xl">Let's Talk</UButton>
				<UButton to="/portal" color="primary" variant="ghost" size="xl">Login</UButton>
			</div -->
		</div>
		<NavigationMobileMenu v-if="navigation" :navigation="navigation" />
	</header>
</template>

テンプレートがロケールフィールドを変数として取得する場合

xxx.vue

<script setup lang="ts">
const { currentLocale, localeTranslation } = usePostTranslations(props.post);

function usePostTranslations(post: Post) {
    const currentLocale = useI18n().locale.value;

    const translations = unref(post.translations);
    const localeTranslation = translations?.find((translation) => translation.languages_code === currentLocale );

    return { currentLocale, localeTranslation };
};
</script>
<template>
			<NuxtLink class="space-y-4" :href="`/${currentLocale}/posts/${post.slug}`">
				<TypographyHeadline
					v-if="localeTranslation?.title"
					:content="localeTranslation.title"
					class="group-hover:text-primary font-mono"
					size="xs"
					as="h3"
				/>
				<VText text-color="light" class="line-clamp-2">
					{{ localeTranslation?.summary }}
				</VText>
			</NuxtLink>
</template>

テンプレートがロケールフィールドを関数から取得する場合

xxx.vue

<script setup lang="ts">
// Access the translated title based on locale
const currentLocale = useI18n().locale.value;

function usePostTranslations(page: any) {
	const translations = page?.translations?.find((translation) => translation.languages_code === currentLocale);
	return translations;
}
</script>
<template>
				<div class="relative">
					<TypographyHeadline :content="usePostTranslations(page)?.title" as="h1" size="lg" />
					<TypographyProse :content="usePostTranslations(page)?.summary" class="mt-2" />
				</div>
</template>

サイトのカスタマイズ

Nuxt.jsとTailwindによるフロントエンドデザイン

フレームワーク:Nuxt, Vue.js

CSS:Tailwind

CSSクラスでダークモードとライトモードの指定可

tailwind.config.ts

.....
.....
export default {
	darkMode: 'class',
.....
.....

フォームのビューモードをダークで固定

components/base/UForm.vue

.....
.....
		<div>
			<FormCustom

				class="grid gap-6 md:grid-cols-6 dark"
		
			/>
		</div>
.....
.....

アイコン(Nuxtモジュール)

Add 200,000+ ready to use icons to your Nuxt application, based on Iconify.

グローバルパラメータ

Deep

リレーショナルデータを条件付する場合

{
	"related_articles": {
		"_limit": 3,
		"comments": {
			"_sort": "rating",
			"_limit": 1
		}
	}
}

フィルタールール

言語による条件付け

const pages = await directus.request(
  readItems('articles', {
    deep: {
      translations: {
        _filter: {
          languages_code: { _in: ['en', 'ja'] },
        },
      },
    },
    fields: ['*', { translations: ['*'] }],
    limit: 1,
  })
);

Filter Operators

Operator Title (in app) Operator Description
Equals _eq Equal to
Doesn’t equal _neq Not equal to
Less than _lt Less than
Less than or equal to _lte Less than or equal to
Greater than _gt Greater than
Greater than or equal to _gte Greater than or equal to
Is one of _in Matches any of the values
Is not one of _nin Doesn’t match any of the values
Is null _null Is null
Isn’t null _nnull Is not null
Contains _contains Contains the substring
Contains (case-insensitive) _icontains Contains the case-insensitive substring
Doesn’t contain _ncontains Doesn’t contain the substring
Starts with _starts_with Starts with
Starts with _istarts_with Starts with, case-insensitive
Doesn’t start with _nstarts_with Doesn’t start with
Doesn’t start with _nistarts_with Doesn’t start with, case-insensitive
Ends with _ends_with Ends with
Ends with _iends_with Ends with, case-insensitive
Doesn’t end with _nends_with Doesn’t end with
Doesn’t end with _niends_with Doesn’t end with, case-insensitive
Is between _between Is between two values (inclusive)
Isn’t between _nbetween Is not between two values (inclusive)
Is empty _empty Is empty (null or falsy)
Isn’t empty _nempty Is not empty (null or falsy)
Intersects _intersects [1] Value intersects a given point
Doesn’t intersect _nintersects [1] Value does not intersect a given point
Intersects Bounding box _intersects_bbox [1] Value is in a bounding box
Doesn’t intersect bounding box _nintersects_bbox [1] Value is not in a bounding box

The following operator has no Title on the Filter Interface as it is only available in validation permissions:

Operator Description
_regex [2] Field has to match regex

[1] Only available on Geometry types.
[2] JavaScript “flavor” regex. Make sure to escape backslashes.

データリレーション

データモデルを構築する上で事前に理解が必要。

Many-to-Many (M2M)

recipes
- id
- name
- ingredients (An M2M alias field. Does not exist in the database, allows access to ingredients linked from recipe_ingredients)
recipes_ingredients (junction collection)
- id
- recipe (stores foreign key from recipes.id)
- ingredient (stores foreign key from ingredients.id)
- quantity (A "context" field. Stores other data associated with the relationship. These are optional.)
ingredients
- id
- name
- recipes (an M2M alias field, does not exist in the database, enables access to all the recipes related to an ingredient)

Many-to-Any (M2A)

pages
- id
- name
- sections (An M2A alias field, does not exist in the database. Provides access to items from page_sections.)
page_sections (junction collection)
- id
- pages_id (An M2O, stores foreign keys from pages.id)
- collection (An M2O, stores name of the related collection, for example headings, text_bodies, or images.)
- item (An M2O, stores foreign keys from headings.id, text.id, images.id, etc.)
headings
- id
- title
text_bodies
- id
- text
images
- id
- file