on
Self-Hosting a reMarkable Cloud
Contents
Overview
I’ve been self-hosting my own things for over 10 years. A requirement for nearly any hardware I buy is that I am able to keep using it with minimal disruption if the company behind it disappears or worse.
I also have a slight obsession with e-ink. Nearly 4 years ago, I realized that there were companies making tablets with e-ink displays, primarily for note-taking. I did a bit of research and found that reMarkable gives you root SSH access out of the box, no jailbreaking required. Some more digging revealed that there was a decent community of nerds doing cool things on the tablets, including a project called rmfakecloud that allows one to host their own cloud for reMarkable tablets to sync, email, convert handwriting, etc. reMarkable has a 100 day return window, so I figured I’d check out the hardware and make sure I could get it running without the device relying on the official cloud servers at all.
In the end, I was able to not only get the device talking to my own server, but even the official desktop and mobile apps. This all is still working, up to the time of writing this and I wanted to outline the solution I have in place in case it’s useful for others. This isn’t meant to be a full tutorial, but hopefully could serve as a jumping-off point for someone interesting in doing something similar.
The Pieces
To achieve my goal, I needed to do three things:
- Host the rmfakecloud server
- Get the traffic sent to my server instead of the official cloud
- Get the applications to trust my server like it was the official cloud
If your only goal is to get a reMarkable tablet talking to your server, you only need one additional piece: rmfakecloud-proxy. The proxy gets installed on the device via a script which places the correct entries in the device’s DNS hosts file, creates and installs a certificate, and then takes care of forwarding the traffic to your rmfakecloud instance.
But I wanted to use apps, and I wanted to use them anywhere in the world.
Hosting the Server
As discussed in a previous post, I run Kubernetes for all of my infrastructure. This is not a requirement at all. I used cert-manager and ingress-nginx to handle the certificate management and reverse proxy functionality respectively, because I already use those services for everything else.
Hosting the actual rmfakecloud service isn’t anything special, it’s a Docker container that needs a volume mounted to /data
. Let’s get some traffic to it!
Getting Traffic To The Server
DNS
We’ve got a server, now we have to send traffic to it instead of the official cloud. To do that, we need our old enemy friend DNS.
On the device, I’m just setting my /etc/hosts
file like the rmfakecloud-proxy does, pointing directly to the public IP address of my ingress-nginx ingress controller. At the time of writing:
203.0.113.5 hwr-production-dot-remarkable-production.appspot.com
203.0.113.5 service-manager-production-dot-remarkable-production.appspot.com
203.0.113.5 local.appspot.com
203.0.113.5 my.remarkable.com
203.0.113.5 ping.remarkable.com
203.0.113.5 internal.cloud.remarkable.com
203.0.113.5 webapp-prod.cloud.remarkable.engineering
203.0.113.5 eu.tectonic.remarkable.com
I re-copy this file after every reMarkable software update. While we’re talking about updates, you should probably disable auto-updates and make sure rmfakecloud works for the new version before you pull the trigger.
But what about for everything else? If you only want to access your cloud while in your house, consider an adblocker like AdGuard Home. Critically for us, it also lets you override DNS like a network-wide hosts file.
The thing that gave me the most trouble was hijacking DNS on iOS for when I’m not at home. I happen to use Tailscale, and it was what ended up solving this particular problem for me. Tailscale allows you to set up custom DNS servers for specific domains, called Split DNS. I set these up for the remarkable.com
and appspot.com
domains, pointing to my AdGuard Home instance, which is on my TailNet:

Reverse Proxy
The reverse proxy needs to handle all of the domains we’re hijacking and serve our self-signed cert for them (more on this in the next section). It should listen on port 443 and send traffic to rmfakecloud on port 3000.
Here’s my Kubernetes ingress to accomplish that, I’m setting some high limits on size and timeouts since we’re syncing documents over this:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/client-body-buffer-size: 128k
nginx.ingress.kubernetes.io/proxy-body-size: 1000m
nginx.ingress.kubernetes.io/proxy-read-timeout: "10800"
name: remarkable-cloud-hijack
namespace: remarkable-cloud
spec:
ingressClassName: nginx
rules:
- host: hwr-production-dot-remarkable-production.appspot.com
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
- host: service-manager-production-dot-remarkable-production.appspot.com
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
- host: local.appspot.com
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
- host: my.remarkable.com
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
- host: internal.cloud.remarkable.com
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
- host: ping.remarkable.com
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
- host: backtrace-proxy.cloud.remarkable.engineering
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
- host: eu.tectonic.remarkable.com
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
tls:
- hosts:
- hwr-production-dot-remarkable-production.appspot.com
- service-manager-production-dot-remarkable-production.appspot.com
- local.appspot.com
- my.remarkable.com
- internal.cloud.remarkable.com
- ping.remarkable.com
- backtrace-proxy.cloud.remarkable.engineering
- eu.tectonic.remarkable.com
secretName: rmfakecloud-tls
Don’t forget the actual ingress for your rmfakecloud instance. I’m using a legit wildcard cert via Let’s Encrypt for this, I don’t like subdomain certs since they leak your domains to the world via certificate transparency lists:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/client-body-buffer-size: 128k
nginx.ingress.kubernetes.io/proxy-body-size: 300m
nginx.ingress.kubernetes.io/proxy-read-timeout: "10800"
name: remarkable-cloud
namespace: remarkable-cloud
spec:
ingressClassName: nginx
rules:
- host: remarkable.mydomain.tld
http:
paths:
- backend:
service:
name: remarkable-cloud
port:
number: 3000
path: /
pathType: Prefix
tls:
- hosts:
- remarkable.mydomain.tld
secretName: mydomain-wildcard
Getting The Apps To Trust Us
We need to be able to generate certs for a bunch of domains, at the time of writing the count is 10, and we may need to add to this as time goes on. This sounds like a job for a certificate authority (CA). With one, we can simply trust a single root certificate on our devices, and that certificate will be used to create the individual certificates we need, and our devices will trust them.
Here’s how I created my CA certificate:
# Generate a private key
openssl genrsa -out ca.key 4096
# Create a self-signed CA certificate (valid for 30 years)
openssl req -x509 -new -nodes -key ca.key -sha256 -days 10950 -out ca.crt \
-subj "/CN=My Internal CA/O=My Organization/C=US"
Now in my case, I need to get that into Kubernetes:
kubectl create secret tls ca-key-pair \
--cert=ca.crt \
--key=ca.key \
--namespace=cert-manager
Since I want cert-manager to create my certs for me, I need to create an Issuer that uses that CA cert:
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: ca-issuer
namespace: cert-manager
spec:
ca:
secretName: ca-key-pair
And finally, the certificate itself (check the rmfakecloud docs to make sure this list hasn’t changed):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: rmfakecloud
namespace: cert-manager
spec:
secretName: rmfakecloud-tls
issuerRef:
name: ca-issuer
dnsNames:
- "*.appspot.com"
- "*.remarkable.com"
- "internal.cloud.remarkable.com"
- "backtrace-proxy.cloud.remarkable.engineering"
- "eu.tectonic.remarkable.com"
If you need to generate the certs with openssl, that’ll look something like this:
# Generate server private key
openssl genrsa -out server-key.pem 4096
# Create a configuration file for the certificate request with SANs
cat > server-cert.conf <<EOF
[req]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req
[dn]
C=US
ST=State
L=City
O=MyOrg
OU=MyUnit
CN=*.remarkable.com
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.appspot.com
DNS.2 = *.remarkable.com
DNS.3 = internal.cloud.remarkable.com
DNS.4 = backtrace-proxy.cloud.remarkable.engineering
DNS.5 = eu.tectonic.remarkable.com
EOF
# Generate certificate signing request (CSR)
openssl req -new -key server-key.pem -out server-cert.csr -config server-cert.conf
# Sign the CSR with the CA (valid for 1 year for proper browser support)
openssl x509 -req -in server-cert.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server-cert.pem -days 365 \
-extensions v3_req -extfile server-cert.conf
On the devices, import ca.crt
into the trusted root certificate store and make sure they’re using the correct DNS server. Once everything is set up and paired to rmfakecloud, the apps work just fine:
