Repository: Kyrela/FixTweetBot
Pull request #43: Improvement: Added Dockerfile and docker-compose
Author: login: shindouj
Labels: name: enhancement
description: New feature or request
Created: 2025-05-09T19:48:16Z | Merged: 2025-05-11T14:39:07Z

Supersedes #30.

I have taken the liberty of taking over the stale pull request with pending review comments from the other person, while making my own improvements over it:

1. Uses Alpine Linux w/ Python 3.13
2. Config does not need to be repeated in override.config.yml - environmental variables are added to the config by the entrypoint script
3. No magic values appended to random Python files
4. Uses the bare minimum dependencies (AFAIK)

I have also skipped implementing CI/CD in this PR - I feel like this is a much more opinionated field (e.g. when to run the pipeline, how to tag images, where to store them etc.). I am happy to help you create one in the future if you wish, or you can tackle the issue yourself.


Diff:
diff --git a/.gitignore b/.gitignore
index b477413..0baefba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ venv/
 __pycache__/
 *.pyc
 core
+docker-compose.yml
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..92abfaf
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
+RUN python -m venv /usr/local/venv && . /usr/local/venv/bin/activate
+RUN /usr/local/venv/bin/pip install --no-cache-dir -r requirements.txt && \
+    /usr/local/venv/bin/pip install --no-cache-dir cryptography
+
+# Step 2: Final image
+
+FROM python:3.13-alpine
+WORKDIR /usr/local/app
+
+ENV USR=app
+ENV GRP=$USR
+ENV UID=1000
+ENV GID=1000
+
+ENV PATH="/usr/local/venv/bin:$PATH" 
+ENV DISCORE_CONFIG=/usr/local/app/docker.config.yml
+ENV DATABASE_DRIVER="mysql"
+
+# needed for console logging
+ENV PYTHONUNBUFFERED=1 
+ENV PYTHONDONTWRITEBYTECODE=1
+
+RUN apk add --no-cache netcat-openbsd
+
+RUN addgroup \
+    --gid "$GID" \
+    $GRP \
+    && \
+    adduser \
+    --disabled-password \
+    --no-create-home \
+    --home "$(pwd)" \
+    --uid "$UID" \
+    --ingroup "$GRP" \
+    $USR
+
+COPY --from=builder /usr/local/venv /usr/local/venv
+COPY --from=builder /usr/local/app /usr/local/app
+
+RUN chown -R $USR:$GRP .
+
+COPY docker-entrypoint.sh /
+RUN chmod +x /docker-entrypoint.sh
+
+USER $USR
+
+ENTRYPOINT ["/docker-entrypoint.sh"]
+CMD ["python", "main.py"]
diff --git a/README.md b/README.md
index 0887041..8a0e420 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,44 @@ If you need help, you can join the [support server](https://discord.gg/3ej9JrkF3
 
 ## Self-hosting
 
+### Docker
+
+First, clone this repository and build your image:
+```
+git clone https://github.com/Kyrela/FixTweetBot.git
+cd FixTweetBot
+docker build -t fixtweetbot .
+```
+
+Copy the example docker-compose and fill it with your bot token. Change the default passwords to random strings.
+You do not need to repeat the same configuration in `override.config.yml` - but you can still create one if you want. To use it, uncomment the proper lines
+in your docker-compose:
+
+```yaml
+    # uncomment and create file if you want to override any default settings
+    # volumes:
+    #    - ./override.config.yml:/usr/local/app/override.config.yml:ro
+```
+
+Then simply run `docker compose up -d`.
+
+#### Available environment variables
+
+| Environmental variable | Comment                                                                         |
+| ---------------------- | ------------------------------------------------------------------------------- |
+| UID                    | User ID used inside the container. Default value: 1000                          |
+| GID                    | Group ID used inside the container. Default value: 1000                         |
+| DATABASE_HOST          | Database hostname. Unset by default.                                            |
+| DATABASE_PORT          | Database port. Unset by default.                                                |
+| DATABASE_NAME          | Database name. Unset by default.                                                |
+| DATABASE_USER          | Database user. Unset by default.                                                |
+| DATABASE_PASSWORD      | Database password. Unset by default.                                            |
+| DATABASE_DRIVER        | Database driver. Default value: "mysql"                                         |
+| DISCORD_TOKEN          | Your bot's token.                                                               |
+| DEV_GUILD              | _Optional_: Server ID that you want to use for controlling your bot's instance. |
+
+### Bare metal
+
 Simply install Python >= 3.10, clone the repository, and run `pip install -r requirements.txt`.
 
 Be sure to have a database set up using MySQL.
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
new file mode 100644
index 0000000..2ca0035
--- /dev/null
+++ b/docker-compose.example.yml
@@ -0,0 +1,50 @@
+services:
+  bot:
+    image: fixtweetbot:latest
+    restart: unless-stopped
+    depends_on:
+      - db
+    # You do NOT need to repeat these configuration values in the override file. They are automatically supplied to the bot.
+    environment:
+      # Should be equal to the name of the database container.
+      - DATABASE_HOST=db
+      - DATABASE_PORT=3306
+      # Should be equal to MYSQL_DATABASE.
+      - DATABASE_NAME=fixtweetbot
+      # Should be equal to MYSQL_USER.
+      - DATABASE_USER=fixtweetbot
+      # Should be equal to MYSQL_PASSWORD.
+      - DATABASE_PASSWORD=changeme123
+      # Your Discord bot token.
+      - DISCORD_TOKEN=your_discord_bot_token
+      # Optional: Server ID that you want to use for controlling your bot's instance.
+      # - DEV_GUILD=your_discord_dev_guild_id
+    # uncomment and create file if you want to override any default settings
+    # volumes:
+    #    - ./override.config.yml:/usr/local/app/override.config.yml:ro
+
+  db:
+    image: mysql:8.0
+    restart: unless-stopped
+    environment:
+      - MYSQL_DATABASE=fixtweetbot
+      - MYSQL_USER=fixtweetbot
+      # Generate a random password for this field.
+      - MYSQL_PASSWORD=changeme123
+      # Generate a random password for this field. Do not use the same password!
+      - MYSQL_ROOT_PASSWORD=changeme234
+      - TZ=UTC
+    volumes:
+      - mysql_data:/var/lib/mysql
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$MYSQL_ROOT_PASSWORD"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 30s
+    ports:
+      - "3306:3306"
+
+volumes:
+  mysql_data:
+    driver: local
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
new file mode 100644
index 0000000..6ce0133
--- /dev/null
+++ b/docker-entrypoint.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+set -e
+
+__config="
+database:
+  host: $DATABASE_HOST
+  port: $DATABASE_PORT
+  user: $DATABASE_USER
+  driver: $DATABASE_DRIVER
+  password: $DATABASE_PASSWORD
+  database: $DATABASE_NAME
+
+token: $DISCORD_TOKEN"
+
+if [ -n "$DEV_GUILD" ]; then
+  __config="
+$__config
+dev_guild: $DEV_GUILD"
+fi
+
+echo "$__config" > /usr/local/app/docker.config.yml
+
+echo -n "Waiting for database.."
+while ! nc -z $DATABASE_HOST $DATABASE_PORT 2>/dev/null; do
+    echo -n "."
+    sleep 1
+done
+
+
+echo -e \\n"Database ready"
+
+masonite-orm migrate -C database/config.py -d database/migrations || echo "Migration failed but continuing..."
+
+exec "$@"


Reviews:
user: login: Gratenes
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T20:57:32Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T21:01:26Z
user: login: Kyrela
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T21:51:15Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T22:29:39Z
user: login: Kyrela
body: First of all, thank you very much for your contribution, great work! Especially your use of `DISCORE_CONFIG` that isn't documented anywhere, it's really cool.

I haven't tested it yet, but I have a few comments/questions, especially regarding the dependencies.
I'm not really a Docker user myself, although I have used it a few times in the past in professional environments, so please be understanding if I make a few mistakes or say stupid things, and don't hesitate to correct me or argue with me.


state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-10T21:33:46Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T22:08:04Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T22:11:25Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T22:12:36Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T22:14:43Z
user: login: Kyrela
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T08:33:46Z
user: login: Kyrela
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T08:37:13Z
user: login: Kyrela
body: After testing by follwoing the README instruction, I figured out a couple things that are dependent from the environement that should probably be changed.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-11T13:35:57Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T13:48:02Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T13:49:03Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T13:51:37Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T13:51:42Z
user: login: Kyrela
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T14:04:41Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T14:08:35Z
user: login: Kyrela
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T14:25:10Z
user: login: shindouj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T14:34:39Z
user: login: Kyrela
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T14:37:21Z
user: login: Kyrela
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-11T14:38:48Z

Review comments:
path: README.md
user: login: Gratenes
body: Should add this back, if its still optional
`# optional, for dev commands`
created_at: 2025-05-09T20:57:32Z
author_association: NONE
path: README.md
user: login: shindouj
body: As far as I understand - it is not. The bot does not run without that config field being set with the following stack trace:
```
bot-1  | Traceback (most recent call last):
bot-1  |   File "/usr/local/app/main.py", line 13, in <module>
bot-1  |     bot.run()
bot-1  |   File "/usr/local/venv/lib/python3.11/site-packages/discore/bot.py", line 136, in run
bot-1  |     super().run(
bot-1  |   File "/usr/local/venv/lib/python3.11/site-packages/discord/client.py", line 906, in run
bot-1  |     asyncio.run(runner())
bot-1  |   File "/usr/local/lib/python3.11/asyncio/runners.py", line 190, in run
bot-1  |     return runner.run(main)
bot-1  |            ^^^^^^^^^^^^^^^^
bot-1  |   File "/usr/local/lib/python3.11/asyncio/runners.py", line 118, in run
bot-1  |     return self._loop.run_until_complete(task)
bot-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
bot-1  |   File "/usr/local/lib/python3.11/asyncio/base_events.py", line 654, in run_until_complete
bot-1  |     return future.result()
bot-1  |            ^^^^^^^^^^^^^^^
bot-1  |   File "/usr/local/venv/lib/python3.11/site-packages/discord/client.py", line 895, in runner
bot-1  |     await self.start(token, reconnect=reconnect)
bot-1  |   File "/usr/local/venv/lib/python3.11/site-packages/discore/bot.py", line 143, in start
bot-1  |     await self._load_cogs()
bot-1  |   File "/usr/local/venv/lib/python3.11/site-packages/discore/bot.py", line 106, in _load_cogs
bot-1  |     await self.add_cog(new_cog)
bot-1  |   File "/usr/local/venv/lib/python3.11/site-packages/discord/ext/commands/bot.py", line 799, in add_cog
bot-1  |     cog = await cog._inject(self, override=override, guild=guild, guilds=guilds)
bot-1  |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
bot-1  |   File "/usr/local/venv/lib/python3.11/site-packages/discord/ext/commands/cog.py", line 745, in _inject
bot-1  |     bot.tree.add_command(command, override=override, guild=guild, guilds=guilds)
bot-1  |   File "/usr/local/venv/lib/python3.11/site-packages/discord/app_commands/tree.py", line 391, in add_command
bot-1  |     commands = self._guild_commands.get(guild_id, {})
bot-1  |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
bot-1  | TypeError: unhashable type: 'Dict'
```
I struggled with my Dockerfile for quite a while until I found out that setting it to an actual value fixes everything. If it is a regression, it should probably be handled in a separate ticket.
created_at: 2025-05-09T21:01:25Z
author_association: CONTRIBUTOR
path: README.md
user: login: Kyrela
body: > I struggled with my Dockerfile for quite a while until I found out that setting it to an actual value fixes everything.

Good catch. I wonder if it ever actually worked. Anyway, the latest commit fixes that.

I'm just patching this quickly, I'll look at this PR in details when I have more time, but thank you for work!
created_at: 2025-05-09T21:51:15Z
author_association: OWNER
path: README.md
user: login: shindouj
body: Thanks for your quick reaction! I have amended my documentation and made the env variable optional as well.
created_at: 2025-05-09T22:29:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
+RUN python -m venv /usr/local/venv && . /usr/local/venv/bin/activate
+RUN /usr/local/venv/bin/pip install --no-cache-dir -r requirements.txt && \
+    /usr/local/venv/bin/pip install --no-cache-dir cryptography
path: Dockerfile
user: login: Kyrela
body: Is `cryptography` necessary?
created_at: 2025-05-10T20:44:11Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
path: Dockerfile
user: login: Kyrela
body: Are all of these necessary? I don't see anything linux-related or rust-related.
created_at: 2025-05-10T20:46:45Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
+RUN python -m venv /usr/local/venv && . /usr/local/venv/bin/activate
+RUN /usr/local/venv/bin/pip install --no-cache-dir -r requirements.txt && \
+    /usr/local/venv/bin/pip install --no-cache-dir cryptography
+
+# Step 2: Final image
+
+FROM python:3.13-alpine
+WORKDIR /usr/local/app
+
+ENV USR=app
+ENV GRP=$USR
+ENV UID=1000
+ENV GID=1000
+
+ENV PATH="/usr/local/venv/bin:$PATH" 
+ENV DISCORE_CONFIG=/usr/local/app/docker.config.yml
+ENV DATABASE_DRIVER="mysql"
+
+# needed for console logging
+ENV PYTHONUNBUFFERED=1 
+ENV PYTHONDONTWRITEBYTECODE=1
+
+RUN apk add --no-cache netcat-openbsd bash
path: Dockerfile
user: login: Kyrela
body: I get that bash is useful (although I bevileve `sh` should be enough for the entrypoint), but what is the purpose of `netcat-openbsd`?
created_at: 2025-05-10T20:49:06Z
author_association: OWNER
diff_hunk: @@ -95,6 +95,42 @@ If you need help, you can join the [support server](https://discord.gg/3ej9JrkF3
 
 ## Self-hosting
 
+### Docker
+
+First, build your image:
+```
+docker build -f fixtweetbot .
+```
+
+Copy the example docker-compose and fill it with your bot token and dev guild. Change the default passwords to random strings.
+You do not need to repeat the same configuration in `override.config.yml` - but you can still create one if you want. To use it, uncomment the proper lines
+in your docker-compose:
+
+```yaml
+    # uncomment and create file if you want to override any default settings
+    # volumes:
+    #    - ./override.config.yml:/usr/local/app/override.config.yml:ro
+```
+
+Then simply run `docker-compose up -d`.
path: README.md
user: login: Kyrela
body: I believe `docker compose` should be used instead of `docker-compose`, to avoid using docker compose v1 on old docker installations.
created_at: 2025-05-10T20:54:09Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
+RUN python -m venv /usr/local/venv && . /usr/local/venv/bin/activate
+RUN /usr/local/venv/bin/pip install --no-cache-dir -r requirements.txt && \
+    /usr/local/venv/bin/pip install --no-cache-dir cryptography
path: Dockerfile
user: login: shindouj
body: Yes. It is a dependency of PyMySQL: https://github.com/PyMySQL/PyMySQL/issues/768
created_at: 2025-05-10T22:08:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
path: Dockerfile
user: login: shindouj
body: They are required with Python 3.13 (for now) as one of the dependencies of `masonite-orm`, `pendulum`, does not have a 3.13 wheel distribution - and `pendulum` has native modules that are compiled with Rust.

However, even if these packages proved to be not needed anymore, they do not leak to the production image anyway as they are only used in the builder image that gets immediately discarded. With Alpine, installing these packages is a negligible time overhead.
created_at: 2025-05-10T22:11:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
+RUN python -m venv /usr/local/venv && . /usr/local/venv/bin/activate
+RUN /usr/local/venv/bin/pip install --no-cache-dir -r requirements.txt && \
+    /usr/local/venv/bin/pip install --no-cache-dir cryptography
+
+# Step 2: Final image
+
+FROM python:3.13-alpine
+WORKDIR /usr/local/app
+
+ENV USR=app
+ENV GRP=$USR
+ENV UID=1000
+ENV GID=1000
+
+ENV PATH="/usr/local/venv/bin:$PATH" 
+ENV DISCORE_CONFIG=/usr/local/app/docker.config.yml
+ENV DATABASE_DRIVER="mysql"
+
+# needed for console logging
+ENV PYTHONUNBUFFERED=1 
+ENV PYTHONDONTWRITEBYTECODE=1
+
+RUN apk add --no-cache netcat-openbsd bash
path: Dockerfile
user: login: shindouj
body: It is used in the entrypoint: https://github.com/Kyrela/FixTweetBot/blob/a35774597b7886f041e39b8ffce34faf422c361b/docker-entrypoint.sh#L23-L27

As for moving to sh, I am not _that_ good in Linux scripting and I can't judge if sh will run the entirety of the script.
created_at: 2025-05-10T22:12:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +95,42 @@ If you need help, you can join the [support server](https://discord.gg/3ej9JrkF3
 
 ## Self-hosting
 
+### Docker
+
+First, build your image:
+```
+docker build -f fixtweetbot .
+```
+
+Copy the example docker-compose and fill it with your bot token and dev guild. Change the default passwords to random strings.
+You do not need to repeat the same configuration in `override.config.yml` - but you can still create one if you want. To use it, uncomment the proper lines
+in your docker-compose:
+
+```yaml
+    # uncomment and create file if you want to override any default settings
+    # volumes:
+    #    - ./override.config.yml:/usr/local/app/override.config.yml:ro
+```
+
+Then simply run `docker-compose up -d`.
path: README.md
user: login: shindouj
body: Old habits die hard. Fixed.
created_at: 2025-05-10T22:14:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
path: Dockerfile
user: login: Kyrela
body: I didn't know about that, thank you for your answer!
created_at: 2025-05-11T08:33:46Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,56 @@
+# Step 1: Builder
+
+FROM python:3.13-alpine AS builder
+WORKDIR /usr/local/app
+
+COPY ./ ./
+
+RUN apk add --no-cache git linux-headers cargo rust
+RUN python -m venv /usr/local/venv && . /usr/local/venv/bin/activate
+RUN /usr/local/venv/bin/pip install --no-cache-dir -r requirements.txt && \
+    /usr/local/venv/bin/pip install --no-cache-dir cryptography
+
+# Step 2: Final image
+
+FROM python:3.13-alpine
+WORKDIR /usr/local/app
+
+ENV USR=app
+ENV GRP=$USR
+ENV UID=1000
+ENV GID=1000
+
+ENV PATH="/usr/local/venv/bin:$PATH" 
+ENV DISCORE_CONFIG=/usr/local/app/docker.config.yml
+ENV DATABASE_DRIVER="mysql"
+
+# needed for console logging
+ENV PYTHONUNBUFFERED=1 
+ENV PYTHONDONTWRITEBYTECODE=1
+
+RUN apk add --no-cache netcat-openbsd bash
path: Dockerfile
user: login: Kyrela
body: Oh, sorry I missed it! I'll try without bash while testing it and make a feedback on it, thank for your answer!
created_at: 2025-05-11T08:37:13Z
author_association: OWNER
diff_hunk: @@ -95,6 +95,42 @@ If you need help, you can join the [support server](https://discord.gg/3ej9JrkF3
 
 ## Self-hosting
 
+### Docker
+
+First, build your image:
+```
+docker build -f fixtweetbot .
path: README.md
user: login: Kyrela
body: I believe this should be `docker build . -t fixtweetbot` instead, because there's no garantee that the directory of the repo is named `fixtweetbot` as it can be changed (and, in any cases, it's `FixTweetBot` by default, it changes everything on case-sensitive file systems), and because all of the commands should be sent from the repo itself.
created_at: 2025-05-11T10:26:20Z
author_association: OWNER
diff_hunk: @@ -95,6 +95,42 @@ If you need help, you can join the [support server](https://discord.gg/3ej9JrkF3
 
 ## Self-hosting
 
+### Docker
+
+First, build your image:
+```
+docker build -f fixtweetbot .
+```
+
+Copy the example docker-compose and fill it with your bot token and dev guild. Change the default passwords to random strings.
path: README.md
user: login: Kyrela
body: the dev guild mention here should be removed, as it's now optionnal
created_at: 2025-05-11T10:27:57Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,50 @@
+services:
+  bot:
+    image: fixtweetbot:latest
path: docker-compose.example.yml
user: login: Kyrela
body: I believe using `build` instead of `image` would simplify the process of running the bot and avoid the problem of the environment-dependant name  (for exemple, the image is named `FixTweetBot` if I don't build with `-t` in my case)
created_at: 2025-05-11T10:37:48Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+__config="
+database:
+  host: $DATABASE_HOST
+  port: $DATABASE_PORT
+  user: $DATABASE_USER
+  driver: $DATABASE_DRIVER
+  password: $DATABASE_PASSWORD
+  database: $DATABASE_NAME
+
+token: $DISCORD_TOKEN"
+
+if [ -n "$DEV_GUILD" ]; then
+  __config="
+$__config
+dev_guild: $DEV_GUILD"
+fi
+
+echo "$__config" > /usr/local/app/docker.config.yml
+
+echo -n "Waiting for database.."
+while ! nc -z $DATABASE_HOST $DATABASE_PORT 2>/dev/null; do
+    echo -n "."
+    sleep 1
+done
+
+
+echo -e \\n"Database ready"
+
+masonite-orm migrate -C database/config.py -d database/migrations || echo "Migration failed but continuing..."
+
+exec "$@"
path: docker-entrypoint.sh
user: login: Kyrela
body: `sh` suffices here after a few modifications

```suggestion
#!/bin/sh
set -e

__config=$(cat <<EOF
database:
  host: $DATABASE_HOST
  port: $DATABASE_PORT
  user: $DATABASE_USER
  driver: $DATABASE_DRIVER
  password: $DATABASE_PASSWORD
  database: $DATABASE_NAME

token: $DISCORD_TOKEN
EOF
)

if [ -n "$DEV_GUILD" ]; then
  __config="$__config
dev_guild: $DEV_GUILD"
fi

echo "$__config" > /usr/local/app/docker.config.yml

echo -n "Waiting for database.."
while ! nc -z $DATABASE_HOST $DATABASE_PORT 2>/dev/null; do
    echo -n "."
    sleep 1
done

echo -e "\nDatabase ready"

masonite-orm migrate -C database/config.py -d database/migrations || echo "Migration failed but continuing..."

exec "$@"

```
created_at: 2025-05-11T13:26:51Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,50 @@
+services:
+  bot:
+    image: fixtweetbot:latest
path: docker-compose.example.yml
user: login: shindouj
body: I am pretty opposed to using `build` seeing how it is incompatible with Swarm; additionally, I would assume this PR would be followed by adding a CI/CD pipeline publishing an image that will make the build step unnecessary anyway.
created_at: 2025-05-11T13:48:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+__config="
+database:
+  host: $DATABASE_HOST
+  port: $DATABASE_PORT
+  user: $DATABASE_USER
+  driver: $DATABASE_DRIVER
+  password: $DATABASE_PASSWORD
+  database: $DATABASE_NAME
+
+token: $DISCORD_TOKEN"
+
+if [ -n "$DEV_GUILD" ]; then
+  __config="
+$__config
+dev_guild: $DEV_GUILD"
+fi
+
+echo "$__config" > /usr/local/app/docker.config.yml
+
+echo -n "Waiting for database.."
+while ! nc -z $DATABASE_HOST $DATABASE_PORT 2>/dev/null; do
+    echo -n "."
+    sleep 1
+done
+
+
+echo -e \\n"Database ready"
+
+masonite-orm migrate -C database/config.py -d database/migrations || echo "Migration failed but continuing..."
+
+exec "$@"
path: docker-entrypoint.sh
user: login: shindouj
body: Is it going to work without `cat` ing the DEV_GUILD as well (L17-L20)? In general, this seems to be a lot of tinkering for not a lot of gain (getting rid of one, arguably small dependency). I'm personally a fan of not complicating your own life for small gains. :P
created_at: 2025-05-11T13:49:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +95,42 @@ If you need help, you can join the [support server](https://discord.gg/3ej9JrkF3
 
 ## Self-hosting
 
+### Docker
+
+First, build your image:
+```
+docker build -f fixtweetbot .
path: README.md
user: login: shindouj
body: You are correct, fixed.
created_at: 2025-05-11T13:51:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +95,42 @@ If you need help, you can join the [support server](https://discord.gg/3ej9JrkF3
 
 ## Self-hosting
 
+### Docker
+
+First, build your image:
+```
+docker build -f fixtweetbot .
+```
+
+Copy the example docker-compose and fill it with your bot token and dev guild. Change the default passwords to random strings.
path: README.md
user: login: shindouj
body: Fixed.
created_at: 2025-05-11T13:51:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+services:
+  bot:
+    image: fixtweetbot:latest
path: docker-compose.example.yml
user: login: Kyrela
body: Fair. I don't know swarm at all, but both of your comments seems relevant.

> I would assume this PR would be followed by adding a CI/CD pipeline publishing an image that will make the build step unnecessary anyway.

I've never worked with docker images pusblishing at all, but publishing it with the github releases and through docker hub would indeed be cool and remove the whole clone & build step. I'll give it a try.
created_at: 2025-05-11T14:04:41Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,50 @@
+services:
+  bot:
+    image: fixtweetbot:latest
path: docker-compose.example.yml
user: login: shindouj
body: As I mentioned in the PR's description - I am happy to help, especially since I run a pipeline for this image already for my own purposes: https://github.com/shindouj/docker-fixtweetbot

This isn't something that can be used 1:1 within this project (it's based on a modified Dockerfile and is pull-based rather than push-based), plus you will need to make some decisions yourself (such as where to publish the images, how to version them, if you want to only make release images or also snapshots etc.), but if you want to take a stab at it on your own it can serve as an inspiration.
created_at: 2025-05-11T14:08:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+__config="
+database:
+  host: $DATABASE_HOST
+  port: $DATABASE_PORT
+  user: $DATABASE_USER
+  driver: $DATABASE_DRIVER
+  password: $DATABASE_PASSWORD
+  database: $DATABASE_NAME
+
+token: $DISCORD_TOKEN"
+
+if [ -n "$DEV_GUILD" ]; then
+  __config="
+$__config
+dev_guild: $DEV_GUILD"
+fi
+
+echo "$__config" > /usr/local/app/docker.config.yml
+
+echo -n "Waiting for database.."
+while ! nc -z $DATABASE_HOST $DATABASE_PORT 2>/dev/null; do
+    echo -n "."
+    sleep 1
+done
+
+
+echo -e \\n"Database ready"
+
+masonite-orm migrate -C database/config.py -d database/migrations || echo "Migration failed but continuing..."
+
+exec "$@"
path: docker-entrypoint.sh
user: login: Kyrela
body: In fact, `sh` works perfectly without any modifications. The `cat` thing is more of a security thing than anything else. In certain environments, indentation, line jumps, etc. can cause problems, but for `DEV_GUILD` there is none of that, and there's no need for it.

*But* even the first `cat` isn't needed: as I said, it works with docker. It may not work in other environments, but it's a *docker entrypoint*, so the environment should always be the same. So you can get away with just changing `#!/bin/bash` to `#!/bin/sh`.
created_at: 2025-05-11T14:25:10Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+__config="
+database:
+  host: $DATABASE_HOST
+  port: $DATABASE_PORT
+  user: $DATABASE_USER
+  driver: $DATABASE_DRIVER
+  password: $DATABASE_PASSWORD
+  database: $DATABASE_NAME
+
+token: $DISCORD_TOKEN"
+
+if [ -n "$DEV_GUILD" ]; then
+  __config="
+$__config
+dev_guild: $DEV_GUILD"
+fi
+
+echo "$__config" > /usr/local/app/docker.config.yml
+
+echo -n "Waiting for database.."
+while ! nc -z $DATABASE_HOST $DATABASE_PORT 2>/dev/null; do
+    echo -n "."
+    sleep 1
+done
+
+
+echo -e \\n"Database ready"
+
+masonite-orm migrate -C database/config.py -d database/migrations || echo "Migration failed but continuing..."
+
+exec "$@"
path: docker-entrypoint.sh
user: login: shindouj
body: Gotcha. I have changed the shell to `/bin/sh` and removed bash.
created_at: 2025-05-11T14:34:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+services:
+  bot:
+    image: fixtweetbot:latest
path: docker-compose.example.yml
user: login: Kyrela
body: Thanks! I'll make sure to make a PR and mention you in it if I need any help.
created_at: 2025-05-11T14:37:21Z
author_association: OWNER

PR comments:
user: login: shindouj
created_at: 2025-05-10T22:17:41Z
body: Thanks for your questions - all good ones! I'm happy to answer them as they come.
author_association: CONTRIBUTOR
user: login: Kyrela
created_at: 2025-05-11T08:38:58Z
body: Thank you for your answers! I'll merge it after I've tested it, if I don't have any other question.
author_association: OWNER
user: login: Kyrela
created_at: 2025-05-11T14:39:44Z
body: Thank you for your work, your patience, and your answers! Really appreciate it.
author_association: OWNER
user: login: lkd70
created_at: 2025-05-12T19:56:45Z
body: Fantastic, thanks a lot for taking over here. Apologies for letting the former go stale, glad to see this has been implemented regardless.
author_association: NONE
user: login: Kyrela
created_at: 2025-07-22T17:01:32Z
body: recheck
author_association: OWNER
user: login: github-actions[bot]
created_at: 2025-07-22T17:01:40Z
body: All contributors have signed the CLA  ✍️ ✅<br/><sub>Posted by the ****CLA Assistant Lite bot****.</sub>
author_association: NONE
user: login: shindouj
created_at: 2025-07-22T17:08:59Z
body: I have read the CLA Document and I hereby sign the CLA
author_association: CONTRIBUTOR
user: login: Kyrela
created_at: 2025-07-22T17:14:15Z
body: recheck
author_association: OWNER
user: login: Kyrela
created_at: 2025-07-22T17:15:31Z
body: > I have read the CLA Document and I hereby sign the CLA

@shindouj Ok, I'm sorry but I had a configuration error 😓 Can you retry please?
author_association: OWNER
user: login: shindouj
created_at: 2025-07-22T17:16:43Z
body: I have read the CLA Document and I hereby sign the CLA
author_association: CONTRIBUTOR
user: login: Kyrela
created_at: 2025-07-22T17:17:16Z
body: Thanks!
author_association: OWNER