Skip to content

Deployment

Pepeunit Deploy

The Pepeunit Deploy repository contains all files and examples needed to run a Pepeunit instance using docker compose. To clone it, run:

bash
git clone https://git.pepemoss.com/pepe/pepeunit/pepeunit_deploy.git
cd pepeunit_deploy

Filling the primary .env file

You can choose between two installation options:

  1. For local use – .env.local.example
  2. For global use – .env.global.example

The difference is that the local option is intended for operation within a local network, while the global option allows accessing the Pepeunit instance via a domain name over https.

Choose one file and remove the .example suffix:

  • .env.local.example -> .env.local
  • .env.global.example -> .env.global

For example, using:

bash
mv .env.local.example .env.local

The .env.local and .env.global files use a reduced set of environment variables, because all docker compose service env files are generated from them:

VariableExample for .env.localExample for .env.globalPurpose
POSTGRES_USERpepeunit-adminpepeunit-adminUsername under which the database will be created
POSTGRES_PASSWORDf6prBUMbhvNnlLZ0f0HNf6prBUMbhvNnlLZ0f0HNPassword for the database user
POSTGRES_DBpepeunit-dbpepeunit-dbDatabase name
CLICKHOUSE_USERpepeunit-adminpepeunit-adminUsername under which the ClickHouse database will be created
CLICKHOUSE_PASSWORDf6prBUMbhvNnlLZ0f0HNf6prBUMbhvNnlLZ0f0HNPassword for the ClickHouse user
CLICKHOUSE_DBdefaultdefaultClickHouse database name
PU_DOMAIN192.168.0.22unit.pepeunit.comDomain name of the Pepeunit instance
PU_SECUREFalse-Selects http or https for the BACKEND_DOMAIN of the Pepeunit instance, https by default
PU_TELEGRAM_TOKEN 1111111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1111111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATelegram Bot token you obtain from Bot Father
PU_TELEGRAM_BOT_LINKhttps://t.me/PepeUnitRobothttps://t.me/PepeUnitRobotLink to the Telegram Bot you create via Bot Father
PU_MQTT_HOST192.168.0.22emqx.pepeunit.comDomain name of the EMQX instance. For local use ensure the address is reachable from containers; 127.0.0.1 will most likely not work
PU_MQTT_SECUREFalse-Selects http or https for MQTT_HOST of the Pepeunit instance, https by default
PU_MQTT_PORT18831883MQTT port for EMQX
PU_MQTT_USERNAMEYabJTlmvQ4tvweuTl0gnYabJTlmvQ4tvweuTl0gnEMQX admin username
PU_MQTT_PASSWORDF2qvym9lxL0DK6DlhmN1HgczWe9lKj30BvJTyvHuF2qvym9lxL0DK6DlhmN1HgczWe9lKj30BvJTyvHuEMQX admin password
GF_USERadminadminGrafana admin login
GF_PASSWORDaN4bzmwMjB0v69LPvxpLLJ7LHXTe6hlqZ703mVmBaN4bzmwMjB0v69LPvxpLLJ7LHXTe6hlqZ703mVmBGrafana admin password

Generating env/.env.service

Run the command that generates the final .env.service files in the env/.env.service directory:

bash
$> python make_env.py
2025-09-16 00:51:44,693 - INFO - Run make envs
2025-09-16 00:51:44,693 - INFO - Run search .env.local or .env.global
2025-09-16 00:51:44,693 - INFO - The .env.global file was found
2025-09-16 00:51:44,693 - INFO - Load variables from .env.global
2025-09-16 00:51:44,694 - INFO - Generate .env.emqx
2025-09-16 00:51:44,694 - INFO - Save env/.env.emqx
2025-09-16 00:51:44,694 - INFO - Generate .env.postgres
2025-09-16 00:51:44,694 - INFO - Save env/.env.postgres
2025-09-16 00:51:44,694 - INFO - Generate .env.frontend
2025-09-16 00:51:44,694 - INFO - Save env/.env.frontend
2025-09-16 00:51:44,694 - INFO - Generate .env.backend
2025-09-16 00:51:44,694 - INFO - Existing .env.backend found, loading sensitive keys
2025-09-16 00:51:44,695 - INFO - Load variables from env/.env.backend
2025-09-16 00:51:44,695 - INFO - Save env/.env.backend
2025-09-16 00:51:44,695 - INFO - Generate .env.grafana
2025-09-16 00:51:44,695 - INFO - Save env/.env.grafana
2025-09-16 00:51:44,695 - INFO - Generate .env.clickhouse
2025-09-16 00:51:44,695 - INFO - Save env/.env.clickhouse
2025-09-16 00:51:44,696 - INFO - Generate .env.backend_data_pipe
2025-09-16 00:51:44,696 - INFO - Existing .env.backend found, loading sensitive keys
2025-09-16 00:51:44,696 - INFO - Load variables from env/.env.backend
2025-09-16 00:51:44,696 - INFO - Save env/.env.backend_data_pipe
2025-09-16 00:51:44,696 - INFO - Generate grafana.ini
2025-09-16 00:51:44,696 - INFO - Environment file generation is complete

DANGER

The python3 make_env.py command re-generates configs every time. When re-generating, existing secret 32‑byte keys are not changed. Use the backup creation commands to preserve the old configuration.

DANGER

You may need to adjust configuration of individual services for fine‑tuning:

Nginx configuration

Usually, you do not need to configure Nginx inside docker compose, but if your Pepeunit instance is located behind an additional Nginx, review the Nginx configuration examples for https and reverse proxy.

Opening ports

For Pepeunit to work correctly, you need to open the following ports:

  1. 80 – for http
  2. 443 – for https
  3. 1883 – for MQTT

WARNING

If port 1883 is used by other applications, change it to another one and update:

DANGER

If you have a public Pepeunit instance with a domain, you must open the ports listed above.

First run

Configure access for configurations in the data directory:

bash
sudo chmod 777 -R data

Start Pepeunit:

bash
docker compose up -d

INFO

An example of a correct Backend startup based on .env.global. For .env.local only IP addresses and Telegram Bot pooling mode will differ:

bash
$> docker logs -f backend
Wait Ready PostgreSQL...
postgres:5432 - accepting connections
PostgreSQL available.
Wait check DB 'pepeunit'...
DB 'pepeunit' Exist.
Fix collation postgres for swap version containers
NOTICE:  version has not changed
ALTER DATABASE
Collation fixed.
Run migration...
{"asctime": "2025-12-06 11:51:13,514", "levelname": "INFO", "name": "alembic.runtime.migration", "message": "Context impl PostgresqlImpl."}
{"asctime": "2025-12-06 11:51:13,514", "levelname": "INFO", "name": "alembic.runtime.migration", "message": "Will assume transactional DDL."}
Del old lock files
{"time": "2025-12-06 11:51:16,012", "level": "INFO", "logger": "gunicorn.error", "message": "Starting gunicorn 23.0.0", "funcName": "info"}
{"time": "2025-12-06 11:51:16,013", "level": "INFO", "logger": "gunicorn.error", "message": "Listening at: http://0.0.0.0:5000 (28)", "funcName": "info"}
{"time": "2025-12-06 11:51:16,014", "level": "INFO", "logger": "gunicorn.error", "message": "Using worker: uvicorn.workers.UvicornWorker", "funcName": "info"}
{"time": "2025-12-06 11:51:16,017", "level": "INFO", "logger": "gunicorn.error", "message": "Booting worker with pid: 29", "funcName": "info"}
{"time": "2025-12-06 11:51:16,088", "level": "INFO", "logger": "gunicorn.error", "message": "Booting worker with pid: 30", "funcName": "info"}
{"time": "2025-12-06 11:51:22,533", "level": "INFO", "logger": "uvicorn.error", "message": "on_connect handler accepted", "funcName": "on_connect"}
{"time": "2025-12-06 11:51:22,533", "level": "INFO", "logger": "uvicorn.error", "message": "on_message handler accepted", "funcName": "on_message"}
{"time": "2025-12-06 11:51:22,534", "level": "INFO", "logger": "uvicorn.error", "message": "on_connect handler accepted", "funcName": "on_connect"}
{"time": "2025-12-06 11:51:22,534", "level": "INFO", "logger": "uvicorn.error", "message": "on_message handler accepted", "funcName": "on_message"}
{"time": "2025-12-06 11:51:22,827", "level": "INFO", "logger": "uvicorn.error", "message": "Started server process [29]", "funcName": "_serve", "color_message": "Started server process [\u001b[36m%d\u001b[0m]"}
{"time": "2025-12-06 11:51:22,827", "level": "INFO", "logger": "uvicorn.error", "message": "Waiting for application startup.", "funcName": "startup"}
{"time": "2025-12-06 11:51:22,844", "level": "INFO", "logger": "uvicorn.error", "message": "Started server process [30]", "funcName": "_serve", "color_message": "Started server process [\u001b[36m%d\u001b[0m]"}
{"time": "2025-12-06 11:51:22,844", "level": "INFO", "logger": "uvicorn.error", "message": "Waiting for application startup.", "funcName": "startup"}
{"time": "2025-12-06 11:51:22,856", "level": "INFO", "logger": "root", "message": "Total migrations to apply: 0", "funcName": "apply_migration"}
{"time": "2025-12-06 11:51:22,856", "level": "INFO", "logger": "root", "message": "Check state EMQX Broker https://dcemqx.pepemoss.com", "funcName": "__init__"}
{"time": "2025-12-06 11:51:23,650", "level": "INFO", "logger": "httpx", "message": "HTTP Request: GET https://dcemqx.pepemoss.com/api-docs/swagger.json \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:25,405", "level": "INFO", "logger": "root", "message": "EMQX Broker https://dcemqx.pepemoss.com - Ready to work", "funcName": "__init__"}
{"time": "2025-12-06 11:51:25,802", "level": "INFO", "logger": "httpx", "message": "HTTP Request: POST https://dcemqx.pepemoss.com/api/v5/login \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:25,803", "level": "INFO", "logger": "root", "message": "Del file auth hook MQTT Broker", "funcName": "delete_auth_hooks"}
{"time": "2025-12-06 11:51:26,266", "level": "INFO", "logger": "httpx", "message": "HTTP Request: DELETE https://dcemqx.pepemoss.com/api/v5/authorization/sources/file \"HTTP/1.1 204 No Content\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:26,268", "level": "INFO", "logger": "root", "message": "Del http auth hook MQTT Broker", "funcName": "delete_auth_hooks"}
{"time": "2025-12-06 11:51:26,679", "level": "INFO", "logger": "httpx", "message": "HTTP Request: DELETE https://dcemqx.pepemoss.com/api/v5/authorization/sources/http \"HTTP/1.1 204 No Content\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:26,681", "level": "INFO", "logger": "root", "message": "Del redis auth hook MQTT Broker", "funcName": "delete_auth_hooks"}
{"time": "2025-12-06 11:51:27,092", "level": "INFO", "logger": "httpx", "message": "HTTP Request: DELETE https://dcemqx.pepemoss.com/api/v5/authorization/sources/redis \"HTTP/1.1 204 No Content\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:27,095", "level": "INFO", "logger": "root", "message": "Set ACL file auth hook MQTT Broker", "funcName": "set_file_auth_hook"}
{"time": "2025-12-06 11:51:27,487", "level": "INFO", "logger": "httpx", "message": "HTTP Request: POST https://dcemqx.pepemoss.com/api/v5/authorization/sources \"HTTP/1.1 204 No Content\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:27,490", "level": "INFO", "logger": "root", "message": "Set http auth hook MQTT Broker", "funcName": "set_http_auth_hook"}
{"time": "2025-12-06 11:51:28,282", "level": "INFO", "logger": "httpx", "message": "HTTP Request: POST https://dcemqx.pepemoss.com/api/v5/authorization/sources \"HTTP/1.1 204 No Content\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:28,284", "level": "INFO", "logger": "root", "message": "Set redis auth hook MQTT Broker", "funcName": "set_redis_auth_hook"}
{"time": "2025-12-06 11:51:28,801", "level": "INFO", "logger": "httpx", "message": "HTTP Request: POST https://dcemqx.pepemoss.com/api/v5/authorization/sources \"HTTP/1.1 204 No Content\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:28,803", "level": "INFO", "logger": "root", "message": "Set cache settings auth hook MQTT Broker", "funcName": "set_auth_cache_ttl"}
{"time": "2025-12-06 11:51:29,286", "level": "INFO", "logger": "httpx", "message": "HTTP Request: PUT https://dcemqx.pepemoss.com/api/v5/authorization/settings \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:29,288", "level": "INFO", "logger": "root", "message": "Set settings for tcp listener", "funcName": "set_tcp_listener_settings"}
{"time": "2025-12-06 11:51:29,710", "level": "INFO", "logger": "httpx", "message": "HTTP Request: PUT https://dcemqx.pepemoss.com/api/v5/listeners/tcp:default \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:29,712", "level": "INFO", "logger": "root", "message": "Set global mqtt settings", "funcName": "set_global_mqtt_settings"}
{"time": "2025-12-06 11:51:30,112", "level": "INFO", "logger": "httpx", "message": "HTTP Request: PUT https://dcemqx.pepemoss.com/api/v5/configs/global_zone \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:30,114", "level": "INFO", "logger": "root", "message": "Disable retainer", "funcName": "set_global_mqtt_settings"}
{"time": "2025-12-06 11:51:30,257", "level": "INFO", "logger": "httpx", "message": "HTTP Request: PUT https://dcemqx.pepemoss.com/api/v5/mqtt/retainer \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:30,264", "level": "INFO", "logger": "root", "message": "Run sync all repository in RepositoryRegistry", "funcName": "sync_local_repository_storage"}
{"time": "2025-12-06 11:51:30,331", "level": "INFO", "logger": "root", "message": "End sync all repository in RepositoryRegistry", "funcName": "sync_local_repository_storage"}


                             ........:
                            :......::-
                             .....::.:
                             .....::.-
                        :.........::.-****
                      :........::::--==++###%
                    :::-++*++=======++*#@@%%-::
                  ....:-=---==++******###%%*-...:
                :....:--===++=*=+**%@@%@%%%%*+:..:
              :....::=====+====+==*##%@%%%#*##+=...:
            -...........:-===++*=++**#%*:....::---...:
           :......-..:*##%%*+++===+#*-.:-..+#%%%#*+:...:
         :......=..++*##%%%%#**++--=..-.--*##%%*%@#+=....-
        :..:-:.+-+=+#%%***+#@#*=---..+:=**+%@%%*@@*+=-...:
       =--===-.%%=#*#%-.=:+@@%**==-::%%#==#@@#%@@+@%**+=:::
       =-=====.+@%%+@@%:.-#@@##***+-:%@%*%@%@%%=%@@#++::-::
       =--====-.#@@*@+@@*@@@#***#**#-:%@*%%+@=@*@@#*==-..::
       ==+*=+++=::#@@@@@@@########*#%==+%@@@@@@@#**++++*+::-
       *@*+++++++++****#####%##%###%#%%%%#########*##+==*#--
       -*@%**==+=+*#=+**###%%%%####%%##%%%%%%%#%%%#####@@%=+
    :::-=--+%%@*##*.-.*#####%%%########%%%%%%%###%%@@%*++=+****
    ....:---------==***#%%%%%*%#%%#%%%%%%%%%#*++=====+**#######%
    ....:-:::-=====-----------------------===+++***+++*#%@@%###%
    :--=:=++++====---::::---------===========++*##%@@@@@@@@@@@@@
       =--+*++++***+=+*****###########%%%%%%%%%#%%%#@@@@@@@@
         +==++++**+...:*=+=*=+==*++##++=+++#%-:-*%#%@@@@@@
            %#*****#####*##*-+-=*==#--+#**%*%%%%@@@@@@@
              *##%@@@@@@@%%%%%%%%%%%%%%@@@@@@@@@@@@@@
              *###%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
                  %%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
               _____                            _ _
              |  __ \                          (_) |
              | |__) |__ _ __   ___ _   _ _ __  _| |_
              |  ___/ _ \ '_ \ / _ \ | | | '_ \| | __|
              | |  |  __/ |_) |  __/ |_| | | | | | |_
              |_|   \___| .__/ \___|\__,_|_| |_|_|\__|
                        | |
                        |_|

       v1.0.0 - AGPL v3 License
       Federated IoT Platform
       Front: https://dcunit.pepeunit.com
       REST:  https://dcunit.pepeunit.com/pepeunit/docs
       GQL:   https://dcunit.pepeunit.com/pepeunit/graphql
       TG:    https://t.me/PepeUnitDevRobot
       Docs:  https://pepeunit.com

{"time": "2025-12-06 11:51:30,333", "level": "INFO", "logger": "root", "message": "Delete webhook before set new webhook", "funcName": "run_webhook_bot"}
{"time": "2025-12-06 11:51:30,333", "level": "INFO", "logger": "root", "message": "Connect to mqtt server: dcemqx.pepemoss.com:1885", "funcName": "run_mqtt_client"}
{"time": "2025-12-06 11:51:30,334", "level": "INFO", "logger": "uvicorn.error", "message": "Used broker version is 5", "funcName": "connection"}
{"time": "2025-12-06 11:51:30,335", "level": "INFO", "logger": "uvicorn.error", "message": "Application startup complete.", "funcName": "startup"}
{"time": "2025-12-06 11:51:30,343", "level": "INFO", "logger": "root", "message": "NoAccessError('403: 1: No Access: Token is invalid')", "funcName": "get_mqtt_auth"}
{"time": "2025-12-06 11:51:30,349", "level": "INFO", "logger": "uvicorn.access", "message": "GET /pepeunit/metrics 200", "funcName": "send", "client_ip": "172.20.1.12", "http_method": "GET", "http_path": "/pepeunit/metrics", "http_version": "1.1", "http_status_code": 200}
{"time": "2025-12-06 11:51:30,445", "level": "INFO", "logger": "gmqtt.mqtt.protocol", "message": "[CONNECTION MADE]", "funcName": "connection_made"}
{"time": "2025-12-06 11:51:30,724", "level": "INFO", "logger": "httpx", "message": "HTTP Request: GET https://dcunit.pepeunit.com/grafana/api/dashboards/uid/all-docker-logs/permissions \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:30,850", "level": "INFO", "logger": "root", "message": "Connect to mqtt server: dcemqx.pepemoss.com:1885", "funcName": "run_mqtt_client"}
{"time": "2025-12-06 11:51:30,850", "level": "INFO", "logger": "uvicorn.error", "message": "Used broker version is 5", "funcName": "connection"}
{"time": "2025-12-06 11:51:30,855", "level": "INFO", "logger": "uvicorn.error", "message": "Application startup complete.", "funcName": "startup"}
{"time": "2025-12-06 11:51:30,978", "level": "INFO", "logger": "gmqtt.mqtt.protocol", "message": "[CONNECTION MADE]", "funcName": "connection_made"}
{"time": "2025-12-06 11:51:31,193", "level": "INFO", "logger": "httpx", "message": "HTTP Request: POST https://dcunit.pepeunit.com/grafana/api/dashboards/uid/all-docker-logs/permissions \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:31,631", "level": "INFO", "logger": "httpx", "message": "HTTP Request: GET https://dcunit.pepeunit.com/grafana/api/dashboards/uid/backend-aggregated-logs/permissions \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:32,072", "level": "INFO", "logger": "httpx", "message": "HTTP Request: POST https://dcunit.pepeunit.com/grafana/api/dashboards/uid/backend-aggregated-logs/permissions \"HTTP/1.1 200 OK\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:32,553", "level": "INFO", "logger": "root", "message": "MQTT subscriptions initialized in this worker", "funcName": "connect"}
{"time": "2025-12-06 11:51:32,554", "level": "INFO", "logger": "gmqtt.mqtt.package", "message": "[SEND SUB] 1 [b'dcunit.pepeunit.com/+/+/+/pepeunit']", "funcName": "build_package"}
{"time": "2025-12-06 11:51:33,087", "level": "INFO", "logger": "root", "message": "Another worker already subscribed to MQTT topics", "funcName": "connect"}
{"time": "2025-12-06 11:51:33,108", "level": "INFO", "logger": "uvicorn.access", "message": "POST /pepeunit/api/v1/bot 422", "funcName": "send", "client_ip": "172.20.1.10", "http_method": "POST", "http_path": "/pepeunit/api/v1/bot", "http_version": "1.0", "http_status_code": 422}
{"time": "2025-12-06 11:51:33,113", "level": "INFO", "logger": "uvicorn.access", "message": "POST /pepeunit/api/v1/units/auth 200", "funcName": "send", "client_ip": "172.20.1.10", "http_method": "POST", "http_path": "/pepeunit/api/v1/units/auth", "http_version": "1.0", "http_status_code": 200}
{"time": "2025-12-06 11:51:33,172", "level": "INFO", "logger": "httpx", "message": "HTTP Request: POST https://dcunit.pepeunit.com/pepeunit/api/v1/bot \"HTTP/1.1 422 Unprocessable Content\"", "funcName": "_send_single_request"}
{"time": "2025-12-06 11:51:33,179", "level": "INFO", "logger": "uvicorn.access", "message": "GET /pepeunit/metrics 200", "funcName": "send", "client_ip": "172.20.1.12", "http_method": "GET", "http_path": "/pepeunit/metrics", "http_version": "1.1", "http_status_code": 200}
{"time": "2025-12-06 11:51:33,223", "level": "INFO", "logger": "gmqtt.client", "message": "[SUBACK] 1 (0,)", "funcName": "_handle_suback_packet"}
{"time": "2025-12-06 11:51:33,249", "level": "INFO", "logger": "root", "message": "Success set TG bot webhook url", "funcName": "run_webhook_bot"}

WARNING

Pay special attention to the following line:

json
{"time": "2025-12-06 11:51:33,223", "level": "INFO", "logger": "gmqtt.client", "message": "[SUBACK] 1 (0,)", "funcName": "_handle_suback_packet"}

It shows whether the Backend managed to subscribe to the dcunit.pepeunit.com/+/+/+/pepeunit topic. If the value in parentheses is (135,) instead of (0,), then the Backend failed to subscribe to the main topic. Usually this indicates one of the following configuration errors:

  1. Port 1883 is closed on the router, hosting provider, or system firewall
  2. While configuring EMQX ports in docker-compose.yml, a different port was specified but not opened
  3. When using a custom MQTT port, you must set it in two .env files: backend and datapipe
  4. Errors in EMQX and Backend configuration, such as PU_MQTT_REDIS_AUTH_URL or PU_REDIS_URL. See Backend env variables for details. These variables must point to the same Redis instance, because Redis is responsible for initial authorization.

Creating an Administrator

The first user created on a Pepeunit instance automatically becomes an Administrator. To do this, just go through the standard registration form.