diff --git a/Dockerfile b/Dockerfile
index 8615be52fb07336082dd21282b2c00fd65bfac6c..d56437a470709cc32dbfffc36527dd69d1aa6d3f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,11 +4,8 @@ FROM debian:stable
 ARG MAXIMA_VERSION
 # e.g. 2.0.22.0.2
 ARG SBCL_VERSION
-# e.g. assStackQuestion/classes/stack/maxima
-ARG LIB_PATH
 
-RUN echo ${LIB_PATH?Error \$LIB_PATH is not defined} \
-	 ${MAXIMA_VERSION?Error \$MAXIMA_VERSION is not defined} \
+RUN echo ${MAXIMA_VERSION?Error \$MAXIMA_VERSION is not defined} \
 	 ${SBCL_VERSION?Error \$SBCL_VERSION is not defined}
 
 ENV SRC=/opt/src \
@@ -28,6 +25,7 @@ RUN apt-get update \
     make \
     wget \
     python3 \
+    gcc \
 #    ca-certificates \
 #    curl \
     texinfo
@@ -55,16 +53,28 @@ RUN cd ${SRC} \
 && make install \
 && make clean
 
+
+RUN apt-get install -y gnuplot gettext-base sudo psmisc libbsd-dev tini
+
+COPY ./src/maxima_fork.c ${SRC}
+
+RUN cd ${SRC} && gcc -shared maxima_fork.c -lbsd -fPIC -Wall -Wextra -o libmaximafork.so \
+    && mv libmaximafork.so /usr/lib
+
 RUN rm -r ${SRC} /SBCL_ARCH
-RUN apt-get install -y gnuplot gettext-base sudo psmisc
 
 RUN mkdir -p ${LIB} ${LOG} ${TMP} ${PLOT} ${ASSETS} ${BIN}
 
+
+# e.g. assStackQuestion/classes/stack/maxima
+ARG LIB_PATH
+
+RUN echo ${LIB_PATH?Error \$LIB_PATH is not defined}
 # Copy Libraries
 COPY ${LIB_PATH} ${LIB}
 
 # Copy optimization scripts
-COPY assets/optimize.mac.template assets/maximalocal.mac.template ${ASSETS}/
+COPY assets/maxima-fork.lisp assets/optimize.mac.template assets/maximalocal.mac.template ${ASSETS}/
 
 RUN grep stackmaximaversion ${LIB}/stackmaxima.mac | grep -oP "\d+" >> /opt/maxima/stackmaximaversion \
     && sh -c 'envsubst < ${ASSETS}/maximalocal.mac.template > ${ASSETS}/maximalocal.mac \
@@ -74,19 +84,14 @@ RUN grep stackmaximaversion ${LIB}/stackmaxima.mac | grep -oP "\d+" >> /opt/maxi
     && maxima -b optimize.mac \
     && mv maxima-optimised ${BIN}/maxima-optimised
 
-RUN apt-get purge -y wget python3 make bzip2 texinfo
+RUN apt-get purge -y wget python3 make bzip2 texinfo gcc
 
-RUN useradd -M maxima-server && echo "Defaults	lecture = always" > /etc/sudoers.d/maxima
 RUN for i in $(seq 16); do \
-           useradd -M "maxima-$i" \
-        && echo "maxima-server     ALL = (maxima-$i) NOPASSWD: ${BIN}/wrapper" >> /etc/sudoers.d/maxima \
-        && echo "maxima-server     ALL = (root) NOPASSWD: /usr/bin/killall -9 -u maxima-$i" >> /etc/sudoers.d/maxima; \
+           useradd -M "maxima-$i"; \
     done
 
 # Add go webserver
 COPY ./bin/web ${BIN}/goweb
-# Add wrapper
-COPY ./bin/wrapper ${BIN}/wrapper
 
-CMD ["su", "-c", "/opt/maxima/bin/goweb", "maxima-server"]
+CMD rm /dev/tty && exec tini ${BIN}/goweb
 
diff --git a/assets/maxima-fork.lisp b/assets/maxima-fork.lisp
new file mode 100644
index 0000000000000000000000000000000000000000..0c408b43e3996c3ad79a62bf3fd0d466e4708bbb
--- /dev/null
+++ b/assets/maxima-fork.lisp
@@ -0,0 +1,29 @@
+(cl:in-package "MAXIMA")
+
+(defun set-tmp-dir-vars (tmp-dir)
+  (defparameter |$image_dir| (concatenate 'string tmp-dir "/output/"))
+  (defparameter |$MAXIMA_TEMPDIR| (concatenate 'string tmp-dir "/work/"))
+  nil)
+(defparameter |$url_base| "!ploturl!")
+
+(cl:defpackage "MAXIMA-FORK"
+	       (:use "CL" "SB-ALIEN")
+	       (:export "FORKING-LOOP"))
+(cl:in-package "MAXIMA-FORK")
+
+;;; load shared library
+(load-shared-object "libmaximafork.so")
+
+;;; define c function
+(declaim (inline fork-new-process))
+(define-alien-routine fork-new-process c-string)
+
+;;; forking loop
+(defun forking-loop ()
+  (finish-output)
+  (let ((tmp-dir (fork-new-process)))
+    (when (not tmp-dir)
+      (sb-ext:exit :code 1))
+    (maxima::set-tmp-dir-vars tmp-dir))
+  t)
+
diff --git a/assets/optimize.mac.template b/assets/optimize.mac.template
index 753db1144f13db60a21f833d17473799fd9b23a7..bb78dbf17c58fda7e617eed9e79faf164796d7f3 100644
--- a/assets/optimize.mac.template
+++ b/assets/optimize.mac.template
@@ -3,6 +3,7 @@
    https://github.com/maths/moodle-qtype_stack/blob/master/doc/en/CAS/Optimising_Maxima.md */
 
 load("${ASSETS}/maximalocal.mac");
+load("${ASSETS}/maxima-fork.lisp");
 load("${LIB}/stackmaxima.mac");
 load(stats);
 load(distrib);
diff --git a/src/maxima_fork.c b/src/maxima_fork.c
new file mode 100644
index 0000000000000000000000000000000000000000..7eb457380438aa39295d500c02164d234498d958
--- /dev/null
+++ b/src/maxima_fork.c
@@ -0,0 +1,212 @@
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <pwd.h>
+#include <signal.h>
+
+#include <sys/resource.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <bsd/unistd.h>
+#include <limits.h>
+#include <grp.h>
+#define N_SLOT 16
+#define RNOFILE 256
+#define FILEPATH_LEN (PATH_MAX + 1)
+char filepath[FILEPATH_LEN];
+
+// inits a maxima process for web service:
+// changes gid/uid to maxima-{slot}
+// redirects input/output, creates temporary subdirectories
+char *fork_new_process() {
+	fflush(stdout);
+
+	// send an S for Synchronization, so that
+	// the server process doesn't accidentally write into
+	// sbcl's buffer
+	// the server should not write anything before it has read this
+	write(STDOUT_FILENO, "S", 1);
+
+	// while the loop is running, the SIGCHLD handler
+	// is deactivated so that children are automatically reaped
+	// after that, it is again restored
+	struct sigaction old, new;
+	new.sa_handler = SIG_IGN;
+	sigemptyset(&new.sa_mask);
+	new.sa_flags = SA_NOCLDWAIT;
+	char *ret = NULL;
+	if (sigaction(SIGCHLD, &new, &old) == -1) {
+		perror("Could not set signal error for children");
+		return NULL;
+	}
+
+	// when sbcl spawns a child process through lisp, sbcl tries to close all
+	// filedescriptors until RLIMIT_NOFILE
+	// in docker containers, this is by standard quite high, so it takes long
+	// which is remediated here by setting it lower manually
+	struct rlimit nofile = { .rlim_cur = RNOFILE, .rlim_max = RNOFILE };
+	if (setrlimit(RLIMIT_NOFILE, &nofile) == -1) {
+		perror("Error setting rlimit_nofile");
+		sigaction(SIGCHLD, &old, NULL);
+		return NULL;
+	}
+
+	for (;;) {
+		// can't flush enough
+		fflush(stdout);
+		int slot;
+		
+		// the slot number and temp directory is sent to the process
+		// over stdin in the format "%d%s", where %s can contain anything
+		// but newlines and musn't start with a number, which isn't a
+		// problem for absolute paths
+		if (scanf("%d", &slot) == EOF) {
+			if (errno != 0) {
+				perror("Error getting slot number from stdin");
+				ret = NULL;
+			}
+			break;
+		}
+		char *tempdir = fgets(filepath, FILEPATH_LEN, stdin);
+		if (!tempdir) {
+			perror("Error getting temp path name");
+			ret = NULL;
+			break;
+		}
+		// remove the last newline, if it exists
+		size_t last_char = strlen(tempdir) - 1;
+		if (tempdir[last_char] == '\n') {
+			tempdir[strlen(tempdir) - 1] = '\0';
+		}
+
+		// we fork the main process and use the child without execve
+		// this way, startup time is improved
+		pid_t pid = fork();
+		if (pid == -1) {
+			perror("Could not fork");
+			ret = NULL;
+			break;
+		}
+		if (pid != 0) {
+			continue;
+		}
+
+		if (chdir(tempdir) == -1) {
+			perror("Could not chdir to temporary directory");
+			ret = NULL;
+			break;
+		}
+
+		// redirect stdout to pipe
+		// note: open outpipe before inpipe to avoid deadlock
+		int outfd = open("outpipe", O_WRONLY);
+		if (outfd == -1) {
+			perror("Could not connect output pipe");
+			ret = NULL;
+			break;
+		}
+		if (dup2(outfd, STDOUT_FILENO) == -1) {
+			perror("Could not copy output file descriptor");
+			ret = NULL;
+			break;
+		}
+
+		// redirect stdin from pipe
+		int infd = open("inpipe", O_RDONLY);
+		if (infd == -1) {
+			perror("Could not create input pipe");
+			ret = NULL;
+			break;
+		}
+		if (dup2(infd, STDIN_FILENO) == -1) {
+			perror("Could not copy input file descriptor");
+			ret = NULL;
+			break;
+		}
+
+		// replace stdin with a new stream, for good measure
+		FILE *new_stdin = fdopen(STDIN_FILENO, "r");
+		if (!new_stdin) {
+			perror("Could not create stream from stdin");
+			ret = NULL;
+			break;
+		}
+		stdin = new_stdin;
+
+		// everything execpt std{in,out,err} is closed
+		// note: this is a function from libbsd
+		closefrom(3);
+
+		// get uid/gid from username
+		if (slot <= 0 || slot > N_SLOT) {
+			dprintf(STDERR_FILENO, "Invalid slot number: %d\n", slot);
+			ret = NULL;
+			break;
+		}
+		char username[16];
+		int len = snprintf(username, 15, "maxima-%d", slot);
+		if (len < 0 || len > 15) {
+			dprintf(STDERR_FILENO, "Internal error getting user name\n");
+			ret = NULL;
+			break;
+		}
+		struct passwd *userinfo = getpwnam(username);
+		if (!userinfo) {
+			dprintf(STDERR_FILENO, "Could not read user information for %s: %s\n",
+					username, strerror(errno));
+			ret = NULL;
+			break;
+		}
+		uid_t uid = userinfo->pw_uid;
+		gid_t gid = userinfo->pw_gid;
+		if (uid == 0 || gid == 0) {
+			dprintf(STDERR_FILENO, "Refusing to setuid/gid to root\n");
+			ret = NULL;
+			break;
+		}
+
+		// note: setgid should be executed before setuid when dropping from root
+		if (setgid(gid) == -1) {
+			perror("Could not set gid");
+			ret = NULL;
+			break;
+		}
+	
+		// remove all aux groups
+		if (setgroups(0, NULL)) {
+			perror("Could not remove aux groups");
+			ret = NULL;
+			break;
+		}
+	
+		// after this, we should be non-root
+		if (setuid(uid) == -1) {
+			perror("Could not set uid");
+			ret = NULL;
+			break;
+		}
+
+		// create temporary folders and files
+		if (mkdir("output", 0770) == -1) {
+			perror("Could not create output directory");
+			ret = NULL;
+			break;
+		}
+		if (mkdir("work", 0770) == -1) {
+			perror("Could not create work directory");
+			ret = NULL;
+			break;
+		}
+		ret = tempdir;
+		break;
+	}
+	// restore normal SIGCHLD handler
+	if (sigaction(SIGCHLD, &old, NULL) == -1) {
+		return NULL;
+	}
+	return ret;
+}