Local Demo

The best way to test InterDiode is to use it locally with Docker Compose, using the compose.yaml file below. It provides a minimal installation of two servers with a unidirectional UDP data transfer. The black instance (the one normally connected to the internet) will be available at http://localhost:8881/, the red instance (normally on the internal network) will be available at http://localhost:8882/.

InterDiode provides Docker/OCI images that are available in Docker Hub. It is possible to always use the latest stable tag or to use another service that handles updating Docker images.

This guide explains the setup based on docker-compose, but the installation of docker-compose itself is out of scope of this documentation. To do this, please follow the official install instructions.

Only Docker and docker-compose are required to be installed on the host machine. The example below demonstrates downloading a free license, but you can use any valid InterDiode license.

export EMAIL=<enter_your_email_here>
export NAME=<enter_your_name_here>
# download a license tied to your email and name, and store it in the compose.env file
echo "LICENSE_KEY=$(curl -L -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data-urlencode product_id=42 --data-urlencode buyer_name=${NAME} --data-urlencode buyer_email=${EMAIL} https://license.interdiode.fr/v1/license/)" > compose.env
# download the compose.yaml file
curl -L -O https://interdiode.fr/static/latest/_static/examples/compose.yaml
# start the demo
docker compose up -d

Next Steps

  • Once all the containers are running, you can access the web interfaces of both instances at http://localhost:8881/ for the black instance and http://localhost:8882/ for the red instance.

  • Access to the black instance and create a new account with the “Create account” button. Then, log in with the newly created account.

  • In the “Administration” section, add the license file that you obtained from the InterDiode team. The license will be automatically applied to the red instance.

  • Access to the red instance and create a new account with the “Create account” button. Then, log in with the newly created account.

compose.yaml
services:
  background_black:
    command: interdiode-ctl run --prometheus-bind 0.0.0.0 --prometheus-port 9000 background
    depends_on:
      db_black:
        condition: service_healthy
      mailpit_black:
        condition: service_healthy
      migrate_black:
        condition: service_completed_successfully
      redis_black:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_black:5432/interdiode_db
    - EMAIL_FROM=admin-black@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_black:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=black
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8081
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_black/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8881/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, python, -c, 'import os; fd = open(''/data/run/interdiode-background.pid''); pid=fd.read().strip() ; os.kill(int(pid), 0); fd.close()']
      timeout: 1s
    image: interdiode/interdiode:2026.6.13
    restart: always
    volumes:
    - ./files/black:/data/files
    - ./caches/black:/data/caches
  background_red:
    command: interdiode-ctl run --prometheus-bind 0.0.0.0 --prometheus-port 9000 background
    depends_on:
      db_red:
        condition: service_healthy
      mailpit_red:
        condition: service_healthy
      migrate_red:
        condition: service_completed_successfully
      redis_red:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_red:5432/interdiode_db
    - EMAIL_FROM=admin-red@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_red:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=red
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8082
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_red/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8882/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, python, -c, 'import os; fd = open(''/data/run/interdiode-background.pid''); pid=fd.read().strip() ; os.kill(int(pid), 0); fd.close()']
      timeout: 1s
    image: interdiode/interdiode:2026.6.13
    restart: always
    volumes:
    - ./files/red:/data/files
    - ./caches/red:/data/caches
  beat_black:
    command: interdiode-ctl run --prometheus-bind 0.0.0.0 --prometheus-port 9000 beat
    depends_on:
      db_black:
        condition: service_healthy
      mailpit_black:
        condition: service_healthy
      migrate_black:
        condition: service_completed_successfully
      redis_black:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_black:5432/interdiode_db
    - EMAIL_FROM=admin-black@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_black:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=black
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8081
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_black/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8881/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, python, -c, 'import os; fd = open(''/data/run/interdiode-beat.pid''); pid=fd.read().strip() ; os.kill(int(pid), 0); fd.close()']
      timeout: 1s
    image: interdiode/interdiode:2026.6.13
    restart: always
    volumes:
    - ./files/black:/data/files
    - ./caches/black:/data/caches
  beat_red:
    command: interdiode-ctl run --prometheus-bind 0.0.0.0 --prometheus-port 9000 beat
    depends_on:
      db_red:
        condition: service_healthy
      mailpit_red:
        condition: service_healthy
      migrate_red:
        condition: service_completed_successfully
      redis_red:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_red:5432/interdiode_db
    - EMAIL_FROM=admin-red@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_red:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=red
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8082
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_red/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8882/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, python, -c, 'import os; fd = open(''/data/run/interdiode-beat.pid''); pid=fd.read().strip() ; os.kill(int(pid), 0); fd.close()']
      timeout: 1s
    image: interdiode/interdiode:2026.6.13
    restart: always
    volumes:
    - ./files/red:/data/files
    - ./caches/red:/data/caches
  db_black:
    environment:
    - POSTGRES_HOST_AUTH_METHOD=trust
    - POSTGRES_DB=interdiode_db
    - POSTGRES_PASSWORD=my-postgres-password
    - POSTGRES_USER=interdiode
    - PGDATA=/var/lib/postgresql/data/postgresql
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, pg_isready, -U, interdiode, -d, interdiode_db]
      timeout: 1s
    image: postgres:18
    restart: always
    volumes:
    - ./postgres/black:/var/lib/postgresql/data/postgresql
  db_red:
    environment:
    - POSTGRES_HOST_AUTH_METHOD=trust
    - POSTGRES_DB=interdiode_db
    - POSTGRES_PASSWORD=my-postgres-password
    - POSTGRES_USER=interdiode
    - PGDATA=/var/lib/postgresql/data/postgresql
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, pg_isready, -U, interdiode, -d, interdiode_db]
      timeout: 1s
    image: postgres:18
    restart: always
    volumes:
    - ./postgres/red:/var/lib/postgresql/data/postgresql
  mailpit_black:
    environment:
    - MP_DATABASE=/tmp/mailpit.db
    - MP_DISABLE_VERSION_CHECK=1
    - MP_DISABLE_WAL=1
    - MP_MAX_MESSAGES=5000
    - MP_POP3_AUTH=admin_user:password user:password black_user:password red_user:password interdiode:password
    - MP_SMTP_AUTH=admin_user:password user:password black_user:password red_user:password interdiode:password
    - MP_SMTP_AUTH_ALLOW_INSECURE=1
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, wget, --spider, -q, 'http://localhost:8025/']
      timeout: 1s
    image: axllent/mailpit
    ports:
    - 127.0.0.1:8981:8025
    restart: always
  mailpit_red:
    environment:
    - MP_DATABASE=/tmp/mailpit.db
    - MP_DISABLE_VERSION_CHECK=1
    - MP_DISABLE_WAL=1
    - MP_MAX_MESSAGES=5000
    - MP_POP3_AUTH=admin_user:password user:password black_user:password red_user:password interdiode:password
    - MP_SMTP_AUTH=admin_user:password user:password black_user:password red_user:password interdiode:password
    - MP_SMTP_AUTH_ALLOW_INSECURE=1
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, wget, --spider, -q, 'http://localhost:8025/']
      timeout: 1s
    image: axllent/mailpit
    ports:
    - 127.0.0.1:8982:8025
    restart: always
  migrate_black:
    command: interdiode-ctl configuration apply --no-input
    depends_on:
      db_black:
        condition: service_healthy
      mailpit_black:
        condition: service_healthy
      redis_black:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_black:5432/interdiode_db
    - EMAIL_FROM=admin-black@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_black:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=black
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8081
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_black/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8881/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    image: interdiode/interdiode:2026.6.13
    volumes:
    - ./files/black:/data/files
    - ./caches/black:/data/caches
  migrate_red:
    command: interdiode-ctl configuration apply --no-input
    depends_on:
      db_red:
        condition: service_healthy
      mailpit_red:
        condition: service_healthy
      redis_red:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_red:5432/interdiode_db
    - EMAIL_FROM=admin-red@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_red:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=red
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8082
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_red/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8882/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    image: interdiode/interdiode:2026.6.13
    volumes:
    - ./files/red:/data/files
    - ./caches/red:/data/caches
  receive_red:
    command: interdiode-ctl hairgap --prometheus-bind 0.0.0.0 --prometheus-port 9000 receive
    depends_on:
      db_red:
        condition: service_healthy
      mailpit_red:
        condition: service_healthy
      migrate_red:
        condition: service_completed_successfully
      redis_red:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_red:5432/interdiode_db
    - EMAIL_FROM=admin-red@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_red:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=red
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8082
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_red/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8882/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, python, -c, 'import os; fd = open(''/data/run/interdiode-hairgap.pid''); pid=fd.read().strip() ; os.kill(int(pid), 0); fd.close()']
      timeout: 1s
    image: interdiode/interdiode:2026.6.13
    restart: always
    volumes:
    - ./files/red:/data/files
    - ./caches/red:/data/caches
  redis_black:
    command: redis-server --requirepass 'my-redis-password' --save 60 1 --loglevel warning
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, redis-cli, ping]
      timeout: 1s
    image: redis:alpine
    restart: always
  redis_red:
    command: redis-server --requirepass 'my-redis-password' --save 60 1 --loglevel warning
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, redis-cli, ping]
      timeout: 1s
    image: redis:alpine
    restart: always
  send_black:
    command: interdiode-ctl hairgap --prometheus-bind 0.0.0.0 --prometheus-port 9000 send
    depends_on:
      db_black:
        condition: service_healthy
      mailpit_black:
        condition: service_healthy
      migrate_black:
        condition: service_completed_successfully
      redis_black:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_black:5432/interdiode_db
    - EMAIL_FROM=admin-black@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_black:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=black
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8081
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_black/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8881/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, python, -c, 'import os; fd = open(''/data/run/interdiode-hairgap.pid''); pid=fd.read().strip() ; os.kill(int(pid), 0); fd.close()']
      timeout: 1s
    image: interdiode/interdiode:2026.6.13
    restart: always
    volumes:
    - ./files/black:/data/files
    - ./caches/black:/data/caches
  web_black:
    command: interdiode-ctl run --prometheus-bind 0.0.0.0 --prometheus-port 9000 http
    depends_on:
      db_black:
        condition: service_healthy
      mailpit_black:
        condition: service_healthy
      migrate_black:
        condition: service_completed_successfully
      redis_black:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_black:5432/interdiode_db
    - EMAIL_FROM=admin-black@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_black:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=black
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8081
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_black/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8881/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, python, -c, 'import os; fd = open(''/data/run/interdiode-http.pid''); pid=fd.read().strip() ; os.kill(int(pid), 0); fd.close()']
      timeout: 1s
    image: interdiode/interdiode:2026.6.13
    ports:
    - 127.0.0.1:8881:8081
    restart: always
    volumes:
    - ./files/black:/data/files
    - ./caches/black:/data/caches
  web_red:
    command: interdiode-ctl run --prometheus-bind 0.0.0.0 --prometheus-port 9000 http
    depends_on:
      db_red:
        condition: service_healthy
      mailpit_red:
        condition: service_healthy
      migrate_red:
        condition: service_completed_successfully
      redis_red:
        condition: service_healthy
    env_file:
    - ./compose.env
    environment:
    - ALLOW_LOCAL_USERS=true
    - ALLOW_USER_CREATION=true
    - DATABASE_URL=postgresql://interdiode:my-postgres-password@db_red:5432/interdiode_db
    - EMAIL_FROM=admin-red@interdiode.example.com
    - EMAIL_HOST_URL=smtp://interdiode:password@mailpit_red:1025
    - HTTP_REMOTE_GROUPS_HEADER=
    - HTTP_REMOTE_USER_HEADER=
    - INTERDIODE_MODE=red
    - LANGUAGE_CODE=en-us
    - LISTEN_ADDRESS=0.0.0.0:8082
    - LOG_DIR=
    - LOG_LEVEL=debug
    - REDIS_URL=redis://:my-redis-password@redis_red/1
    - RED_DESTINATION_IP=receive_red
    - RED_DESTINATION_PORT=9091
    - REMOTE_USER_DEFAULT_GROUPS=
    - REQUIRE_NEW_USER_VALIDATION=false
    - SERVER_BASE_URL=http://localhost:8882/
    - SHARED_TRANSFER_KEY=secret_key
    - TRANSFER_MODE=udp
    - WORKER_PROCESSES=4
    healthcheck:
      interval: 1s
      retries: 10
      test: [CMD, python, -c, 'import os; fd = open(''/data/run/interdiode-http.pid''); pid=fd.read().strip() ; os.kill(int(pid), 0); fd.close()']
      timeout: 1s
    image: interdiode/interdiode:2026.6.13
    ports:
    - 127.0.0.1:8882:8082
    restart: always
    volumes:
    - ./files/red:/data/files
    - ./caches/red:/data/caches