Module 4: Dev, Staging, and Prod Environments in Self-Hosted

2. Reproducible base: the Self-Hosted AI Starter Kit

Description

By the end of this lesson you will be able to explain in your own words what a container is and what Docker Compose is —the two pieces this whole module rests on— and you'll meet the Self-Hosted AI Starter Kit, n8n's official package that brings up n8n, a database, a vector store, and local AI models, all with one command. You're going to read its docker-compose.yml line by line, understand what each service does, and know why it's the reproducible, zero-cost base from which we're going to derive the three environments.

This matters because everything that follows —the three isolated environments, their keys, their credentials— gets built on top of Docker Compose. If Docker Compose is a black box to you, the rest of the module turns into magic you copy without understanding, and magic you don't understand breaks at the worst moment. This lesson gives you the foundations: what runs, how it runs, and how to verify it's running. And along the way it hands you something valuable on its own: an n8n instance with local, free AI models, which in Module 5 you're going to use to test your AI workflows without paying a provider a single cent.

Connection to the module: lesson 1 gave you the why of environments. This one gives you the technical base: the reproducible package the three environments are going to be born from. In lesson 3 you're going to take this stack, understand it, and derive three isolated versions from it —one per environment. Think of this lesson as getting to know the base recipe well before cooking three dishes from it. You're not going to bring up the three environments yet; you're going to bring up one —the kit as it comes— to understand what it's made of.

What a container is, with plain analogies

Before touching anything, you need two concepts. The first is the container.

Think about how installing a complicated program on your computer used to work. You'd download the program, and it turned out it needed a specific version of something else, which in turn needed a library that clashed with another one you already had installed for a different program. Half an afternoon fighting dependencies, and in the end "works on my machine but not on yours." That hell has a name, and containers solve it.

A container is a sealed package that brings the program plus everything the program needs to run —its exact version of everything, its libraries, its configuration— bundled together, isolated from the rest of your machine. Think of it as a complete lunchbox: not just the sandwich, but also the juice, the napkin, and the fork, all in one closed container. It doesn't matter what table you open it on —your laptop, a teammate's, a server in the cloud— inside it's always exactly the same thing, working exactly the same way. That's what it means for a container to be reproducible: it behaves the same on any machine, because it carries its world inside.

A couple of terms you're going to see and are worth telling apart:

  • An image is the recipe or the mold for the container: the definition of what goes inside. For example, n8nio/n8n:latest is n8n's official image. The image is the mold; it doesn't run on its own.
  • A container is a live instance of that image: the mold already filled and running. From one image you can create many containers, just like from one Jell-O mold you get many identical Jell-Os. This idea —many containers from the same mold— is exactly what's going to give us three environments that are the same but separate.
  • Docker is the program that runs the containers on your machine. You installed it as a prerequisite for the guide. It's the "oven" where the lunchboxes get heated and run.

The property that makes containers perfect for environments is isolation: one container doesn't see what's happening inside another. Two containers on the same machine are like two closed lunchboxes sitting side by side: they share the table, but what's inside one doesn't mix with what's inside the other. When in lesson 3 we run three n8n instances, that isolation is what guarantees a change in one doesn't touch the others.

What Docker Compose is, with plain analogies

The second concept is Docker Compose, and it comes from a practical problem.

n8n rarely runs alone. To really work it needs a database to store the workflows and credentials in. If you also want local AI, you need a model server. If you want RAG, a vector store. Suddenly you don't have one container, you have four, and all of them have to start in the right order, know each other, and talk to each other. Bringing up four containers by hand, one by one, with the exact commands in the right order, every time, is tedious and error-prone.

Docker Compose solves that. It's a tool that reads a single file —called docker-compose.yml— where you declare all the containers that make up your system, how each one gets configured, and how they connect to each other. Then, with one command, Compose brings up the whole set at once, in the right order.

Think of it as the conductor of an orchestra. Each musician —each container— knows how to play their instrument. But for a symphony to come out instead of noise, someone has to say who comes in when and how they coordinate. The docker-compose.yml is the score; the docker compose up command is the conductor raising the baton. You don't talk to each musician separately: you talk to the conductor, once, and they coordinate everyone.

It's worth knowing the file's vocabulary, because you're going to read it a lot:

  • A service (service) is each container declared in the file. n8n is one service, postgres is another. A service says which image to use and how to configure it.
  • A volume (volume) is a persistent disk space Docker gives a container to store data that has to survive even if the container shuts down. This is crucial: by default, when a container gets destroyed, everything inside it is lost —the lunchbox gets thrown out. A volume is like an external drawer connected to the lunchbox: even if you throw out the lunchbox and bring a new one, the drawer —with your workflows and your database— is still there. Without volumes, every time you restarted n8n you'd lose everything.
  • A network (network) is the private channel through which containers in the same Compose talk to each other. Compose creates one automatically, and inside it every service is reachable by its name: n8n finds the database just by writing postgres, with no IP or manual network configuration.
  • A port (port) is the window through which your machine reaches a container. A container is isolated, so to reach it from your browser you have to open a window: 5678:5678 means "connect port 5678 on my machine to port 5678 inside the container." Ports are going to be key in lesson 3, because three environments on the same machine can't all use the same window.

With those two concepts —container and Compose— you can already read the Starter Kit. Let's go.

What the Self-Hosted AI Starter Kit is

The Self-Hosted AI Starter Kit is an official n8n project: a ready-made docker-compose.yml that brings up, in one shot, everything you need to work with n8n and local AI, free and on your machine. It's the fastest, most reproducible way to have a "real" n8n running, and that's why we use it as our base.

It brings four pieces, each in its own container:

ServiceWhat it isWhat it's for in the guide
n8nThe automation platformRuns order-triage and the rest of the workflows
PostgreSQLA robust databaseStores the workflows and credentials (better than n8n's default database)
QdrantA vector storeStores embeddings for RAG cases; here we just meet it, we don't use it in depth
OllamaA local AI model serverRuns language models on your own machine, free, no external API

Two honest clarifications before moving on. First: of these four pieces, the two this module really uses are n8n and PostgreSQL. Qdrant (the vector store) and Ollama (the local models) come in the kit and are extremely valuable —Ollama is what's going to let you test AI workflows at zero cost in Module 5— but for this module's goal (isolating environments) the n8n + database pair is the essential part. In lesson 3 you're going to see that, to avoid wasting extra memory, each environment is going to run a lighter version of the stack. For now, get to know the full kit, because it's the base everything derives from.

Second: Qdrant is the vector store the official kit ships. Others exist —Milvus, for example— that you could swap in, but the kit ships with Qdrant. And Ollama's models: the kit downloads a model from the Llama family the first time you run its demo workflow. The model's exact version changes with the kit's version, so that detail is worth checking in the official repository when you use it; you can download other models, like Mistral, with ollama pull mistral. Don't memorize the model version: memorize that it's local and free.

The kit's docker-compose.yml, from the inside

Let's look at the kit's heart. You're not going to write it yourself —you clone it from the official repository— but reading it with understanding is what separates you from copying without knowing. This is, in simplified and annotated form, the skeleton of the Starter Kit's docker-compose.yml. The concrete values (images, ports, volumes) are the official kit's; read the comments, they're the map.

# docker-compose.yml — Self-Hosted AI Starter Kit (annotated skeleton)

volumes:                 # The persistent drawers: survive even if the container gets recreated
  n8n_storage:           # The ~/.n8n folder lives here: the encryption key and the config
  postgres_storage:      # The database's data lives here
  ollama_storage:        # The downloaded AI models live here (several GB)
  qdrant_storage:        # The vector store's embeddings live here

services:

  postgres:                          # The database
    image: postgres:16-alpine        # Official PostgreSQL image, version 16, lightweight
    container_name: postgres
    environment:                     # Filled in from the .env (see below)
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    volumes:
      - postgres_storage:/var/lib/postgresql/data   # Data goes to the persistent drawer
    healthcheck:                     # Docker watches that the database is healthy
      test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']

  n8n:                               # The automation platform
    image: n8nio/n8n:latest          # Official n8n image, latest version
    container_name: n8n
    ports:
      - 5678:5678                    # The window: you reach it at http://localhost:5678
    environment:
      - DB_TYPE=postgresdb           # n8n uses PostgreSQL, not its default database
      - DB_POSTGRESDB_HOST=postgres  # Finds the database by its service name
      - DB_POSTGRESDB_USER=${POSTGRES_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}   # The master key (Module 3)
      - N8N_DIAGNOSTICS_ENABLED=false
      - N8N_PERSONALIZATION_ENABLED=false
    volumes:
      - n8n_storage:/home/node/.n8n  # The ~/.n8n folder goes to the persistent drawer
    depends_on:                      # n8n doesn't start until the database is healthy
      postgres:
        condition: service_healthy

  qdrant:                            # The vector store
    image: qdrant/qdrant
    container_name: qdrant
    ports:
      - 6333:6333
    volumes:
      - qdrant_storage:/qdrant/storage

  ollama-cpu:                        # The local AI model server (CPU version)
    profiles: ["cpu"]                # Only starts if you request the "cpu" profile (see command)
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - 11434:11434
    volumes:
      - ollama_storage:/root/.ollama

Let's go to the pieces most worth understanding:

volumes (at the top). The four persistent drawers. The most important one for us is n8n_storage, mounted at /home/node/.n8n inside n8n's container: that's where the ~/.n8n folder lives, and with it the encryption key n8n generates. Remember from Module 3: that folder holds secrets, which is why .gitignore ignores it. With Docker, that folder no longer lives loose on your disk; it lives in a Docker volume, even more isolated.

postgres. The database. It uses the official postgres:16-alpine image (the alpine tag means a lightweight Linux variant, so the container weighs less). Notice it doesn't expose any port to your machine: only the other containers reach it, over Compose's private network. That's deliberate: the database has no reason to be reachable from your browser. The healthcheck is a test Docker runs periodically to know whether the database is alive; n8n uses it so it doesn't start too early.

n8n. The star. Three key details. First, ports: 5678:5678 is the window: that's why you reach the editor at http://localhost:5678. Second, the DB_* variables tell it to use PostgreSQL instead of its default database, and to find the database just by writing postgres —the service name— with no IP: Compose's network resolves that. Third, N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} takes the encryption key from the .env file. Keep that detail in mind: it's exactly the piece you're going to make different per environment in lesson 4.

depends_on with condition: service_healthy. Tells Compose "don't start n8n until the database is healthy." It's the conductor enforcing the order: database first, then n8n. Without this, n8n would start, wouldn't find the database ready, and would fail.

ollama-cpu and its profiles. The local model server. The profiles: ["cpu"] line is a switch: this service only starts if you explicitly request the cpu profile when bringing up the stack. The kit ships several profiles —cpu, gpu-nvidia, gpu-amd— to fit your hardware, so it doesn't force everyone to start Ollama if they don't want to. You're going to see it in the command below.

Worked example: bringing up the kit for the first time

Let's bring up the kit as it comes, to see it work and understand what it's made of. Remember: you run these commands on your machine; this guide doesn't run them for you. I'm going to tell you command by command what it does and what you're going to see.

Step 1 — Clone the official repository.

git clone https://github.com/n8n-io/self-hosted-ai-starter-kit.git
cd self-hosted-ai-starter-kit

git clone downloads a complete copy of the repository to your machine; cd moves you into the folder that got created. What to expect: a new folder called self-hosted-ai-starter-kit with the docker-compose.yml inside, and your terminal now "standing" inside it.

Step 2 — Create your .env file from the example.

cp .env.example .env

cp copies a file; here it copies the .env.example template into a new .env file. This .env is where the concrete values live —passwords, the encryption key— that docker-compose.yml reads with the ${...} syntax. What to expect: a new .env file, with content roughly like this:

POSTGRES_USER=root
POSTGRES_PASSWORD=password
POSTGRES_DB=n8n

N8N_ENCRYPTION_KEY=super-secret-key
N8N_USER_MANAGEMENT_JWT_SECRET=even-more-secret

⚠️ Stop here, this matters. Those values —password, super-secret-key, even-more-secret— are example placeholders, not real secrets. They're there so the kit starts up on the first try and you can try it out. For any serious use you have to replace them with your own, random values, and that .env with the real values never gets committed —same as you learned in Module 3. Lesson 4 devotes its full attention to generating real keys and keeping them out of the repository. For now, for this first "does it start" test, the example values work; just be crystal clear that they're not for production.

Step 3 — Bring up the stack.

docker compose --profile cpu up

docker compose up is the conductor raising the baton: it reads the docker-compose.yml, downloads any missing images, creates the volumes and networks, and starts the containers in order. The --profile cpu turns on the Ollama service in its CPU version (if you have a compatible GPU, you'd use --profile gpu-nvidia or --profile gpu-amd; on a Mac with Apple Silicon, the pattern is different and it's worth checking the kit's README).

What to expect: the first time, this takes a while. Docker downloads several gigabytes of images and models —you're going to see many download-progress lines; that's fine, it's normal, it isn't broken. When it's done, the terminal stays "busy" showing the running containers' logs. Among those lines you're going to see a message from n8n indicating the editor is available. If you open your browser at http://localhost:5678, n8n's initial setup screen greets you. That screen is the success signal: it means n8n started, found its database, and is ready.

Step 4 — Shut down the stack when you're done. In the terminal where it's running, press Ctrl+C, and then:

docker compose --profile cpu down

down shuts down and removes the containers and the network, but —and this is the reassuring part— it keeps the volumes. Your workflows, your database, and your models stay in their persistent drawers. Next time you bring up the stack, everything is where you left it. Shutting down the container doesn't delete your data: that's what volumes are for.

The local models: the ace up your sleeve

It's worth stopping on Ollama, because even though it isn't this module's focus, it's the piece that makes one of the whole guide's biggest promises possible, and it's worth understanding it from now.

Ollama is a server that runs language models on your own machine. Instead of order-triage's AI Agent node calling a model provider over the internet —and getting charged for every call— it can call a model living in the ollama container, on your laptop, for free. The first time a workflow uses a model, Ollama downloads it (several gigabytes, which stay in the ollama_storage volume); from then on, calls are local and instant, with no cost or usage limit.

Why is this an ace up your sleeve? Because testing AI workflows costs money when every test calls a paid provider. If you want to run order-triage a hundred times to test that it classifies correctly, a hundred calls to an external provider add up. With a local model, those hundred tests cost zero. In Module 5, when you learn to test AI Agent-node workflows in depth, Ollama's local models are what makes that "at zero cost" literal. This module just introduces the tool; Module 5 exploits it.

How you reach the model from n8n: inside Compose's network, the ollama service is reachable by its name, on port 11434. In the AI Agent node you'd configure the Ollama model pointing at http://ollama:11434 (the service name, not localhost, because n8n talks to it from inside the container network). To download a model by hand, you enter Ollama's container and use ollama pull:

docker exec -it ollama ollama pull mistral

docker exec -it ollama enters the container named ollama; ollama pull mistral downloads the Mistral model. What to expect: a download progress bar and, when it finishes, the model available for your workflows. (Which models exist and which one's best depends on the moment and your machine; check Ollama's site.)

An honest note about resources: local AI models demand memory. A small model runs fine on a modern laptop; large ones demand a lot of RAM and, to run fast, a GPU. That's why the kit ships the cpu/gpu-nvidia/gpu-amd profiles. If your machine is tight, use small models in dev to test the workflow's logic —that it classifies, that it decides— even if the local model's quality doesn't match a production provider's. To test the logic, a modest model is enough; fine quality gets validated in staging/prod with the real model. Not being able to spend money while testing is worth more than the last drop of quality in the playroom environment.

Why this base is "reproducible" and zero-cost

It's worth closing with the two properties that make this kit the ideal base for the module.

Reproducible. Everything needed to run n8n is declared in the docker-compose.yml and the .env. There are no hidden manual steps, no "and also I installed this by hand and forgot to write it down." Anyone who clones the repository and runs the same command gets exactly the same stack. That reproducibility is why Docker Compose is the right base for environments: if dev and prod are born from the same file, you know they're alike by construction, and the differences between them are only the ones you declared on purpose in their .env. No phantom differences.

Zero-cost. The kit is open-source software: n8n Community, PostgreSQL, Qdrant, and Ollama are all free. AI models run on your own machine with Ollama, so you don't even pay for calls to a model provider's API. This guide's entire path —including testing AI workflows in Module 5— gets done without pulling out a card. The only cost is your own machine running the containers.

With this base understood, you already have what you need for the big step: turning one stack into three isolated stacks, one per environment. That's lesson 3.

Common mistakes

Committing the .env with the encryption key (practical, and the most serious one). What happens: someone clones the kit, copies .env.example to .env, and in a distracted git add . pushes the .env —with the encryption key and the passwords— to the repository. Why it happens: the .env is right there in the project folder, and git add . catches everything it sees. How to spot it: run git status and look for .env in the list; if it shows up without being ignored, you're one commit from the accident. How to fix it: the Module 3 reflex —.env in .gitignore from before the first commit, .env.example does get versioned (with no real values). And if the .env's values are still the example ones, there's no secret to lose yet; the danger arrives the moment you replace them with real ones, so set up .gitignore well before that moment.

Leaving the example values and believing it's already secure (conceptual). What happens: someone brings up the kit with POSTGRES_PASSWORD=password and N8N_ENCRYPTION_KEY=super-secret-key and moves on as if that were a real configuration. Why it happens: the kit starts up perfectly with those values, so "it works" gets confused with "it's fine." How to spot it: if your encryption key literally says super-secret-key, it isn't secret —it's in the kit's public repository, everyone knows it. How to fix it: replace the example values with your own random ones before any use that matters. Lesson 4 teaches you how to generate them properly.

Confusing shutting down the container with deleting the data (conceptual). What happens: someone runs docker compose down, watches the containers disappear, and panics believing they lost their workflows. Or the opposite: they want to start from scratch and think down already deleted everything, when the data is still in the volumes. Why it happens: no distinction is made between the container (ephemeral) and the volume (persistent). How to spot it: run docker volume ls; if your volumes (..._n8n_storage, ..._postgres_storage) are still listed, your data is still there. How to fix it: remember the rule —the container is the disposable lunchbox, the volume is the drawer that survives. To actually delete the data you need docker compose down -v (the -v removes the volumes), and that command gets used with full awareness that it does delete.

Exercises

Exercise 1 — Translate the docker-compose.yml. Look at this lesson's kit docker-compose.yml skeleton and answer, without re-reading the explanation: (a) why doesn't the postgres service have a ports section, if n8n needs to talk to it? (b) What would happen to your workflows if you deleted the n8n_storage volume? (c) What does the depends_on: postgres: condition: service_healthy line do?

See solution

(a) Because postgres only needs to be reachable by other containers, not by your browser. Containers in the same Compose talk over an internal private network, where they find each other by name (postgres), with no ports exposed. Exposing a port means opening a window to your machine, and the database doesn't need it: it would even be less secure.

(b) You'd lose everything living in ~/.n8n: the encryption key n8n generated and the configuration. Since the encryption key would be gone, even if you kept the database, the credentials encrypted with that key could no longer be decrypted. That's why the volume is sacred.

(c) It tells Compose to not start n8n until the database passes its health check (healthcheck). It's the conductor enforcing the order: the database alive first, then n8n. It stops n8n from starting, not finding the database ready, and failing.

Why it works: if you answered all three, you already read a docker-compose.yml with understanding, not as a spell. That ability is what makes lesson 3 —three stacks derived from this one— feel natural instead of magical.

Exercise 2 — Predict the occupied port. Imagine you already have the kit running with n8n on port 5678. Without shutting it down, you try to bring up a second identical stack, with another n8n that also asks for port 5678. What do you think happens, and why? What would you have to change in the second stack so both can coexist?

See solution

The second stack fails to start with a port-already-in-use error (something like "port is already allocated"). The reason: a port on your machine is a single window; only one container can hold 5678 at a time. Two containers can't share the same window.

For both to coexist, the second n8n has to use a different window: for example, mapping 5679:5678 instead of 5678:5678. That way the second n8n still listens on its internal 5678, but you reach it from http://localhost:5679, without colliding with the first one.

Why it works: you just discovered on your own the central problem of running several environments on one machine —ports collide— and its solution —a different window per environment. It's exactly what lesson 3 formalizes with 5678/5679/5680 for dev/staging/prod.

Exercise 3 — Tell apart image, container, and volume. Explain each one in one sentence, with your own analogy (different from the lunchbox and the Jell-O mold), and then say which of the three you should never lose if you're keeping n8n's configuration, and why.

See solution

There's no single correct answer; what matters is that the three analogies distinguish well: the image is the definition or recipe (doesn't run on its own), the container is a live instance of that recipe (ephemeral), and the volume is the persistent storage connected to the container (survives its destruction).

The one you should never lose is the volume, specifically n8n_storage. You can re-download the image whenever you want (it's on the internet), and you can recreate the container with a command. But the volume contains your encryption key and your data: if it's lost, there's nowhere to recover it from. The image and the container are replaceable; the volume is unique.

Why it works: this distinction is what prevents two opposite panics —believing shutting down a container deletes the data, and believing it's safe when you actually deleted the volume. Being clear on which of the three is irreplaceable gives you peace of mind and care in the right dose.

Summary and next step

In this lesson you met the two pieces the whole module rests on. A container is a sealed package with the program plus everything it needs, reproducible and isolated —the complete lunchbox; an image is its mold and a volume is the persistent drawer that survives even if the container gets destroyed. Docker Compose is the orchestra conductor that reads a single docker-compose.yml and brings up all the containers coordinated with one command. You met the Self-Hosted AI Starter Kit —n8n + PostgreSQL + Qdrant + Ollama— read its docker-compose.yml from the inside (services, volumes, ports, depends_on, profiles), and brought it up with git clone, cp .env.example .env, and docker compose --profile cpu up, verifying success at http://localhost:5678. And you locked in the security warning: the example .env values aren't real secrets, and the .env with your own values never gets committed.

Before moving on you should be able to: explain with an analogy what a container is and what Docker Compose is; name the kit's four services and what each one is for; and say why the n8n_storage volume is the one that must never be lost.

Lesson 3 takes the module's big leap: taking this single stack and turning it into three isolated stacks, one per environment. You're going to see how Docker isolates dev, staging, and prod so they don't touch each other —separate containers, volumes, networks, and databases— what mechanism makes that isolation happen (the Compose project name), what never gets shared between environments, and the naming and port conventions that keep you from confusing one environment with another.

Resources