Why Move from Portainer to Komodo#
Portainer has long been the de facto standard for managing Docker through a web interface, but it has limitations:
- no full-fledged CI/CD workflow. It does have one, but not at the level competitors offer;
- weak git repository support. Again, you can connect Portainer to GitHub and it will even work, but not all the functionality is there, and what is there isn’t implemented the way competitors do it.
- an aging UI. Yes, I fully understand that interface design is a very subjective thing. Some like retro, some like Metro. But still, some things are confusing.
- some functionality is paid.
Komodo is a younger project (source on GitHub) that closes these gaps: it can deploy stacks straight from git, supports distributed management of multiple hosts via the Periphery agent - and this is baked into its architecture from the start - and offers a more modern approach to CI/CD within a home infrastructure. As far as I can tell, these are exactly the things the developers focused on. The full list of features is in the official “What is Komodo” description.
In this article I’ll describe the full installation: from the docker-compose file to setting up SSO via Authentik and connecting an additional host as a remote agent.
Architecture#
Komodo consists of three components:
- Core - the central service, web interface, and API.
- Periphery - an agent that runs on each managed host and talks to Core. This component is required for installation even on the main host.
- Database - Core stores all data (resource configurations, users, logs) in a MongoDB-compatible database.
The database deserves a separate mention: Komodo can work either with a regular MongoDB, or with FerretDB - a proxy adapter that emulates the MongoDB protocol while actually storing data in Postgres. No other option is currently supported. For myself, and while writing this article, I chose the classic MongoDB option - it’s officially recommended and the most tested (details in the Core setup documentation). Although why they didn’t provide the option to work with Postgres directly is not really clear! From my point of view, that’s a huge downside.
Preparation: Directory Structure#
I prefer bind mounts over named Docker volumes - it’s more convenient for me to make backups and work with data directly from the host. But this adds certain complications, especially around directory permissions. My project data lives at /home/stilicho/docker/komodo.
Let’s create the directory structure in advance:
mkdir -p /home/stilicho/docker/komodo/{mongo/data,mongo/configdb,keys,backups}As you can tell, I already looked at the project structure ahead of time and tested everything, so the command above wasn’t pulled out of thin air.
Something worth thinking through right away, not later. If, like me, all your other docker-compose projects live next to each other in a common parent folder (for example /home/stilicho/docker/vaultwarden, /home/stilicho/docker/gotify, etc.), then PERIPHERY_ROOT_DIRECTORY needs to point to that parent directory (/home/stilicho/docker), not to a subfolder inside komodo. Otherwise, later, when you migrate existing stacks into Komodo (there’s a section on this below), Periphery physically won’t be able to see their files and will throw No such file or directory, even if the path in the UI is set correctly. More details in the section about migrating stacks. I know what I’m talking about, since I ran into this problem myself.
Installation Configuration#
docker-compose.yaml#
Below is the resulting compose file. It’s based on the official MongoDB example, with a few important tweaks:
- volumes replaced with bind mounts under our path;
- each service given a
container_namefor convenience;
################################
# 🦎 KOMODO COMPOSE - MONGO 🦎
################################
# This Docker Compose file deploys three Komodo components:
# 1. MongoDB - Komodo's database.
# 2. Komodo Core - Komodo's main server and web interface.
# 3. Komodo Periphery - the agent that performs Docker
# operations on the managed server.
services:
# ============================================================
# MongoDB
# ============================================================
mongo:
# MongoDB Docker image.
# Without a tag specified, the latest tag will be used.
image: mongo
# Fixed container name.
# This makes the container named komodo-mongo,
# regardless of the name of the Compose project directory.
container_name: komodo-mongo
labels:
# Empty label that tells Komodo itself
# that this container should not be stopped when performing
# the StopAllContainers operation.
#
# This is especially important because MongoDB is
# Komodo's own database.
komodo.skip:
# Additional MongoDB startup parameters.
#
# --quiet disables some of MongoDB's informational messages.
#
# --wiredTigerCacheSizeGB 0.25 limits the WiredTiger cache size
# to about 256 MB.
#
# For a small home server this keeps MongoDB
# from hogging extra RAM.
command: --quiet --wiredTigerCacheSizeGB 0.25
# Automatically restarts the container after a crash,
# as well as after the Docker host reboots.
#
# If the container was stopped manually, Docker will not
# automatically start it again.
restart: unless-stopped
# Publishing MongoDB's port externally is disabled.
#
# MongoDB doesn't need to be reachable from the host:
# Komodo Core connects to it directly
# via the internal Docker network komodo.
#
# ports:
# - 27017:27017
volumes:
# Persistent storage for MongoDB data.
#
# Left side - directory on the Docker host.
# Right side - directory inside the MongoDB container.
#
# This makes the database data persist across container recreation.
- /home/stilicho/docker/komodo/mongo/data:/data/db
# Persistent storage for MongoDB configuration data.
- /home/stilicho/docker/komodo/mongo/configdb:/data/configdb
networks:
# Connect MongoDB to Komodo's internal network.
#
# On this network, Komodo Core can reach MongoDB
# by the service name "mongo" and port 27017.
- komodo
environment:
# MongoDB admin username.
#
# The value comes from the KOMODO_DATABASE_USERNAME variable,
# defined in the Compose environment.
MONGO_INITDB_ROOT_USERNAME: ${KOMODO_DATABASE_USERNAME}
# MongoDB admin password.
#
# The value comes from the KOMODO_DATABASE_PASSWORD variable.
MONGO_INITDB_ROOT_PASSWORD: ${KOMODO_DATABASE_PASSWORD}
# ============================================================
# Komodo Core
# ============================================================
core:
# Komodo Core Docker image.
#
# The COMPOSE_KOMODO_IMAGE_TAG variable lets you choose
# the image version.
#
# If the variable is not set, tag "2" is used.
image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-2}
# Fixed name for the Komodo Core container.
container_name: komodo-core
# Runs an init process inside the container.
#
# This helps correctly handle signals and processes
# inside the container.
init: true
# Automatically restarts the container after a crash
# or Docker host reboot.
restart: unless-stopped
depends_on:
# Komodo Core depends on MongoDB.
#
# Compose will first start the mongo container,
# then the core container.
#
# Important: depends_on doesn't guarantee that MongoDB is fully
# ready to accept connections - it only controls startup order.
- mongo
ports:
# Publishes port 9120 of the Komodo Core container
# to port 9120 on the Docker host.
#
# In this Compose file, this allows accessing Core directly,
# bypassing Traefik.
#
# If access to Komodo should only go through Traefik,
# this port publication can be removed later.
- 9120:9120
# Loads environment variables from the compose.env file.
#
# This lets you avoid storing secrets directly
# in docker-compose.yml.
env_file: ./compose.env
environment:
# MongoDB address for Komodo Core.
#
# "mongo" - the MongoDB service's DNS name inside the Docker network.
# 27017 - MongoDB's standard port.
#
# So Core reaches the DB as:
# mongo:27017
KOMODO_DATABASE_ADDRESS: mongo:27017
volumes:
# Keys used for communication between Komodo Core
# and Komodo Periphery.
#
# Host directory:
# /home/stilicho/docker/komodo/keys
#
# Directory inside the container:
# /config/keys
- /home/stilicho/docker/komodo/keys:/config/keys
# Directory for Komodo database backups.
#
# Backups are saved on the Docker host and therefore
# won't disappear when the Core container is recreated.
#
# Komodo documentation:
# https://komo.do/docs/setup/backup
- /home/stilicho/docker/komodo/backups:/backups
networks:
# Connection to the proxy network.
#
# This network is used by Traefik to reach
# the Komodo Core container.
- proxy
# Connection to Komodo's internal network.
#
# Through this, Core communicates with MongoDB
# and other Komodo components.
- komodo
security_opt:
# Prevents the container from gaining new privileges.
#
# This is an additional security measure.
# It prevents privilege escalation of a process inside the container.
- no-new-privileges:true
labels:
# ========================================================
# Traefik
# ========================================================
# Allow Traefik to discover and serve this container.
- "traefik.enable=true"
# --------------------------------------------------------
# HTTP router
# --------------------------------------------------------
# Komodo's HTTP router works via the web entrypoint,
# usually corresponding to port 80.
- "traefik.http.routers.komodo.entrypoints=web"
# The router triggers if the request's Host header
# equals komodo.stilicho.ru.
- "traefik.http.routers.komodo.rule=Host(`komodo.stilicho.ru`)"
# Creates a middleware that redirects HTTP requests
# to HTTPS.
- "traefik.http.middlewares.komodo-https-redirect.redirectscheme.scheme=https"
# Attaches the redirect middleware to the HTTP router.
- "traefik.http.routers.komodo.middlewares=komodo-https-redirect"
# --------------------------------------------------------
# HTTPS router
# --------------------------------------------------------
# The HTTPS router uses the websecure entrypoint,
# usually corresponding to port 443.
- "traefik.http.routers.komodo-secure.entrypoints=websecure"
# The HTTPS router also only serves requests
# for the domain komodo.stilicho.ru.
- "traefik.http.routers.komodo-secure.rule=Host(`komodo.stilicho.ru`)"
# Enable TLS for this router.
- "traefik.http.routers.komodo-secure.tls=true"
# Explicitly specify that the HTTPS router should use
# the Traefik service named komodo.
- "traefik.http.routers.komodo-secure.service=komodo"
# Tell Traefik that inside the Docker network
# the Komodo Core app listens on port 9120.
#
# Traefik talks to the container directly,
# so publishing port 9120 on the host isn't required for it.
- "traefik.http.services.komodo.loadbalancer.server.port=9120"
# Tell Traefik which Docker network to use
# to connect to the container.
#
# The proxy network is used here.
- "traefik.docker.network=proxy"
# ============================================================
# Komodo Periphery
# ============================================================
# Periphery can be run in two ways:
#
# 1. As a Docker container - this is the variant shown below.
#
# 2. As a systemd service directly on the host
# using the Periphery binary.
#
# The containerized variant is convenient when Docker is already
# the primary environment for managing services.
periphery:
# Komodo Periphery Docker image.
#
# Uses the same version variable as Core.
# If the variable is not set, tag "2" is used.
image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-2}
# Fixed name for the Periphery container.
container_name: komodo-periphery
# Adds an init process inside the container.
init: true
# Automatically restarts Periphery after a crash
# or Docker host reboot.
restart: unless-stopped
depends_on:
# Periphery depends on Komodo Core.
#
# Compose starts Core first,
# then Periphery.
- core
# Loads environment variables from compose.env.
env_file: ./compose.env
volumes:
# Shared keys for Core and Periphery.
#
# This directory is used for authenticated
# communication between Komodo components.
- /home/stilicho/docker/komodo/keys:/config/keys
# Docker socket.
#
# Through this socket, Periphery gets the ability
# to manage the Docker daemon on the host:
#
# - create containers;
# - stop containers;
# - start containers;
# - get information about containers;
# - manage Docker Compose.
#
# IMPORTANT:
# access to docker.sock effectively grants the container
# a very high level of access to the Docker host.
- /var/run/docker.sock:/var/run/docker.sock
# Mount the host's /proc inside the container.
#
# This lets Periphery get information
# about processes and the host's system state.
- /proc:/proc
# Periphery's root directory.
#
# The PERIPHERY_ROOT_DIRECTORY variable determines
# which directory Periphery will use
# for storing working data.
#
# If the variable is not set, /etc/komodo is used.
#
# For example:
#
# PERIPHERY_ROOT_DIRECTORY=/etc/komodo
#
# then you get:
#
# /etc/komodo:/etc/komodo
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
networks:
# The proxy network connects Periphery to the shared Docker network
# with Traefik and other services.
- proxy
# Komodo's internal network for communication
# between Komodo components.
- komodo
# ================================================================
# Docker networks
# ================================================================
networks:
# --------------------------------------------------------------
# proxy network
# --------------------------------------------------------------
proxy:
# This is an external Docker network.
#
# external: true means Compose does NOT create this network.
# It must already exist.
#
# Usually such a network is created once:
#
# docker network create proxy
#
# After that, various Compose projects can connect to it,
# such as Traefik, Komodo, and other services.
external: true
# --------------------------------------------------------------
# komodo network
# --------------------------------------------------------------
komodo:
# External Docker network for Komodo's internal components.
#
# It is used for communication between:
#
# Komodo Core
# │
# ├── MongoDB
# │
# └── Periphery
#
# Like proxy, this network must be created beforehand.
external: true
flowchart LR
Internet["Internet / LAN"]
Traefik["Traefik"]
Core["Komodo Core
:9120"]
Mongo["MongoDB
:27017"]
Periphery["Komodo Periphery"]
Host["Docker Host"]
Internet -->|HTTPS| Traefik
subgraph Networks["Docker Networks"]
direction LR
subgraph Proxy["proxy"]
Traefik
end
subgraph Komodo["komodo"]
Core
Mongo
Periphery
end
Traefik --> Core
Core --> Mongo
Core --> Periphery
end
Periphery -->|docker.sock| Host
So Traefik sees Core through proxy, while Core, MongoDB, and Periphery communicate through komodo.
Both networks are external, so create them ahead of time (Compose won’t create external networks itself):
docker network create proxy # if not already created for the reverse proxy
docker network create komodoAn important point about networks. If you explicitly specify networks: for a service (as with core - the proxy network for Traefik), Docker Compose stops automatically adding it to the project’s default network. That’s why all three services that need to talk to each other (mongo, core, periphery) are explicitly connected to the separate komodo network - it’s specifically dedicated to internal communication between Komodo containers, while proxy is only used where the service actually needs to be visible to Traefik (i.e., only for core).
compose.env - environment variables#
The second file, compose.env, stores all the settings and secrets. It’s mandatory and provided for by the developers from the start. Some values need to be generated yourself.
####################################
# 🦎 KOMODO COMPOSE - VARIABLES 🦎 #
####################################
COMPOSE_KOMODO_IMAGE_TAG="2"
COMPOSE_KOMODO_BACKUPS_PATH=/home/stilicho/docker/komodo/backups
## Database access credentials - be sure to change from defaults!
KOMODO_DATABASE_USERNAME=<pick_a_username>
KOMODO_DATABASE_PASSWORD=<pick_a_strong_password>
TZ=Europe/Moscow
#=-------------------------=#
#= Komodo Core Environment =#
#=-------------------------=#
KOMODO_HOST=https://komodo.stilicho.ru
KOMODO_TITLE=Komodo
KOMODO_PERIPHERY_PUBLIC_KEY=file:/config/keys/periphery.pub
## Local admin in case of OIDC issues
KOMODO_LOCAL_AUTH=true
KOMODO_INIT_ADMIN_USERNAME=admin
KOMODO_INIT_ADMIN_PASSWORD=<strong_password>
KOMODO_FIRST_SERVER_NAME=Local
KOMODO_DEFAULT_PAGINATION_LIMIT=50
## Secrets - generate random strings, e.g.: openssl rand -hex 32
KOMODO_WEBHOOK_SECRET=<random_hex_32>
KOMODO_JWT_SECRET=<random_hex_32>
KOMODO_JWT_TTL="1-day"
KOMODO_MONITORING_INTERVAL="15-sec"
KOMODO_RESOURCE_POLL_INTERVAL="1-hr"
## Enable this so OIDC users don't require manual activation
KOMODO_ENABLE_NEW_USERS=true
## OIDC Login (Authentik)
KOMODO_OIDC_ENABLED=true
KOMODO_OIDC_PROVIDER=https://authentik.stilicho.ru/application/o/komodo/
## Uncomment only if Core reaches Authentik via an internal address
## different from the public domain above
# KOMODO_OIDC_REDIRECT_HOST=https://auth.stilicho.ru
KOMODO_OIDC_CLIENT_ID=<client_id_from_Authentik>
KOMODO_OIDC_CLIENT_SECRET=<client_secret_from_Authentik>
KOMODO_OIDC_AUTO_REDIRECT=false ## if set to true, there will be no login-choice option. Leave false until an admin has been created
#=------------------------------=#
#= Komodo Periphery Environment =#
#=------------------------------=#
PERIPHERY_CORE_ADDRESS=ws://core:9120
PERIPHERY_CONNECT_AS=${KOMODO_FIRST_SERVER_NAME}
PERIPHERY_CORE_PUBLIC_KEYS=file:/config/keys/core.pub
## All Periphery stacks/repos/builds must live inside this path.
## Point this to the parent folder containing ALL your compose
## projects (including komodo, but also everything else) - otherwise
## Periphery won't be able to read the files of existing stacks
## when you migrate them into Komodo.
PERIPHERY_ROOT_DIRECTORY=/home/stilicho/docker
PERIPHERY_INCLUDE_DISK_MOUNTS=/etc/hostnameFor the full list of variables with comments, see the original file on GitHub - there are a lot of them, I only kept what’s actually needed for a working installation. You can always add more to taste later, as needed.
Setting Up Login via Authentik#
If you, like me, use Authentik as a single sign-on point for homelab services, Komodo integrates with it nicely - there’s even a dedicated page about Authentik integration in the official documentation.
Configuring Authentik for Komodo#
To authenticate Komodo users through Authentik, you need to create an Application + OAuth2/OpenID Connect Provider pair in Authentik, then pass the resulting parameters into Komodo’s configuration.
Important for Authentik 2026.5 and newer: this version added the ability to specify the Redirect URI type separately. For Komodo you need to add a Redirect URI of type Authorization.
In Authentik versions before 2026.5, all Redirect URIs were automatically treated as Authorization-type URIs. In that case it’s enough to add just the Authorization URL and not configure a Post Logout URI.
1. Creating the Application and Provider in Authentik#
Log in to Authentik as an administrator. Open the Authentik admin interface and go to:
Applications → Applications
Click New Application.
Authentik lets you create a pair right away:
- Application
- OAuth2/OpenID Connect Provider
Application#
In the Name field, enter a clear application name:
KomodoIf needed, you can select an application group and configure display settings. Pay attention to the Slug field.
For example:
komodoYou’ll need this slug when configuring Komodo. It’s actually been auto-generated in Authentik since around 2023 or so, I’m not sure why guides make a point of mentioning it.
2. Creating the OAuth2/OpenID Connect Provider#
In the Choose a Provider type section, select:
OAuth2/OpenID ConnectThen configure the Provider.
Name#
You can enter:
Komodo OIDCor leave the name Authentik suggests automatically.
Authorization flow#
Choose an appropriate Authorization Flow.
If you already use a standard flow for OIDC applications, you can use it here.
Client ID#
Authentik will automatically create a:
Client IDSave this value - you’ll need it in Komodo’s configuration.
Client Secret#
Also save the:
Client SecretThis is the secret Komodo will use when communicating with Authentik.
Important: the Client Secret must not be published or placed in a public Git repository.
3. Configuring the Redirect URI#
This is one of the most important integration parameters. In the Provider settings, find Redirect URIs.
For Komodo, add:
https://komodo.stilicho.ru/auth/oidc/callbackFor Authentik 2026.5 and newer, set:
Type: Strict
Mode: AuthorizationThe resulting entry should look roughly like this:
Strict | Authorization | https://komodo.stilicho.ru/auth/oidc/callbackWhy This Specific Address?#
After successful authorization, Authentik needs to send the user back to Komodo.
Komodo accepts the OIDC authentication result via:
/auth/oidc/callbackSo the full URL is formed from Komodo’s address:
https://komodo.stilicho.ruand the callback:
/auth/oidc/callbackResulting in:
https://komodo.stilicho.ru/auth/oidc/callbackImportant: the URL must exactly match the address Komodo actually uses. For example, you can’t specify
http://if Komodo is only reachable viahttps://.
4. Signing Key#
In the Signing Key field, choose any available signing key. If Authentik already has a default key, you can use it.
5. Bindings - optional#
The Configure Bindings section lets you restrict access to the application. For example, you can create a binding that only allows a specific user group to use Komodo. If you don’t need a restriction at this stage, you can skip this step.
6. Launch URL - optional#
For the Launch URL you can specify:
https://komodo.stilicho.ru/auth/oidc/loginThis lets you launch Komodo’s authorization directly from Authentik’s Application Dashboard.
7. Saving the Application#
Once you’ve finished configuring, click:
Submit
Authentik now has the following set up:
Application
│
└── OAuth2/OpenID Connect Provider
│
├── Client ID
├── Client Secret
├── Redirect URI
└── Signing KeySave the following values:
Application Slug
Client ID
Client SecretYou’ll need them when configuring Komodo.
Configuring Authentik in Komodo’s Environment File#
Now you need to pass Komodo the connection parameters for Authentik. In this case, the parameters go in:
compose.envThis file is loaded into the Komodo Core container via:
env_file: ./compose.envAdd or change the following variables:
KOMODO_HOST=https://komodo.stilicho.ru
KOMODO_OIDC_ENABLED=true
KOMODO_OIDC_PROVIDER=https://Authentik.stilicho.ru/application/o/<application_slug>/
KOMODO_OIDC_CLIENT_ID=<client_id_from_Authentik>
KOMODO_OIDC_CLIENT_SECRET=<client_secret_from_Authentik>For example:
KOMODO_HOST=https://komodo.stilicho.ru
KOMODO_OIDC_ENABLED=true
KOMODO_OIDC_PROVIDER=https://Authentik.stilicho.ru/application/o/komodo/
KOMODO_OIDC_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxx
KOMODO_OIDC_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxImportant point: Komodo can’t create OIDC users directly with admin rights. The first login always happens through the local admin defined in KOMODO_INIT_ADMIN_USERNAME/PASSWORD.
Launching#
Before spinning up the containers, there’s one trick worth doing - a symlink, so Docker Compose automatically picks up the variables without the --env-file flag:
cd /home/stilicho/docker/komodo
ln -s compose.env .env
docker compose up -dIf you don’t create the symlink and run docker compose up -d without --env-file compose.env, Compose won’t substitute the
KOMODO_DATABASE_USERNAME/PASSWORD variables into the environment: block of the mongo service - you’ll see a warning about empty values, and MongoDB will initialize with empty credentials. If this has already happened, you need to wipe the data and restart:
docker compose down
rm -rf mongo/data/* mongo/configdb/*
docker compose up -dCheck the logs for errors:
docker logs komodo-mongo --tail 50
docker logs komodo-core --tail 50First Login and Activating the OIDC Account#
- Open
https://komodo.stilicho.ru. - Log in as the local admin (
admin/ the password fromcompose.env). - Click the OIDC button and log in via Authentik - Komodo will automatically create your user (already active, thanks to
KOMODO_ENABLE_NEW_USERS=true). - Go back in as the local admin → Settings → Users → find the newly created OIDC user → promote them to Admin.
After that you can use Authentik login as your main method, with the local admin remaining as a fallback.
An important nuance with KOMODO_OIDC_AUTO_REDIRECT=true. This option automatically redirects any unauthenticated user to Authentik, bypassing Komodo’s login form - even in incognito mode. Because of this, you won’t be able to log in as the local admin the normal way. If you need to reach the local login form (for example, to promote a newly created OIDC user to admin), temporarily disable the redirect:
# in compose.env
KOMODO_OIDC_AUTO_REDIRECT=falsedocker compose up -dLog in as admin, make the necessary changes, then set KOMODO_OIDC_AUTO_REDIRECT=true back and apply docker compose up -d again.
Komodo’s Features and Where to Find Things#
Before moving on to configuration, let’s briefly go over the interface - what Komodo can actually do and where to find it after your first login. In the left-hand menu there are several main resource sections:
- Servers - a list of connected Docker hosts (you’ll have at least one - the local one - plus any hosts connected via Periphery, like in the Immich section below). You can see connection status, CPU/RAM/disk, and Docker version here.
- Stacks - the equivalent of Portainer’s “Stacks”: management of docker-compose projects. This is where your stacks end up after migration (see the next section).
- Containers - a flat list of all containers across all connected servers, with start/stop/restart/logs/exec actions, not tied to a specific stack.
- Builds - if you want Komodo to build Docker images from a git repository itself (the CI part, which Portainer doesn’t have at all).
- Repos - management of git repositories that Komodo clones and can watch for changes (for auto-redeploy via webhook).
- Procedures and Actions - multi-step automation scenarios (e.g., “stop stack A → make a backup → update the image → start it again”), which can be triggered manually, on a schedule, or via webhook.
- Syncs - declarative configuration sync: you describe all resources (stacks, servers, procedures) in TOML files in git, and Komodo brings the real state in line with what’s described - similar to Terraform, but for your homelab.
- Alerts - the alert log.
- Settings - users, roles, API keys, webhooks for notifications (Discord/Slack/Telegram/Gotify, etc.).
In a typical homelab scenario, you’ll spend 80% of your time in Servers and Stacks - the rest gets used as your needs grow (git builds, config sync).
Migrating Existing docker-compose Stacks into Komodo#
An important point to understand right away: unlike Dockge, Komodo does not scan the host automatically and does not pick up already-running compose projects on its own. Each stack needs to be explicitly “declared” in Komodo - specifying where its files are and which server it belongs to (the mechanics are described in the official Docker Compose / Stacks documentation). There’s no ready-made tool for automatic migration specifically from Portainer - the developers themselves confirm this in a GitHub discussion - you have to migrate manually, stack by stack (there’s an unofficial utility, komodo-import, which generates configuration from existing folders, but it’s a community tool, not part of Komodo itself). So I did it all by hand.
The good news: migration doesn’t require stopping your current containers. Portainer and Komodo work fine in parallel while you migrate stacks one at a time - your workflow isn’t interrupted.
How to Migrate One Stack#
Servers → select a host (or the general Stacks section → Create Stack).
Give the stack a name - it must match the name of the existing compose project. You can find out the current project name like this:
docker compose lsFile source - choose one of three modes:
- UI Defined - paste the compose file contents directly into the web interface; Komodo writes the file to the host itself on deploy.
- Files on Server - specify the path to an already-existing compose file on the host (this is what you need for migrating existing stacks - just point it to where they already live, e.g.
/home/alaricus/docker/vaultwarden). - Git Repo - Komodo clones the repository to the host and deploys from there; changes are tracked in git, and you can set up auto-redeploy on push via webhook.
For the Files on Server option - be sure to specify the correct
Run Directory(the folder containing the compose file) andFile Paths(the file name, usuallydocker-compose.yaml,docker-compose.yml, orcompose.yaml).If the stack has its own
.envfile (a common case - for example, I had many containers set up this way) - don’t try to recreate it via the Environment field in the UI. Use the separate Additional Env Files field, point it to.env(path relative toRun Directory), and make sure to uncheck “Track” - that’s specifically meant for externally managed files that you’ll keep editing manually on disk rather than through Komodo.Attach the stack to the desired Server (for the local host - the one named
Local, etc.).Click Deploy (or first Refresh, so Komodo reads the current state without recreating the containers) - if containers are already running under the same project name, Komodo will simply “take them under management” rather than recreate them from scratch.
Important note about paths with remote hosts. If a stack is being migrated to a host with a Periphery agent, the path to the compose file must be reachable inside that agent’s
root_directory. This applies to both installation methods, not just the container - I myself mistakenly thought the systemd variant didn’t have this restriction, but that’s not the case:root_directoryis a built-in limitation of Periphery itself, not a consequence of Docker mounting. For the container, this means the path must fall inside the mountedPERIPHERY_ROOT_DIRECTORY; for systemd, it must match theroot_directoryinperiphery.config.toml. In both cases, all your stacks must physically live inside that path.
If You Get “No Such File or Directory”#
This is the most common error during the first stack migration, and it almost always means one thing: the containerized Periphery can’t physically see the specified directory, because PERIPHERY_ROOT_DIRECTORY points to too narrow a folder (for example, only to komodo itself, rather than to the parent directory containing all your projects - see the warning at the beginning of the article).
Step-by-step diagnostics:
1. Check that Periphery can actually see the stack’s directory:
docker exec komodo-periphery ls /home/stilicho/docker/<stack_name>If you get No such file or directory - the mount is too narrow; move on to step 2.
2. Check that the .env → compose.env symlink was created (without it, PERIPHERY_ROOT_DIRECTORY from compose.env won’t be substituted into docker-compose.yaml when the container is recreated):
ls -la ~/docker/komodo/.envNo file - create it (see the “Launching” section above).
3. Fix PERIPHERY_ROOT_DIRECTORY in compose.env to point to the parent folder containing all your stacks, and recreate the containers from the project folder itself:
cd /home/stilicho/docker/komodo
docker compose down
docker compose up -dImportant: down + up, not restart.
4. Check the actual mount inside the already-running container:
docker inspect komodo-periphery --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'You should see a line with your new parent directory, not the old narrow path.
5. Go back to the stack page in the Komodo UI and click Refresh again (sometimes the interface holds a cached result from the previous attempt - if the error doesn’t disappear right away, refresh the whole browser page).
Bulk Migration via Sync#
If you have a lot of stacks and migrating them one by one through the UI is tedious, you can describe them all at once declaratively via Syncs - TOML files with resource definitions that Komodo applies in one go. Details in the official Sync Resources documentation. For a homelab with a dozen or two containers, manual migration through the UI is usually faster than figuring out TOML syntax - but if you have fifty stacks, Sync is worth it. Thankfully I don’t have more than 50 on a single host yet.
A single Komodo Core can manage several Docker hosts at once - for this, you need to install the Periphery agent on each additional host. For example, I have a separate host running Immich in Docker, which I connected exactly this way.
Periphery can be run as a Docker container (this is already built into our docker-compose.yaml for the local host), but for remote hosts, the officially recommended approach is a systemd install - it’s simpler and avoids complications with mounting the socket and paths through the container layer. A full description of all installation methods is in the official “Connect More Servers” documentation.
Root Installation (Recommended Method)#
On the target host where you want to install the agent:
curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py \
| sudo python3 - \
--core-address="https://komodo.stilicho.ru" \
--connect-as="immich-host" \
--onboarding-key="O-your_key_from_UI"The --onboarding-key comes from the Komodo UI: Servers → Add Server, where a one-time key of the form O-... is generated, which links the new agent to your Core.
Enable autostart:
sudo systemctl enable periphery
sudo systemctl status peripheryUser Installation (Without a Root Service)#
If you don’t want a root service, you can install Periphery as a user-level systemd service:
curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py \
| python3 - --user \
--core-address="https://komodo.stilicho.ru" \
--connect-as="immich-host" \
--onboarding-key="O-your_key_from_UI"Be sure to enable linger, otherwise the service will stop when you log out of the SSH session:
sudo loginctl enable-linger $USERPitfalls I Hit (and How to Avoid Them from the Start)#
With the user installation, at the time of writing this article the script does not change root_directory in the config - it remains /etc/komodo by default, which a regular user can’t write to. As a result, the service crashes with an error:
Failed to write private key pem to "/etc/komodo/keys/periphery.key"
Caused by: Permission denied (os error 13)And a second pitfall I hit later, when trying to migrate an existing immich stack into Komodo: root_directory isn’t just “where to put the keys” - it’s the only directory inside which Periphery can see any files at all (and this applies to the systemd variant too, not just the container - I myself mistakenly thought otherwise). If you point it to a “narrow” path like ~/.local/share/komodo, Periphery won’t see your actual compose projects living somewhere else (e.g., ~/docker/immich), and when you try to add a Stack you’ll get No such file or directory.
To avoid hitting these pitfalls twice, set the correct value in the config right away - the parent directory where all your compose projects on this host live (or will live):
nano ~/.config/komodo/periphery.config.tomlroot_directory = "/home/<your_user>/docker"Create the directory as your own user, not via sudo mkdir (otherwise it will be owned by root again, and the permission issues will repeat):
mkdir -p /home/<your_user>/dockerRestart:
systemctl --user daemon-reload
systemctl --user restart periphery
journalctl --user -u periphery -n 30 --no-pagerYou should see this in the log:
INFO PeripheryStartup: PeripheryConfig { ... root_directory: "/home/<your_user>/docker", ... }
INFO Logged in to Komodo Core komodo.stilicho.ru websocket as Server immich-hostIf you’ve already gone through onboarding with the old “narrow” path (as I initially, unknowingly, did) - after changing
root_directory, the keys (periphery.key,periphery.pub,core.pub) will be looked for at the new path and won’t be there. The simplest fix is to copy them from the old location to the new one, so you don’t have to onboard again:mkdir -p /home/<your_user>/docker/keys cp /home/<your_user>/.local/share/komodo/keys/* /home/<your_user>/docker/keys/After that, restarting the service should work without redoing onboarding.
Check that the needed folder (in my case, immich) is now visible:
ls /home/<your_user>/docker/immichIf the files are visible, you can go back to the Komodo UI and create a Stack for this service exactly the same way we did for local stacks (Server → immich-host, Source → Files on Server, Run Directory → /home/<your_user>/docker/immich, File Paths → your compose file’s name - check the exact name via docker compose ls right on that host).
Also make sure your user is in the docker group - without this, Periphery won’t be able to reach docker.sock:
groups $USER
sudo usermod -aG docker $USER # if needed - then log back inChecking in the UI#
After successful onboarding, the new host will appear in the Komodo UI under the Servers tab with status “Connected,” with visible metrics (CPU/RAM/disk) and the full container list for that host - including, in my case, Immich. From there you can manage them (deploy, restart, logs) directly from Komodo, without SSHing in.
Disk Monitoring and Alerts#
Komodo can send alerts about disk usage on each connected server. To get the correct size for the root partition specifically (rather than an under/overestimated value due to how counting works), compose.env already has:
PERIPHERY_INCLUDE_DISK_MOUNTS=/etc/hostnameI immediately got an alert like this (the one below is my actual alert):
{
"name": "Local",
"path": "/etc/hostname",
"used_gb": 178.38,
"total_gb": 228.39
}- this isn’t a Komodo bug, but an honest message about actual disk usage. I checked what was actually eating up space:
docker system df -v
sudo du -xh --max-depth=1 / | sort -rh | head -20Note the -x flag on du - it prevents the command from crossing into other mounted filesystems (e.g., network shares under /mnt), so you only see what’s actually taking up space on the local disk. And don’t forget sudo - without it, du silently skips directories like /var/lib/docker, which a regular user doesn’t have read access to, and the resulting figure will be significantly underestimated.
In the end I cleaned up the clutter and the alert went away.
Summary#
We deployed Komodo with:
- MongoDB as the database (without FerretDB/Postgres);
- bind mounts instead of named volumes for all data;
- Traefik routing by domain;
- login via Authentik (OIDC) with a fallback to the local admin;
- an additional host connected via Periphery as a systemd agent.
The main practical lesson of this article is that root_directory/PERIPHERY_ROOT_DIRECTORY needs to be thought through right away, before your first deploy, not after you hit No such file or directory. This restriction applies equally to both the containerized Periphery and the systemd agent (including on remote hosts like my immich-host) - point it right away to the parent directory where all your compose projects on a given host live or will live, not a narrow service folder. I lost a lot of time on this point trying to figure out what was wrong.
I also described what the interface itself looks like and how to migrate already-running compose stacks into Komodo without downtime.
Still left out of scope for now are Builds and Repos - i.e., building images straight from git and a full CI/CD workflow. I’m planning to record a video about setting up Forgejo as a self-hosted git server and connecting it to Komodo for automatic builds and webhook-triggered deploys - I’ll cover that in upcoming articles.




