diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml
index 1b63a9d..3ee15bc 100644
--- a/.github/workflows/build-push.yml
+++ b/.github/workflows/build-push.yml
@@ -8,22 +8,39 @@ on:
     branches: [ "master" ]
 
 jobs:
-  upload-solution:
-    name: Upload Octobot to production
+  upload-container:
+    name: Upload Octobot Docker container
     runs-on: ubuntu-latest
     permissions:
-      actions: read
-      contents: read
+      packages: write
     environment: production
 
     steps:
       - name: Checkout repository
         uses: actions/checkout@v4
 
-      - name: Publish solution
-        run: dotnet publish $PUBLISH_FLAGS
+      - name: Build container
+        run: docker build --build-arg PUBLISH_OPTIONS=$PUBLISH_OPTIONS -t $IMAGE_NAME -f Dockerfile .
+        shell: bash
         env:
-          PUBLISH_FLAGS: ${{vars.PUBLISH_FLAGS}}
+          PUBLISH_OPTIONS: ${{vars.PUBLISH_OPTIONS}}
+          IMAGE_NAME: ${{vars.IMAGE_NAME}}
+
+      - name: Push container
+        run: |
+          echo $CR_PAT | docker login ghcr.io -u $REPO_OWNER --password-stdin
+          docker push ghcr.io/$NAMESPACE/$IMAGE_NAME:latest
+        shell: bash
+        env:
+          CR_PAT: ${{secrets.GITHUB_TOKEN}}
+          NAMESPACE: ${{vars.NAMESPACE}}
+          IMAGE_NAME: ${{vars.IMAGE_NAME}}
+          REPO_OWNER: ${{github.repository_owner}}
+
+  upload-solution:
+    name: Upload Octobot to production
+    runs-on: ubuntu-latest
+    environment: production
 
       - name: Setup SSH key
         run: |
@@ -44,15 +61,15 @@ jobs:
           SSH_HOST: ${{secrets.SSH_HOST}}
           STOP_COMMAND: ${{vars.STOP_COMMAND}}
 
-      - name: Upload published solution
+      - name: Update Docker image
         run: |
-          scp -r $UPLOAD_FROM $SSH_USER@$SSH_HOST:$UPLOAD_TO
+          ssh $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest
         shell: bash
         env:
           SSH_USER: ${{secrets.SSH_USER}}
           SSH_HOST: ${{secrets.SSH_HOST}}
-          UPLOAD_FROM: ${{vars.UPLOAD_FROM}}
-          UPLOAD_TO: ${{vars.UPLOAD_TO}}
+          NAMESPACE: ${{vars.NAMESPACE}}
+          IMAGE_NAME: ${{vars.IMAGE_NAME}}
 
       - name: Start new instance
         run: |
diff --git a/.gitignore b/.gitignore
index f97f6b8..fcda727 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ riderModule.iml
 /.vs/
 GuildData/
 Logs/
+compose.yaml
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0ef831a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,15 @@
+FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build-env
+WORKDIR /Octobot
+
+# Copy everything
+COPY . ./
+# Load build argument with publish options
+ARG PUBLISH_OPTIONS="-c Release"
+# Build and publish a release
+RUN dotnet publish ./TeamOctolings.Octobot $PUBLISH_OPTIONS -o out
+
+# Build runtime image
+FROM mcr.microsoft.com/dotnet/runtime:8.0@sha256:a335dccd3231f7f9e2122691b21c634f96e187d3840c8b7dbad61ee09500e408
+WORKDIR /Octobot
+COPY --from=build-env /Octobot/out .
+ENTRYPOINT ["./TeamOctolings.Octobot"]
diff --git a/compose.example.yaml b/compose.example.yaml
new file mode 100644
index 0000000..522281f
--- /dev/null
+++ b/compose.example.yaml
@@ -0,0 +1,17 @@
+services:
+  octobot:
+    container_name: octobot
+    build:
+      context: .
+      args:
+        - PUBLISH_OPTIONS
+    environment:
+      - BOT_TOKEN
+    volumes:
+      - guild-data:/Octobot/GuildData
+      - logs:/Octobot/Logs
+    restart: unless-stopped
+
+volumes:
+  guild-data:
+  logs: