diff --git a/Dockerfile b/Dockerfile
index 6ef9f83..d24c47d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,41 +1,10 @@
-FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf 
-FROM python:3.12.8-alpine3.21
+FROM python:3.14.0a6-bookworm
+RUN mkdir -p /code 
 WORKDIR /code
-ENV FLASK_APP=app.py
-ENV FLASK_RUN_HOST=0.0.0.0
-RUN apk add --no-cache gcc musl-dev linux-headers git
-
-#WKHTMLTOPDFNEEDS
-RUN apk add --no-cache \
-    libstdc++ \
-    libx11 \
-    libxrender \
-    libxext \
-    libssl3 \
-    ca-certificates \
-    docker \
-    fontconfig \
-    freetype \
-    ttf-dejavu \
-    ttf-droid \
-    ttf-freefont \
-    ttf-liberation \
-    # more fonts
-  && apk add --no-cache --virtual .build-deps \
-    msttcorefonts-installer \
-  # Install microsoft fonts
-  && update-ms-fonts \
-  && fc-cache -f \
-  # Clean up when done
-  && rm -rf /tmp/* \
-  && apk del .build-deps
-COPY --from=wkhtmltopdf /bin/wkhtmltopdf /bin/wkhtmltopdf
-COPY --from=wkhtmltopdf /bin/wkhtmltoimage /bin/wkhtmltoimage
+RUN apt update && apt install build-essential docker -y     
 COPY pyproject.toml pyproject.toml 
 COPY src src
+RUN mkdir /drive
 RUN pip install --upgrade pip
 RUN pip install -e .
-EXPOSE 8081
 
-CMD ["python","-m","snek.app"]
-#CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]
diff --git a/compose.yml b/compose.yml
index 70d21ba..9d55d3b 100644
--- a/compose.yml
+++ b/compose.yml
@@ -7,26 +7,12 @@ services:
       - "8081:8081"
     volumes:
       - ./:/code
-      - /media/storage/snek/molodetz.nl/drive:/code/drive
+      - /media/storage/snek.molodetz.nl/drive:/code/drive
+      - /media/storage/snek.molodetz.nl/drive:/drive
       - /var/run/docker.sock:/var/run/docker.sock
-      - /media/storage/snek/molodetz.nl/drive:/drive
     environment:
      - PYTHONDONTWRITEBYTECODE=1
      - PYTHONUNBUFFERED=1
     entrypoint: ["gunicorn", "-w", "1", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]
     #entrypoint: ["python","-m","snek.app"]
-  snecssh:
-    build:
-      context: .
-      dockerfile: DockerfileDrive
-    restart: always
-    ports:
-      - "2225:2225"
-    volumes:
-      - ./:/code
-    environment:
-     - PYTHONDONTWRITEBYTECODE="1"
-     - PYTHONUNBUFFERED="1"
-    entrypoint: ["python","-m","snekssh.app2"]
-    #["python","-m","snek.app"]
    
diff --git a/src/snek/scripts/chat.js b/src/snek/scripts/chat.js
new file mode 100644
index 0000000..a4d6782
--- /dev/null
+++ b/src/snek/scripts/chat.js
@@ -0,0 +1,54 @@
+const channelUid = "{{ channel.uid.value }}";
+
+function initInputField(textBox) {
+    textBox.addEventListener('change', (e) => {
+        e.preventDefault();
+        this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
+    });
+
+    textBox.addEventListener('keydown', (e) => {
+        if (e.key === 'Enter' && !e.shiftKey) {
+            e.preventDefault();
+            const message = e.target.value.trim();
+            if (message) {
+                app.rpc.sendMessage(channelUid, message);
+                e.target.value = '';
+            }
+        }
+    });
+    textBox.focus();
+}
+
+function updateTimes() {
+    document.querySelectorAll(".time").forEach((time) => {
+        time.innerText = app.timeDescription(time.dataset.created_at);
+    });
+}
+
+function isElementVisible(element) {
+    const rect = element.getBoundingClientRect();
+    return (
+        rect.top >= 0 &&
+        rect.left >= 0 &&
+        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+        rect.right <= (window.innerWidth || document.documentElement.clientWidth)
+    );
+}
+
+const messagesContainer = document.querySelector(".chat-messages");
+
+let isLoadingExtra = false;
+
+messagesContainer.addEventListener("scroll", () => {
+    loadExtra();
+});
+
+setInterval(updateTimes, 1000);
+
+app.addEventListener("channel-message", (data) => {
+    if (data.channel_uid !== channelUid) {
+        if(!isMentionForSomeoneElse(data.message)){
+            channelSidebar.notify(data);
+        }
+    }
+});
diff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py
new file mode 100644
index 0000000..2d9341e
--- /dev/null
+++ b/src/snek/system/terminal.py
@@ -0,0 +1,48 @@
+import asyncio
+import aiohttp
+import aiohttp.web
+import os
+import pty
+import shlex
+import subprocess
+import pathlib
+
+commands = {
+    'alpine': 'docker run -it alpine /bin/sh',
+    'r': 'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh',
+}
+
+class TerminalSession:
+    def __init__(self,command):
+        self.master, self.slave = pty.openpty()
+        self.sockets =[]
+        self.process = subprocess.Popen(
+            command.split(" "),
+            stdin=self.slave,
+            stdout=self.slave,
+            stderr=self.slave,
+            bufsize=0,
+            universal_newlines=True
+        )
+
+    async def read_output(self, ws):
+        loop = asyncio.get_event_loop()
+        self.sockets.append(ws)
+        if len(self.sockets) > 1:
+            return 
+        while True:
+            try:
+                data = await loop.run_in_executor(None, os.read, self.master, 1024)
+                if not data:
+                    break
+                try:
+                    for ws in self.sockets: await ws.send_bytes(data)  # Send raw bytes for ANSI support
+                except:
+                    self.sockets.remove(ws)
+            except Exception:
+                break
+
+    async def write_input(self, data):
+        os.write(self.master, data.encode())
+
+
diff --git a/src/snek/templates/static b/src/snek/templates/static
new file mode 120000
index 0000000..d9bc54d
--- /dev/null
+++ b/src/snek/templates/static
@@ -0,0 +1 @@
+../static/
\ No newline at end of file
diff --git a/src/snek/templates/terminal.html b/src/snek/templates/terminal.html
new file mode 100644
index 0000000..d9ad6d1
--- /dev/null
+++ b/src/snek/templates/terminal.html
@@ -0,0 +1,47 @@
+{% extends "app.html" %}
+
+{% block sidebar %}
+Reboot
+{% endblock %}
+
+{% block main %}
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
+    <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
+    <style>
+       #terminal { width: 100%; height: 480px; overflow-y: none; }
+    </style>
+
+  <div class="container" id="terminal"></div>
+
+    <script>
+        const term = new Terminal({ cursorBlink: true });
+        const fitAddon = new FitAddon.FitAddon();
+        term.loadAddon(fitAddon);
+        term.open(document.getElementById("terminal"));
+        fitAddon.fit();
+
+        window.addEventListener("resize", () => fitAddon.fit());
+        
+        const schema = window.location.protocol === "https:" ? "wss" : "ws";
+        const hostname = window.location.host;
+        const url = `${schema}://${hostname}/terminal.ws`;
+
+        const socket = new WebSocket(url);
+        socket.binaryType = "arraybuffer";  // Support binary data
+
+        socket.onopen = () => term.write("\x1b[32mConnected to Molodetz\x1b[0m\r\n");
+
+        socket.onmessage = (event) => {
+            const data = new Uint8Array(event.data);
+            term.write(new TextDecoder().decode(data));
+        };
+
+        term.onData(data => socket.send(new TextEncoder().encode(data)));
+
+        socket.onclose = () => term.write("\r\n\x1b[31mConnection closed\x1b[0m\r\n");
+    </script>
+
+
+
+{% endblock main %}
diff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py
new file mode 100644
index 0000000..e54dfb7
--- /dev/null
+++ b/src/snek/view/terminal.py
@@ -0,0 +1,56 @@
+from snek.system.view import BaseView 
+import aiohttp 
+import asyncio
+from snek.system.terminal import TerminalSession
+import pathlib
+
+class TerminalSocketView(BaseView):
+    
+    login_required = True
+
+    user_sessions = {}
+    
+    async def prepare_drive(self):
+        user = await self.services.user.get(uid=self.session.get("uid"))
+        root = pathlib.Path("drive").joinpath(user["uid"])
+        root.mkdir(parents=True, exist_ok=True)
+        terminal_folder = pathlib.Path("terminal")
+        for path in terminal_folder.iterdir():
+            destination_path = root.joinpath(path.name)
+            if not destination_path.exists():
+                if not path.is_dir():
+                    destination_path.write_bytes(path.read_bytes())
+        return root 
+        
+    async def get(self):
+       
+
+
+        ws = aiohttp.web.WebSocketResponse()
+        await ws.prepare(self.request)
+        user = await self.services.user.get(uid=self.session.get("uid"))
+        root = await self.prepare_drive()
+        
+        command = f"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash"
+        print(command)
+
+        session = self.user_sessions.get(user["uid"])
+        if not session:
+            self.user_sessions[user["uid"]] = TerminalSession(command=command)
+        session = self.user_sessions[user["uid"]]   
+        asyncio.create_task(session.read_output(ws))  
+
+        async for msg in ws:
+            if msg.type == aiohttp.WSMsgType.BINARY:
+                await session.write_input(msg.data.decode())
+
+        
+        return ws
+
+class TerminalView(BaseView):
+
+    login_required = True
+
+    async def get(self):
+        request = self.request
+        return await self.request.app.render_template('terminal.html',self.request)
diff --git a/terminal/.bashrc b/terminal/.bashrc
new file mode 100644
index 0000000..2b5d791
--- /dev/null
+++ b/terminal/.bashrc
@@ -0,0 +1,108 @@
+# ~/.bashrc: executed by bash(1) for non-login shells.
+# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
+# for examples
+
+# If not running interactively, don't do anything
+[ -z "$PS1" ] && return
+
+# don't put duplicate lines in the history. See bash(1) for more options
+# ... or force ignoredups and ignorespace
+HISTCONTROL=ignoredups:ignorespace
+
+# append to the history file, don't overwrite it
+shopt -s histappend
+
+# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
+HISTSIZE=1000
+HISTFILESIZE=2000
+
+# check the window size after each command and, if necessary,
+# update the values of LINES and COLUMNS.
+shopt -s checkwinsize
+
+# make less more friendly for non-text input files, see lesspipe(1)
+[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"
+
+# set variable identifying the chroot you work in (used in the prompt below)
+if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then
+    debian_chroot=$(cat /etc/debian_chroot)
+fi
+
+# set a fancy prompt (non-color, unless we know we "want" color)
+case "$TERM" in
+    xterm-color) color_prompt=yes;;
+esac
+
+# uncomment for a colored prompt, if the terminal has the capability; turned
+# off by default to not distract the user: the focus in a terminal window
+# should be on the output of commands, not on the prompt
+#force_color_prompt=yes
+
+if [ -n "$force_color_prompt" ]; then
+    if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
+	# We have color support; assume it's compliant with Ecma-48
+	# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
+	# a case would tend to support setf rather than setaf.)
+	color_prompt=yes
+    else
+	color_prompt=
+    fi
+fi
+
+if [ "$color_prompt" = yes ]; then
+    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
+else
+    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
+fi
+unset color_prompt force_color_prompt
+
+# If this is an xterm set the title to user@host:dir
+case "$TERM" in
+xterm*|rxvt*)
+    PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
+    ;;
+*)
+    ;;
+esac
+
+# enable color support of ls and also add handy aliases
+if [ -x /usr/bin/dircolors ]; then
+    test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
+    alias ls='ls --color=auto'
+    #alias dir='dir --color=auto'
+    #alias vdir='vdir --color=auto'
+
+    alias grep='grep --color=auto'
+    alias fgrep='fgrep --color=auto'
+    alias egrep='egrep --color=auto'
+fi
+
+# some more ls aliases
+alias ll='ls -alF'
+alias la='ls -A'
+alias l='ls -CF'
+
+# Alias definitions.
+# You may want to put all your additions into a separate file like
+# ~/.bash_aliases, instead of adding them here directly.
+# See /usr/share/doc/bash-doc/examples in the bash-doc package.
+
+if [ -f ~/.bash_aliases ]; then
+    . ~/.bash_aliases
+fi
+
+
+cp ~/r /usr/local/bin 
+
+chmod -x /usr/local/bin/*
+
+apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv -y
+
+echo "r is installed."
+
+# enable programmable completion features (you don't need to enable
+# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
+# sources /etc/bash.bashrc).
+#if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
+#    . /etc/bash_completion
+#fi
diff --git a/terminal/.profile b/terminal/.profile
new file mode 100644
index 0000000..c4c7402
--- /dev/null
+++ b/terminal/.profile
@@ -0,0 +1,9 @@
+# ~/.profile: executed by Bourne-compatible login shells.
+
+if [ "$BASH" ]; then
+  if [ -f ~/.bashrc ]; then
+    . ~/.bashrc
+  fi
+fi
+
+mesg n 2> /dev/null || true
diff --git a/terminal/r b/terminal/r
new file mode 100755
index 0000000..2cc65df
Binary files /dev/null and b/terminal/r differ