diff --git a/README.md b/README.md
index a5cb1a4..b19c7cc 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,15 @@ docker run -d -it  \
 docker attach factorio
 ```
 
+### RCON
+
+Alternativly (e.g. for scripting) the RCON connection can be used to send commands to the running factorio server.
+This does not require the RCON connection to be exposed.
+
+```shell
+docker exec factorio rcon /h
+```
+
 ### Upgrading
 
 Before upgrading backup the save. It's easy to make a save in the client.
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..a3e7e8f
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,35 @@
+version: "2"
+services:
+  factorio:
+    container_name: factorio
+    image: factoriotools/factorio:stable
+    restart: unless-stopped
+    ports:
+      - "34197:34197/udp"
+      - "27015:27015/tcp"
+    volumes:
+      - ./data:/factorio
+    environment:
+      - UPDATE_MODS_ON_START=true
+    #labels:
+    #  # Labels to allow autoupdate only if no players are online
+    #  - com.centurylinklabs.watchtower.enable=true
+    #  - com.centurylinklabs.watchtower.scope=factorio
+    #  - com.centurylinklabs.watchtower.lifecycle.pre-update="/players-online.sh"
+
+  # Uncomment the following files to use watchtower for updating the factorio container
+  # Full documentation of watchtower: https://github.com/containrrr/watchtower
+  #watchtower:
+  #  container_name: watchtower_factorio
+  #  image: containrrr/watchtower
+  #  restart: unless-stopped
+  #  volumes:
+  #   - /var/run/docker.sock:/var/run/docker.sock
+  #  environment:
+  #    # Only update containers which have the option 'watchtower.enable=true' set
+  #    - WATCHTOWER_TIMEOUT=30s
+  #    - WATCHTOWER_LABEL_ENABLE=true
+  #    - WATCHTOWER_POLL_INTERVAL=3600
+  #    - WATCHTOWER_LIFECYCLE_HOOKS=true
+  #  labels:
+  #    - com.centurylinklabs.watchtower.scope=factorio
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 2aaec4c..d03f8bf 100755
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,5 +1,14 @@
-FROM debian:stable-slim
+# build rcon client
+FROM debian:stable-slim AS rcon-builder
+RUN apt-get -q update \
+    && DEBIAN_FRONTEND=noninteractive apt-get -qy install build-essential
 
+WORKDIR /src
+COPY rcon/* /src
+RUN make
+
+# build factorio image
+FROM debian:stable-slim
 LABEL maintainer="https://github.com/factoriotools/factorio-docker"
 
 ARG USER=factorio
@@ -81,6 +90,7 @@ RUN set -ox pipefail \
 
 COPY files/*.sh /
 COPY files/config.ini /opt/factorio/config/config.ini
+COPY --from=rcon-builder /src/rcon /bin/rcon
 
 VOLUME /factorio
 EXPOSE $PORT/udp $RCON_PORT/tcp
diff --git a/docker/files/players-online.sh b/docker/files/players-online.sh
new file mode 100755
index 0000000..5814b74
--- /dev/null
+++ b/docker/files/players-online.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+PLAYERS=$(rcon /players)
+ONLINE_COUNT=$(echo "$PLAYERS" | grep -c " (online)$")
+
+if [[ "$ONLINE_COUNT" -gt "0" ]]; then
+    echo "$PLAYERS"
+    # exit with 75 (EX_TEMPFAIL) so watchtower skips the update
+    # https://containrrr.dev/watchtower/lifecycle-hooks/
+    exit 75
+fi
diff --git a/docker/rcon/Makefile b/docker/rcon/Makefile
new file mode 100644
index 0000000..71131c0
--- /dev/null
+++ b/docker/rcon/Makefile
@@ -0,0 +1,13 @@
+# Optimization
+OPT = -O3 -flto
+TARGET = rcon
+
+CC = gcc
+CFLAGS = -std=c17 -Wall -Wextra -pedantic $(OPT)
+REMOVE = rm -f
+
+all:
+	$(CC) $(CFLAGS) main.c -o $(TARGET)
+
+clean:
+	$(REMOVE) $(TARGET)
diff --git a/docker/rcon/main.c b/docker/rcon/main.c
new file mode 100644
index 0000000..5eb7206
--- /dev/null
+++ b/docker/rcon/main.c
@@ -0,0 +1,217 @@
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <arpa/inet.h>
+
+#define MIN_PACKET 10
+#define MAX_PACKET 4096
+#define MAX_BODY (MAX_PACKET - (3 * sizeof(uint32_t)) - 2)
+
+#define RCON_HOST "127.0.0.1"
+
+typedef enum {
+    RCON_TYPE_RESPONSE = 0,
+    RCON_TYPE_EXECCOMMAND = 2,
+    RCON_TYPE_AUTH_RESPONSE = 2,
+    RCON_TYPE_AUTH = 3,
+} packet_type;
+
+typedef struct {
+    uint32_t length;
+    uint32_t id;
+    packet_type type;
+    char body[MAX_BODY];
+} packet;
+
+int rcon_open(const char *port);
+void rcon_create(packet* pkt, packet_type type, const char* body);
+bool rcon_send(int rcon_socket, const packet* pkt);
+bool rcon_auth(int rcon_socket, const char* password);
+bool rcon_recv(int rcon_socket, packet* pkt, packet_type expected_type);
+char* combine_args(int argc, char* argv[]);
+char* read_password(const char* conf_dir);
+
+int main(int argc, char* argv[]) {
+    if (argc < 2) {
+        fprintf(stderr, "error: missing command argument\n");
+        return EXIT_FAILURE;
+    }
+    
+    srand((unsigned int)time(NULL));
+    
+    const char* port = getenv("RCON_PORT");
+    if (port == NULL) {
+        fprintf(stderr, "error: missing $RCON_PORT env\n");
+        return EXIT_FAILURE;
+    }
+    
+    const char* conf_dir = getenv("CONFIG");
+    if (conf_dir == NULL) {
+        fprintf(stderr, "error: missing $CONFIG env");
+        exit(EXIT_FAILURE);
+    }
+    
+    int rcon_socket = rcon_open(port);
+    if (rcon_socket == -1) {
+        fprintf(stderr, "error: could not connect\n");
+        return EXIT_FAILURE;
+    }
+    
+    if (!rcon_auth(rcon_socket, read_password(conf_dir))) {
+        fprintf(stderr, "error: login failed\n");
+        return EXIT_FAILURE;
+    }
+    
+    packet pkt;
+    rcon_create(&pkt, RCON_TYPE_EXECCOMMAND, combine_args(argc, argv));
+    if (!rcon_send(rcon_socket, &pkt)) {
+        fprintf(stderr, "error: send command failed\n");
+        return EXIT_FAILURE;
+    }
+    
+    if (rcon_recv(rcon_socket, &pkt, RCON_TYPE_RESPONSE) && pkt.length > 0) {
+        puts(pkt.body);
+    }
+    
+    return EXIT_SUCCESS;
+}
+
+char* combine_args(int argc, char* argv[]) {
+    // combine all cli arguments
+    char* command = malloc(MAX_BODY);
+    memset(command, 0, MAX_BODY);
+    strcat(command, argv[1]);
+    
+    for (int idx = 2; idx < argc; idx++) {
+        strcat(command, " ");
+        strcat(command, argv[idx]);
+    }
+    
+    return command;
+}
+
+char* read_password(const char* conf_dir) {
+    char* path = malloc(strlen(conf_dir) + 64);
+    strcpy(path, conf_dir);
+    strcat(path, "/rconpw");
+    
+    FILE* fptr = fopen(path, "r");
+    fseek(fptr, 0, SEEK_END);
+    long fsize = ftell(fptr);
+    fseek(fptr, 0, SEEK_SET);  /* same as rewind(f); */
+
+    char *password = malloc(fsize + 1);
+    fread(password, fsize, 1, fptr);
+    fclose(fptr);
+
+    password[fsize] = 0;
+    if (password[fsize-1] == '\n') {
+        password[fsize-1] = 0;
+    }
+    
+    return password;
+}
+
+int rcon_open(const char *port) {
+    struct sockaddr_in address = {
+        .sin_family = AF_INET,
+        .sin_port = htons(atoi(port))
+    };
+    inet_aton(RCON_HOST, &address.sin_addr);
+    
+    int rcon_socket = socket(AF_INET, SOCK_STREAM, 0);
+    if (connect(rcon_socket, (struct sockaddr*) &address, sizeof(address)) < 0) {
+        return -1;
+    } else {
+        return rcon_socket;
+    }
+}
+
+void rcon_create(packet* pkt, packet_type type, const char* body) {
+    size_t body_length = strlen(body);
+    if (body_length >= MAX_BODY - 2) {
+        fprintf(stderr, "error: command to long");
+        exit(EXIT_FAILURE);
+    }
+    
+    pkt->id = abs(rand());
+    pkt->type = type;
+    pkt->length = (uint32_t)(sizeof(pkt->id) + sizeof(pkt->type) + body_length + 2);
+    
+    memset(pkt->body, 0, MAX_BODY);
+    strncpy(pkt->body, body, MAX_BODY);
+}
+
+bool rcon_recv(int rcon_socket, packet* pkt, packet_type expected_type) {
+    memset(pkt, 0, sizeof(*pkt));
+    
+    // Read response packet length
+    ssize_t expected_length_bytes = sizeof(pkt->length);
+    ssize_t rx_bytes = recv(rcon_socket, &(pkt->length), expected_length_bytes, 0);
+    
+    if (rx_bytes == -1) {
+        perror("error: socket error");
+        return false;
+    } else if (rx_bytes == 0) {
+        fprintf(stderr, "error: no data recieved\n");
+        return false;
+    } else if (rx_bytes < expected_length_bytes || pkt->length < MIN_PACKET || pkt->length > MAX_PACKET) {
+        fprintf(stderr, "error: invalid data\n");
+        return false;
+    }
+    
+    ssize_t received = 0;
+    while (received < pkt->length) {
+        rx_bytes = recv(rcon_socket, (char *)pkt + sizeof(pkt->length) + received, pkt->length - received, 0);
+        if (rx_bytes < 0) {
+            perror("error: socket error");
+            return false;
+        } else if (rx_bytes == 0) {
+            fprintf(stderr, "error: connection lost\n");
+            return false;
+        }
+        
+        received += rx_bytes;
+    }
+    
+    return pkt->type == expected_type;
+}
+
+bool rcon_send(int rcon_socket, const packet* pkt) {
+    size_t length = sizeof(pkt->length) + pkt->length;
+    char *ptr = (char*) pkt;
+    
+    while (length > 0) {
+        ssize_t ret = send(rcon_socket, ptr, length, 0);
+        
+        if (ret == -1) {
+            return false;
+        }
+        
+        ptr += ret;
+        length -= ret;
+    }
+    
+    return true;
+}
+
+bool rcon_auth(int rcon_socket, const char* password) {
+    packet pkt;
+    rcon_create(&pkt, RCON_TYPE_AUTH, password);
+    
+    if (!rcon_send(rcon_socket, &pkt)) {
+        return false;
+    }
+    
+    if (!rcon_recv(rcon_socket, &pkt, RCON_TYPE_AUTH_RESPONSE)) {
+        return false;
+    }
+    
+    return true;
+}