This guide describes the steps necessary to get Directus and SvelteKit app deployed to your own server.

I'm describing steps based on app.amateurinmotion.com domain and Demo application. Replace those with your domain and application name.

References:

  • https://dokploy.com/
  • https://directus.io/
  • https://svelte.dev/

See https://docs.dokploy.com/docs/core/installation for a referrial link for Hetzner to get 20€ off.

DNS

Add A records pointing to your server's IP:

  • app.amateurinmotion.com
  • *.app.amateurinmotion.com

Random

Later we'll need random strings. On your local machine add the following alias to your ~/.zshrc:

alias rand="openssl rand -base64"

Open new Terminal (or source ~/.zshrc) to test it out:

air:~ $ rand 32
T/kCF2Q7FIZLcZc8UE/Sc0soh+U5qsaLDVpTCpfAli4=

Install Dokploy

As root on your server run Dokploy installer:

curl -sSL https://dokploy.com/install.sh | sh

This will install docker, docker compose, dokplay. When it's done, open a http://<ip>:3000 URL provided by the installer.

Sign up.

Dokploy SSL

In Dokploy open:

  • Web Server > Server domain
  • Domain: app.amateurinmotion.com
  • Let's Encrypt Email: ampatspell@gmail.com
  • Enable HTTPS
  • Certificate provider: Let's Encrypt
  • Save

Open https://app.amateurinmotion.com, sign in with the same credentials. Maybe change your password – previous one was set in non-SSL connection.

Open:

  • Requests
  • Activate logging

Then open:

  • Web server
  • Click on Traefik
  • Reload

Dokploy GitHub Provider

Open:

  • Git
  • Create your first provider
  • Follow GitHub instructions
  • When you're redirected back to Dokploy, click on that tiny icon right after "Action required"
  • Again follow GitHub instructions

Project

Open:

  • Projects
  • Create new project (Demo)

Directus

Click on:

  • Create Service
  • Compose
  • Name: Directus
  • Compose type: Docker Compose

Open Directus service:

  • Advanced
  • Scroll down
  • Enable Isolated Deployment: Yes
  • Save

Go Back to General:

  • Select Raw provider

Paste the following docker-compose.yaml and Save:

services:
  directus:
    image: 'directus/directus:11'
    volumes:
      - 'database:/directus/database'
      - 'uploads:/directus/uploads'
    environment:
      - PUBLIC_URL=${PUBLIC_URL}
      - KEY=${KEY}
      - SECRET=${SECRET}
      - ADMIN_EMAIL=${ADMIN_EMAIL}
      - ADMIN_PASSWORD=${ADMIN_PASSWORD}
      - DB_CLIENT=sqlite3
      - DB_FILENAME=/directus/database/data.db
      - WEBSOCKETS_ENABLED=true
      - ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION=10000
      - MARKETPLACE_TRUST=${MARKETPLACE_TRUST}
      - CORS_ENABLED=true
      - CORS_METHODS=GET,POST,PATCH,DELETE,OPTIONS
      - CORS_ORIGIN=${CORS_ORIGIN}
    restart: always
    healthcheck:
      test:
        - CMD
        - wget
        - '-q'
        - '--spider'
        - 'http://127.0.0.1:8055/admin/login'
      interval: 5s
      timeout: 20s
      retries: 10
volumes:
  uploads:
  database:

Open Environment, paste the following:

PUBLIC_URL=https://demo-directus.app.amateurinmotion.com
CORS_ORIGIN=https://demo.app.amateurinmotion.com,http://localhost:5173
KEY=<KEY>
SECRET=<SECRET>
ADMIN_EMAIL=ampatspell@gmail.com
ADMIN_PASSWORD=<PASSWORD>
MARKETPLACE_TRUST=sandbox

MARKETPLACE_TRUST can be either sandbox or all.

Save.

Generate three random strings (with rand command) and replace <KEY>, <SECRET> and <PASSWORD> with them.

Save.

Go back to General and click Deploy. Wait for the green light.

Go to Domains and add new:

  • Service name: Directus
  • Host: demo-directus.app.amateurinmotion.com
  • Path: /
  • Internal path: /
  • Container Port: 8055
  • HTTPS: On
  • Provider: Let's Encrypt
  • Create

Click on the link, wait for Let's Encrypt.

Sign in with ADMIN_EMAIL and ADMIN_PASSWORD from environment variables above.

SvelteKit

nvm use 24
npx sv@latest create
  • SvelteKit Minimal
  • Typescript
  • prettier
  • eslint
  • sveltekit-adapter - node
  • devtools-json

Docker

Add a Dockerfile

FROM node:24-alpine AS builder

WORKDIR /app

RUN apk update
RUN apk add --no-cache coreutils

ARG GITHUB_TOKEN

COPY package*.json .
COPY .npmrc .
RUN npm ci
COPY . .

ARG PRIVATE_DIRECTUS_ADMIN_TOKEN
ARG PUBLIC_DIRECTUS_URL
ARG PUBLIC_DIRECTUS_TOKEN

# RUN npm exec tools generate
RUN npm run build
RUN npm prune --production

FROM node:24-alpine

WORKDIR /app

RUN apk --no-cache add curl

COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .

EXPOSE 3000

ARG PRIVATE_DIRECTUS_ADMIN_TOKEN
ARG PUBLIC_DIRECTUS_URL
ARG PUBLIC_DIRECTUS_TOKEN

ENV NODE_ENV=production
CMD [ "node", "build" ]

HEALTHCHECK \
  --interval=1m \
  --timeout=10s \
  --start-period=5s \
  --retries=10 \
  CMD curl -f http://localhost:3000 || exit 1

Add .dockerignore

Dockerfile
.dockerignore
.git
.gitignore
.gitattributes
README.md
.prettierrc
.eslintrc.cjs
.graphqlrc
.editorconfig
.svelte-kit
.vscode
node_modules
build
package
**/.env

GitHub

Publish to GitHub as https://github.com/ampatspell/demo

Deploy

Open Demo project in Dokploy:

  • Create service: Application
  • Name: Frontend
  • Create

Open Frontend app:

  • Select GitHub account
  • Repository: demo
  • Branch: main
  • Build path: /
  • Trigger type: On Push
  • Save

Build type:

  • Dockerfile
  • Docker File: Dockerfile
  • Docker Context Path: .
  • Leave build stage empty
  • Save

Deploy.

Domain

Open Domains:

  • Add domain
  • Host: demo.app.amateurinmotion.com
  • Path: /
  • Internal Path: /
  • Container port: 3000
  • HTTPS: On
  • Certificate provider: Let's Encrypt
  • Create

Open domain, wait for Let's Encrypt.

Directus Data Model

Open Directus at https://demo-directus.app.amateurinmotion.com

In Admin > Data Model:

  • Create new collection
  • Name: hello
  • Threat as single object
  • In the next step optional fiels leave as is
  • Create

In the newly created data model:

  • Create field
  • Input
  • Set key: message
  • Save

In Content > Hello:

  • Type in the message: To whom it may concern it is springtime it is late afternoon.
  • Save

Directus role

Open Users:

  • Add User
  • First Name: Frontend
  • Scroll down to Policies
  • Create new
  • Name: Frontend
  • Add Collection: hello
  • Read: All access
  • Save
  • Generate a token (this will be PUBLIC_DIRECTUS_TOKEN, save it somewhere)
  • Save

Open Admin User:

  • Generate a new token for admin (PRIVATE_ADMIN_TOKEN, also save it somewhere)
  • Save

GitHub token

Open GitHub

  • Settings > Developer settings
  • Personal access tokens
  • Tokens (classic)
  • Generate new token (classic)
  • Note: app.amateurinmotion read:packages for npm install
  • Expiration: Set it to more than 7 days.
  • Check: read:packages
  • Generate token
  • Save it somewhere

Expiration: This token will be used every time you re-deploy your app.

Open ~/.zshrc and export your token:

export GITHUB_TOKEN="ghp_*****"

Directus SDK and tools

In SvelteKit demo app:

Add a .npmrc

//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
@ampatspell:registry=https://npm.pkg.github.com
engine-strict=true

Add .env

PUBLIC_DIRECTUS_URL=https://demo-directus.app.amateurinmotion.com
PUBLIC_DIRECTUS_TOKEN=<token from above>
PRIVATE_DIRECTUS_ADMIN_TOKEN=<token from above>

and

npm install \
    @ampatspell/base \
    @ampatspell/directus

Add generate and tools scripts to package.json:

"scripts": {
   …
+  "tools": "tools",
+  "generate": "tools generate"  
},

Generate types:

npm run generate

Add src/lib/directys/directus.ts file:

import { type Schema } from './schema';
import { getBaseDirectus, type Fetch } from '@ampatspell/directus/directus';

export const getDirectus = (fetch: Fetch) => getBaseDirectus<Schema>(fetch);

export type Directus = ReturnType<typeof getDirectus>;

Add the following files to .gitignore:

  • /src/lib/directus/schema.ts
  • /src/lib/directus/diff.json

In src/routes add:

// +layout.server.ts

import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async (event) => {
  const visualEditingEnabled = event.url.searchParams.get('visual-editing') === 'true';
  return {
    visualEditingEnabled,
  };
};
// +page.server.ts

import { getDirectus } from '$lib/directus/directus';
import { CollectionNames } from '$lib/directus/schema';
import { readSingleton } from '@directus/sdk';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const directus = getDirectus(fetch);
  return {
    hello: await directus.request(readSingleton(CollectionNames.hello, {}))
  };
};
// +page.svelte

<script lang="ts">
  let { data } = $props();
  let hello = $derived(data.hello);
</script>

<div class="hello">{hello.message}</div>

Uncomment npm run generate in Dockerfile builder layer:

FROM node:24-alpine AS builder

WORKDIR /app

COPY package*.json .
RUN npm ci
COPY . .

ARG PRIVATE_DIRECTUS_ADMIN_TOKEN
ARG PUBLIC_DIRECTUS_URL
ARG PUBLIC_DIRECTUS_TOKEN

- # RUN npm exec tools generate
+ RUN npm exec tools generate
RUN npm run build
RUN npm prune --production
…

Dokploy

Open Dokploy:

  • Open Demo project
  • Open Frontend application
  • Add the same .env variables to Environment Settings and Build-time Variables
  • Add GITHUB_TOKEN to Build-time variables

Environment settings:

PUBLIC_DIRECTUS_URL=https://demo-directus.app.amateurinmotion.com
PUBLIC_DIRECTUS_TOKEN=<token from above>
PRIVATE_DIRECTUS_ADMIN_TOKEN=<token from above>

Build-time Variables:

PUBLIC_DIRECTUS_URL=https://demo-directus.app.amateurinmotion.com
PUBLIC_DIRECTUS_TOKEN=<token from above>
PRIVATE_DIRECTUS_ADMIN_TOKEN=<token from above>
GITHUB_TOKEN=<token from above>

Back to the app locally, git commit, push.

Wait a bit.

Open https://demo.app.amateurinmotion.com/

Done.