Can I self-host TickTick?
YES · ONE COMMAND— setup effort 1 of 4YES — it's called Super Productivity. It takes one prompt, a 256 MB VPS, and about 8 minutes. That is $3 a month you stop paying TickTick — $36 a year on the Premium plan.
Why people pay for TickTick
Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.
TickTick sells the feeling that nothing is being dropped. A task goes in from a phone, a browser tab, a widget or a voice note, and it comes back out on every other device with a reminder attached, and that reflex is what people renew for. Premium then charges for the views that make a long list usable: the calendar, custom filters, higher folder and list ceilings, and the history that shows what changed.
| Plan | List price | What it buys |
|---|---|---|
| Free | free | Free. The upgrade page does not print the free tier's ceilings; it lists what Premium unlocks, which is the calendar views, the Filter feature, higher folder, list, task and checklist ceilings, task and list history, checklist-item reminders, calendar widgets, estimated Pomo, premium themes and white noises, and the Android Quick Ball. |
| Premiumthe plan this page prices against | $3/mo | Sold by the year. On 2026-08-14 the upgrade page read 'Limited-Time Offer: Annual plan for US$ 49.99 US$ 35.99 (28% off)', so US$35.99 for the year at the offer price and US$49.99 at list. The monthly figure recorded here is the offer price divided by twelve; at list price it works out to US$4.17 a month. No monthly billing option appears on that page. |
Vendor list prices in USD, read from the pricing page on 2026-08-14 · confidence: medium
Replaced by Super Productivity
One project, named before the prompt, so you know what you are about to install.
A task list with timeboxing, a Pomodoro timer and per-task time tracking, served from one container that keeps none of your data.
The closest match to what a TickTick subscriber actually does all day: a personal list with timeboxing, a Pomodoro timer and per-task time tracking, MIT licensed, served from one container. The trade is worth understanding before you install rather than after. The web build is local-first in the strictest sense, so your tasks live in the browser profile you opened it in and not on the server you set up. That makes the install short and the backup a file you export yourself, and it makes cross-device sync the part you have to build. Reminders only reach you while a tab is open.
What else we looked at
Ranked, with the reason each one placed where it did. Only the pick has a prompt on this page — the runners-up are named so the choice is visible, not hidden.
- Super Productivityour pickONE COMMAND— setup effort 1 of 4
A task list with timeboxing, a Pomodoro timer and per-task time tracking, served from one container that keeps none of your data.
The closest match to what a TickTick subscriber actually does all day: a personal list with timeboxing, a Pomodoro timer and per-task time tracking, MIT licensed, served from one container. The trade is worth understanding before you install rather than after. The web build is local-first in the strictest sense, so your tasks live in the browser profile you opened it in and not on the server you set up. That makes the install short and the backup a file you export yourself, and it makes cross-device sync the part you have to build. Reminders only reach you while a tab is open.
- VikunjaONE COMMAND— setup effort 1 of 4
One container holding every task, project and due date, with no project cap and no per-seat bill.
The one to pick if what you want back is a list with a server behind it: one container, an embedded database, real accounts, and the same tasks on every device without any browser trickery. It keeps the top spot on this catalogue's Todoist page, and it sits second here because TickTick's daily loop is a timer and a calendar as much as a list, and Vikunja has no Pomodoro timer and puts time tracking behind a paid key. Reminders still need a mail server or a webhook before anything chases you.
The swap
You'd run
Super Productivity
ONE COMMAND · ~8 min to running · 256 MB RAM
TickTick Premium · vendor list price · checked 2026-08-14 · source · confidence: medium
Before you start
- RAM floor
- 256 MBfloor from upstream docs — not measured by us yet
- Disk
- 2 GBthe app, its data, and room for one backup
- Domain needed
- yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
- Time budget
- ~8 minunder 10 minutes, through the first backup
The prompt
Two paths to the same Super Productivity: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.
Where it runs
296 lines · 14,988 bytes
What this prompt will do
- Preflight
- Layout
- Secrets
- compose.yml
- Caddy and TLS
- Firewall
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.
Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.
Install Super Productivity v18.19.0 on that server, reachable at https://<DOMAIN>, behind the
existing Caddy with automatic TLS.
## 1. Preflight
If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer. Its
A record must already point at this server. Say three things when you ask, because they decide
whether this is the right install at all.
One: this server holds none of their tasks. The image is nginx serving a built web app as static
files, and upstream states that data is stored in the browser and the container provides no
persistent storage, so clearing site data for the hostname deletes the lot.
Two: with no server-side store there is no account and no login, and the page answers everybody. A
stranger who loads the URL gets their own empty list, not a window into the user's. Caddy
`basic_auth` is the opt-in and this install does not enable it.
Three: cross-device sync is not one setting away, and it is what a TickTick subscriber most wants
back. Block 8 carries the two measured reasons this install leaves it off.
Super Productivity needs 256 MB of RAM available and 2 GB free on /srv. The image is about 320 MB
and idles at nearly nothing. The pinned tag publishes linux/amd64 and linux/arm64. Measure:
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
If available RAM is under 256 MB or free disk is under 2 GB, print both numbers and stop. Do not
install and hope. If `dig +short` prints nothing, print that and stop: the certificate is
load-bearing here, because browsers grant a service worker and WebCrypto only on a secure origin.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/super-productivity /srv/super-productivity/backups
ls -la /srv/super-productivity
```
Assert: `ls -la` shows `backups` owned by the login user. There is no `data/` directory: step 4
mounts nothing, so a `data/` here would archive empty space and imply a safety that does not exist.
Say that to the user now.
## 3. Secrets
No secret is generated, there is no `.env` file, and there is no default credential for step 7 to
close. This build ships no account, no registration form and no administration screen, so there is
no first claimant and no claim race. Do not invent a setup step for software with none.
What replaces it is publication, and a sync credential this install never touches. The image's
entrypoint can write `assets/sync-config-default-override.json` from `WEBDAV_BASE_URL`,
`WEBDAV_USERNAME`, `WEBDAV_SYNC_FOLDER_PATH`, `SYNC_INTERVAL`, `IS_COMPRESSION_ENABLED` and
`IS_ENCRYPTION_ENABLED`, and that file is served to every visitor. Upstream provides no variable
for a password: the sync password is typed into the browser and stored by the browser. Step 4 sets
none of those variables, so the served file stays the one comment line the image ships, and step 7
prints it as evidence.
## 4. compose.yml
```bash
cat > /srv/super-productivity/compose.yml <<'EOF'
# Super Productivity · the deterministic fallback. Authored by caniselfhostit
# from the upstream documentation, not copied from a repository:
# run with docker .... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docs/wiki/2.13-Run-with-Docker.md
# entrypoint ......... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docker-entrypoint.sh
# nginx template ..... https://github.com/super-productivity/super-productivity/blob/v18.19.0/nginx/default.conf.template
#
# One service: nginx serving the built web app as static files. No database, no
# volume, no state on this box, because upstream states that data is stored in
# the browser and the container provides no persistent storage. No env_file and
# no environment block, on purpose: the entrypoint would write a world-readable
# sync-config-default-override.json under assets/ from the WEBDAV_ and
# SYNC_INTERVAL variables, and with WEBDAV_BACKEND unset the image's /webdav/
# proxy answers 404. Block 8 of the prompts says why. No
# `user:` line: nginx binds 80 in the container and the image names no
# unprivileged user; the healthcheck uses the curl the image installs.
#
# Image identity, checked 2026-08-14: the wiki's docker run line names a Docker
# Hub repository that does not resolve; the one that does, and that upstream's
# own compose file names, is johannesjo/super-productivity. Tag v18.19.0 was
# released 2026-08-07; digest read from registry-1.docker.io on 2026-08-14, an
# OCI index covering linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
super-productivity:
image: johannesjo/super-productivity:v18.19.0@sha256:ae91fe9ac19561e0f3669d15a2c4c71d7a75c43a29eb44ddc010ae50d1f63c82
container_name: super-productivity
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8199.
- "127.0.0.1:8199:80"
EOF
cd /srv/super-productivity && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. One service, one published port, no volumes, no env_file. Do not
add a Caddy service here: Caddy already runs under systemd on this box.
## 5. Caddy and TLS
Write the site block to `/srv/super-productivity/Caddyfile`, then append it to the live Caddyfile
with `<DOMAIN>` replaced. Copy the live file first: a syntax error takes down every site here.
```bash
cat > /srv/super-productivity/Caddyfile <<'EOF'
# Super Productivity · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. What is served here is
# public by design: there are no accounts, because there is no server-side store
# to have an account in, and a stranger loading this URL gets an empty task list
# in their own browser rather than a view of yours. Caddy basic_auth is the
# opt-in. TLS is not decoration here: the app registers a service worker and
# uses WebCrypto, and browsers grant neither on a plain-http hostname.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8199 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8199
}
EOF
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-super-productivity
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
DOMAIN_HOST=<DOMAIN>
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" /srv/super-productivity/Caddyfile | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Set `DOMAIN_HOST` to the real hostname from step 1 before running `sed`. Assert: `caddy validate`
exits 0 and the reload exits 0. If validate fails, restore
`/etc/caddy/Caddyfile.before-super-productivity`, reload, and report what it objected to. Caddy
issues the certificate on the first request and renews it on its own.
## 6. Firewall
Two ports open, both Caddy's, both idempotent:
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
80/tcp answers the ACME challenge and redirects to HTTPS, 443/tcp is the only way in, and 443/udp
is HTTP/3. 8199 stays closed because compose binds it to 127.0.0.1. Assert: `ufw status verbose`
prints `Status: active`, shows 80, 443/tcp and 443/udp, and no rule mentioning 8199.
## 7. Start and verify
```bash
cd /srv/super-productivity
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sSL https://<DOMAIN>/ | grep -c '<title>Super Productivity</title>'
curl -sS https://<DOMAIN>/assets/sync-config-default-override.json; echo
curl -sS https://<DOMAIN>/assets/sync-config-default-override.json | grep -ciE 'password|userName|baseUrl|syncFolderPath'
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/webdav/
docker compose ps
```
Assert all five and print what you received. The loop ends printing `200`. The title grep prints
`1`, because the built page carries `<title>Super Productivity</title>`. The third command prints
the served sync defaults, a single `_comment` key and nothing else. The fourth prints `0`, and that
zero is the security assert here: it proves the file the server hands every visitor names no
account, no server and no credential, because step 4 set none of the variables that would put them
there. The fifth prints `404`, the image's own proxy route answering with `WEBDAV_BACKEND` unset.
If any miss, stop, run `docker compose logs --tail 40 super-productivity`, and name the likely
earlier step. A 502 with a healthy container is step 5. A `200` with a title count of `0` means
another site block owns the hostname. Anything but `404` on the last line means an environment
variable reached the container that step 4 did not put there. A running container is not success.
STOP: tell the user to open https://<DOMAIN> in a private window, add one throwaway task, and
confirm two things: that the app loads with no login and the task appears, and that they are content
for anyone reaching this hostname to load the same app, knowing that gives them their own empty list
and no access to theirs. Do not continue until they confirm.
## 8. First backup and restore
Two backups here, and they are not the same thing. Do both, in this order.
The first is this box: one compose file and the live Caddy site block. Nothing under
/srv/super-productivity is written by the app, so the container need not stop.
```bash
cd /srv/super-productivity
sudo tar -czf /srv/super-productivity/backups/super-productivity-$(date +%F).tar.gz \
-C /srv/super-productivity compose.yml \
-C /etc/caddy Caddyfile
ls -lh /srv/super-productivity/backups/
```
Assert: the archive exists and is non-empty. Print its size. If that tar reports an error, stop
and report it.
A backup on the same disk as the data is not a backup. Run this one from the user's machine, not
the server:
```bash
mkdir -p ~/backups/super-productivity
scp vps:/srv/super-productivity/backups/*.tar.gz ~/backups/super-productivity/
```
To restore this box cold: recreate the directories as in step 2, untar the archive into
/srv/super-productivity, append the Caddy block to /etc/caddy/Caddyfile with `<DOMAIN>` replaced as
in step 5, validate, reload, then `cd /srv/super-productivity && docker compose up -d`. Same app,
same URL, no tasks, because none were ever here.
The second backup is the user's, and it is the one that matters. In the app: Settings, the
Sync & Backup tab, then Export data. That downloads one plaintext JSON file of tasks, projects,
tags, time tracking, notes, metrics and archives, which upstream describes as a restorable snapshot
of the application model. Import in the same place replaces current data with it, and that is the
restore. Keep the file off the machine that downloaded it and treat it as private: it is not
encrypted. The web build schedules no automatic file backup, so this export is the mechanism.
STOP: tell the user to export that file now, before a real week of work goes in, and to confirm both the filename and that a copy of it is off the machine that downloaded it. Do not continue until they confirm.
Sync would make that export a fallback rather than the plan, and this install does not configure
it, for two reasons measured against this image on 2026-08-14. Upstream states browser WebDAV sync
is likely to fail on CORS, and Nextcloud sends no CORS headers on its WebDAV endpoints. The image's
own same-origin answer, an nginx `/webdav/` route pointed at `WEBDAV_BACKEND`, refuses uploads over
its 1 MB `client_max_body_size` default with 413 and sends no TLS server name upstream, so an HTTPS
Nextcloud sharing an address with other sites can fail the handshake. Anyone taking that route must
set `WEBDAV_BACKEND` to a scheme and host with no path and no trailing slash, because a path there
replaces the whole request URI. For a Nextcloud user the reliable answer is the desktop build, which
is not subject to CORS.
## 9. Updating later
New versions are listed at https://github.com/super-productivity/super-productivity/releases. The
release tag and the image tag are the same string, and the image lives at
`johannesjo/super-productivity` even though the repository is now
`super-productivity/super-productivity`. Take a backup first, then edit the image line in
compose.yml to the new tag and its digest:
```bash
cd /srv/super-productivity
docker compose pull
docker compose up -d
docker compose logs --tail 20 super-productivity
```
Re-run step 7's asserts before calling the update done. The app also updates itself through its
service worker and asks the browser to reload, so a user with the tab open may be prompted minutes
later. Their data is in that browser: tell them to reload, not to clear.
## 10. What will probably go wrong
I opened this on my laptop, added a week of tasks, then opened the same URL on my phone, found an
empty list, and spent ten minutes sure the install was broken. It was not. There is no server-side
store, so the phone got its own copy of the app and its own empty database, and nothing was lost.
That is the one fact to hold on to, and it is why step 8 makes you export a file. Second: the
app refuses to run in two tabs at once and shows a blocker asking you to close one, which reads
like a crash until you notice the other tab. Third: if the browser denies the app's request for
persistent storage it says so once, in a small notification, and that is the only warning before
the browser can evict your tasks under disk pressure.
## 11. Out of scope
- Do not add a Caddy container to the compose file. Caddy already runs under systemd on this box.
- Do not publish 8199 on `0.0.0.0` or open it in the firewall.
- Do not invent accounts, a first-run wizard, or a claim-race warning for a build that has none.
- Do not set `WEBDAV_BACKEND` or any `WEBDAV_` variable in this session, and do not install
SuperSync: it is upstream's own sync server, beta by upstream's own description, and it wants a
Postgres and a second container.No terminal agent? Use the chat fallback — slower, you paste the commands
For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.
This path is slower: you paste every command yourself, and there is nobody watching the output but
you. If you can run Claude Code, use the other tab.
You are installing Super Productivity v18.19.0 on a VPS where Prompt Zero is done: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny. Run everything over `ssh vps` unless
a step says otherwise, and replace `<DOMAIN>` with the hostname whose A record already points at the
box.
Read these three before step 1, because they decide whether you want this install rather than how it
goes. The server will hold none of your tasks: the image is nginx serving a built web app as static
files, and upstream states that data is stored in the browser and the container provides no
persistent storage, so everything you type lives in the browser profile that loaded the page and
clearing site data for the hostname deletes it. There is no account and no login, because there is
no server-side store to have an account in, so the page answers everybody and a stranger who loads
it gets their own empty list rather than a view of yours. And cross-device sync is not one setting
away; step 8 has the two measured reasons this install leaves it off.
## 1. Preflight
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
You should see: at least `256` MB available, at least `2` G free, `amd64` or `arm64`, and your
server's IP address.
If you do not: with under 256 MB or under 2 GB, stop and resize the box rather than installing and
hoping. If `dig +short` prints nothing, add the A record and wait a minute. Caddy cannot issue a
certificate for a name that does not resolve, and the certificate is load-bearing here, because
browsers grant a service worker and WebCrypto only on a secure origin. `df -BG` is a GNU coreutils
flag; on a box that is neither Debian nor Ubuntu, run `df -h /srv` and read the figure yourself.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/super-productivity /srv/super-productivity/backups
ls -la /srv/super-productivity
```
You should see: `backups` under /srv/super-productivity, owned by you.
If you do not: check that you ran this as your login user and not as root. There is deliberately no
`data/` directory here; the compose file in step 4 mounts nothing, so a `data/` would be an empty
archive pretending to be a safety net.
## 3. Secrets
Nothing to generate. There is no `.env` file, no default credential, no registration form and no
administration screen, so there is no first claimant and no claim race, and no step here to run.
Two things to know instead. The image's entrypoint can write
`assets/sync-config-default-override.json` from `WEBDAV_BASE_URL`, `WEBDAV_USERNAME`,
`WEBDAV_SYNC_FOLDER_PATH`, `SYNC_INTERVAL`, `IS_COMPRESSION_ENABLED` and `IS_ENCRYPTION_ENABLED`,
and that file is served to everyone who loads your URL. Step 4 sets none of them, and step 7 prints
the file as proof. Upstream provides no variable for a password in any case: a sync password is
typed into the browser and kept by the browser, never by this server.
The second is a chat-window rule. Do not paste your sync password, any app password from another
service, or the contents of the JSON file step 8 exports into this chat. That export is plaintext
and it can carry the API credentials of any issue provider you connect later. The other tab's agent
never sees those values; this one will, if you paste them.
## 4. compose.yml
```bash
cat > /srv/super-productivity/compose.yml <<'EOF'
# Super Productivity · the deterministic fallback. Authored by caniselfhostit
# from the upstream documentation, not copied from a repository:
# run with docker .... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docs/wiki/2.13-Run-with-Docker.md
# entrypoint ......... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docker-entrypoint.sh
# nginx template ..... https://github.com/super-productivity/super-productivity/blob/v18.19.0/nginx/default.conf.template
#
# One service: nginx serving the built web app as static files. No database, no
# volume, no state on this box, because upstream states that data is stored in
# the browser and the container provides no persistent storage. No env_file and
# no environment block, on purpose: the entrypoint would write a world-readable
# sync-config-default-override.json under assets/ from the WEBDAV_ and
# SYNC_INTERVAL variables, and with WEBDAV_BACKEND unset the image's /webdav/
# proxy answers 404. Block 8 of the prompts says why. No
# `user:` line: nginx binds 80 in the container and the image names no
# unprivileged user; the healthcheck uses the curl the image installs.
#
# Image identity, checked 2026-08-14: the wiki's docker run line names a Docker
# Hub repository that does not resolve; the one that does, and that upstream's
# own compose file names, is johannesjo/super-productivity. Tag v18.19.0 was
# released 2026-08-07; digest read from registry-1.docker.io on 2026-08-14, an
# OCI index covering linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
super-productivity:
image: johannesjo/super-productivity:v18.19.0@sha256:ae91fe9ac19561e0f3669d15a2c4c71d7a75c43a29eb44ddc010ae50d1f63c82
container_name: super-productivity
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8199.
- "127.0.0.1:8199:80"
EOF
cd /srv/super-productivity && docker compose config >/dev/null && echo "compose OK"
```
You should see: `compose OK`.
If you do not: `docker compose config` prints the line it objected to, and it is almost always an
indentation change made while pasting. Re-paste the block in one go rather than editing it in place.
Do not add a Caddy service here; Caddy already runs under systemd on this box.
## 5. Caddy and TLS
```bash
cat > /srv/super-productivity/Caddyfile <<'EOF'
# Super Productivity · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. What is served here is
# public by design: there are no accounts, because there is no server-side store
# to have an account in, and a stranger loading this URL gets an empty task list
# in their own browser rather than a view of yours. Caddy basic_auth is the
# opt-in. TLS is not decoration here: the app registers a service worker and
# uses WebCrypto, and browsers grant neither on a plain-http hostname.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8199 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8199
}
EOF
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-super-productivity
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
DOMAIN_HOST=<DOMAIN>
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" /srv/super-productivity/Caddyfile | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Set `DOMAIN_HOST` to your real hostname before the `sed` line runs.
You should see: `Valid configuration` from `caddy validate`, and no output from the reload.
If you do not: restore the copy from a moment ago with
`sudo cp /etc/caddy/Caddyfile.before-super-productivity /etc/caddy/Caddyfile`, reload, and read what
validate objected to. The usual cause is `DOMAIN_HOST` still being literal, which writes the angle
brackets into the site name.
## 6. Firewall
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
You should see: `Status: active`, with 80/tcp, 443/tcp and 443/udp allowed and nothing mentioning
8199.
If you do not: 80/tcp answers the ACME challenge, 443/tcp is the only way in, 443/udp is HTTP/3.
8199 must never appear; compose binds it to 127.0.0.1, so opening it in the firewall would publish
the container past the reverse proxy. If a previous run added it, remove it with
`sudo ufw delete allow 8199`.
## 7. Start and verify
```bash
cd /srv/super-productivity
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sSL https://<DOMAIN>/ | grep -c '<title>Super Productivity</title>'
curl -sS https://<DOMAIN>/assets/sync-config-default-override.json; echo
curl -sS https://<DOMAIN>/assets/sync-config-default-override.json | grep -ciE 'password|userName|baseUrl|syncFolderPath'
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/webdav/
docker compose ps
```
You should see: the loop ending on `200`; a title count of `1`; a served sync-defaults file
containing a single `_comment` key and nothing else; a `0` from the grep after it; and `404` from
the /webdav/ line. That `0` is the security check on this install: it proves the file your server
hands to every visitor names no account, no server and no credential. The `404` is the image's own
proxy route answering with `WEBDAV_BACKEND` unset, so no second door is open on your hostname.
If you do not: a 502 with a healthy container means the Caddy step, so re-read step 5. A `200` with
a title count of `0` usually means another site block already owns the hostname, so look through
/etc/caddy/Caddyfile for a duplicate. Anything other than `404` on the last line means an
environment variable reached the container that step 4 did not put there. In every case run
`docker compose logs --tail 40 super-productivity` before changing anything. A running container is
not success.
One more thing before you trust a browser tab as evidence: the app registers a service worker, so a
browser that has loaded the page once keeps serving its cached copy even while the container is
down. Judge the install by the curl output and `docker compose ps` on the server, never by whether
the tab still looks fine.
STOP: open https://<DOMAIN> in a private window, add one throwaway task, and confirm two things to yourself: that the app loads with no login and the task appears, and that you are content for anyone who reaches this hostname to load the same app, knowing that gives them their own empty list and no access to yours. Do not continue until both are true.
## 8. First backup and restore
Two backups, and they are not the same thing. Do both, in this order.
The first is this box, and it is small on purpose: the compose file and the live Caddy site block.
Nothing under /srv/super-productivity is written by the app, so the container does not need to stop.
```bash
cd /srv/super-productivity
sudo tar -czf /srv/super-productivity/backups/super-productivity-$(date +%F).tar.gz \
-C /srv/super-productivity compose.yml \
-C /etc/caddy Caddyfile
ls -lh /srv/super-productivity/backups/
```
You should see: one `.tar.gz` listed, a few kilobytes, not zero bytes.
If you do not: read the tar error rather than moving past it. A zero-byte archive means the paths
were wrong, and an archive you never open is not a backup. Then copy it off the box, from your own
machine and not the server:
```bash
mkdir -p ~/backups/super-productivity
scp vps:/srv/super-productivity/backups/*.tar.gz ~/backups/super-productivity/
```
List the archive before you trust it, because an archive nobody has opened is a hope:
```bash
tar -tzf /srv/super-productivity/backups/*.tar.gz
```
You should see: exactly two names, `compose.yml` and `Caddyfile`.
To restore this box cold: recreate the two directories as in step 2, untar the archive into
/srv/super-productivity, append the Caddy site block to /etc/caddy/Caddyfile with `<DOMAIN>`
replaced as in step 5, validate, reload, then `cd /srv/super-productivity && docker compose up -d`.
That gives you the same app at the same URL, and no tasks, because none were ever here.
The second backup is yours, and it is the one that matters. In the app: Settings, the Sync & Backup
tab, then Export data. That downloads one plaintext JSON file of tasks, projects, tags, time
tracking, notes, metrics and archives, which upstream describes as a restorable snapshot of the
application model. Import in the same place replaces current data with it, and that is your restore.
Keep the file off the machine that downloaded it, and treat it as private: it is not encrypted. The
web build schedules no automatic file backup, so this export is the mechanism.
STOP: export that file now, before a real week of work goes in, and put a copy somewhere that is not the machine that downloaded it. Do not continue until that copy exists.
Sync would make that export a fallback rather than the plan, and this install does not configure it,
for two reasons measured against this image on 2026-08-14. Upstream states browser WebDAV sync is
likely to fail on CORS, and Nextcloud sends no CORS headers on its WebDAV endpoints. The image's own
same-origin answer, an nginx `/webdav/` route pointed at `WEBDAV_BACKEND`, refuses uploads over its
1 MB `client_max_body_size` default with 413 and sends no TLS server name upstream, so an HTTPS
Nextcloud that shares an address with other sites can fail the handshake. If you take that route
anyway, `WEBDAV_BACKEND` has to be a scheme and host with no path and no trailing slash, because a
path there replaces the whole request URI. For a Nextcloud user the reliable answer is the desktop
build, which is not subject to CORS.
## 9. Updating later
New versions are listed at https://github.com/super-productivity/super-productivity/releases. The
release tag and the image tag are the same string, and the image lives at
`johannesjo/super-productivity` even though the repository is now
`super-productivity/super-productivity`. Back up first, then edit the image line in
/srv/super-productivity/compose.yml to the new tag and its digest:
```bash
cd /srv/super-productivity
docker compose pull
docker compose up -d
docker compose logs --tail 20 super-productivity
```
You should see: the new image pulled, the container recreated, and a quiet nginx log. Read the
release notes before moving a pin: this project ships several releases a month, and v18.19.0,
released on 2026-08-07, will not stay the newest for long.
If you do not: the most common cause is a digest that does not match the tag you typed. Put the old
tag and digest back, run the same three commands, and the previous version returns. The app also
updates itself through its service worker and asks the browser to reload, so you may be prompted
minutes after the container changed. Reload; do not clear site data, because your tasks are in that
browser.
## 10. What will probably go wrong
I opened this on my laptop, added a week of tasks, then opened the same URL on my phone, found an
empty list, and spent ten minutes sure the install was broken. It was not. There is no server-side
store, so the phone got its own copy of the app and its own empty database, and nothing was lost.
That is the one fact to hold on to, and it is why step 8 makes you export a file. Second: the app
refuses to run in two tabs at once and shows a blocker asking you to close one, which reads like a
crash until you notice the other tab. Third: if the browser denies the app's request for persistent
storage it says so once, in a small notification, and that is the only warning before the browser
can evict your tasks under disk pressure.
## 11. Out of scope
- Do not add a Caddy container to the compose file. Caddy already runs under systemd on this box.
- Do not publish 8199 on `0.0.0.0` or open it in the firewall.
- Do not invent accounts or a first-run wizard for a build that has none.
- Do not set `WEBDAV_BACKEND` or any `WEBDAV_` variable in this session, and do not install
SuperSync: it is upstream's own sync server, beta by upstream's own description, and it wants a
Postgres and a second container.
If a check fails, name the step before changing anything else. Preflight is step 1. Compose errors
are step 4. Certificate and 502 problems are step 5. Ports that should be closed are step 6. A 200
with the wrong body is step 4 or 5. An empty archive is step 8.
NOT YET VERIFIED: no harness run has been recorded against this install path.260 lines · 13,415 bytes
What this prompt will do
- Preflight
- Docker
- Layout
- Secrets
- compose.yml
- Nothing is public
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.
Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.
Install Super Productivity v18.19.0 under ~/selfhost/super-productivity, answering at
http://localhost:8199.
## 1. Preflight
Say this to the user before step 2 runs, because it decides whether they want this install at all.
Super Productivity is local-first, and on this path that is literal twice over. The container is
nginx handing a built web app to the browser; upstream states that data is stored in the browser
and the container provides no persistent storage. So everything they type lives in this computer's
browser profile at http://localhost:8199, clearing site data for that address deletes their tasks,
and no phone or second laptop can open the list at all. What they get is a task list with
timeboxing, a Pomodoro timer and time tracking that never leaves this machine, and a backup that is
a file they export and keep.
Detect the OS and measure the machine:
```bash
uname -s
case "$(uname -s)" in
Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```
`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. Super
Productivity needs 256 MB of RAM available and 2 GB free on the home disk; the image is about
320 MB and publishes linux/amd64 and linux/arm64. If available RAM is under 256 MB or free disk is
under 2 GB, print both numbers and stop. Do not install and hope.
## 2. Docker
Check before installing anything:
```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```
If that printed `docker OK` and a compose version, skip to step 3.
Otherwise, install Docker for the OS step 1 detected:
- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
Homebrew, STOP: tell the user to download Docker Desktop from
https://www.docker.com/products/docker-desktop/ and install it, and wait until they
confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
terms, and wait for the whale icon to say it is running. Do not continue until they
confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
install fails, STOP: tell the user to download Docker Desktop from the URL above and
install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
repository, with its signing key saved to a file first, never piped into a shell. The
fence is guarded, a no-op on anything but a Linux with apt:
```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker "$USER"
fi
```
Adding the user to the docker group is root-equivalent on this machine; say that to the
user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
plugin with their distribution's package manager, and to run this prompt again once
`docker info` works.
Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.
## 3. Layout
```bash
mkdir -p ~/selfhost/super-productivity/backups
ls -la ~/selfhost/super-productivity
```
Assert: `backups` exists. There is no `data/` directory and no ownership fix to run on any of the
three systems, because step 5 mounts nothing: the container writes nothing you would want back.
## 4. Secrets
No secret is generated and there is no `.env` file. This build ships no account, no registration
form and no administration screen, so there is nothing to name, rotate or close, and no default
credential to worry about. Do not invent a first-run setup step for software with none.
One thing is worth saying anyway. The image's entrypoint can write
`assets/sync-config-default-override.json` from `WEBDAV_BASE_URL`, `WEBDAV_USERNAME`,
`WEBDAV_SYNC_FOLDER_PATH`, `SYNC_INTERVAL`, `IS_COMPRESSION_ENABLED` and `IS_ENCRYPTION_ENABLED`,
and step 5 sets none of them. Upstream provides no variable for a password in any case: a sync
password is typed into the browser and stored by the browser, never by this container.
## 5. compose.yml
```bash
cat > ~/selfhost/super-productivity/compose.yml <<'EOF'
# Super Productivity · the deterministic fallback for the local path. Authored
# by caniselfhostit from the upstream documentation, not copied from a
# repository:
# run with docker .... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docs/wiki/2.13-Run-with-Docker.md
# image build ........ https://github.com/super-productivity/super-productivity/blob/v18.19.0/Dockerfile
# entrypoint ......... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docker-entrypoint.sh
#
# One service on the computer you are sitting at. No volumes and no bind mounts,
# because there is nothing to mount: the image is nginx serving the built web
# app as static files, and every task you type is stored by the browser under
# http://localhost:8199, not by this container. That is also why there is no
# .env and no environment block. Browsers treat http://localhost as a secure
# context, so the parts of the app that need WebCrypto keep working without a
# certificate.
#
# Tag v18.19.0 was released 2026-08-07; digest read from registry-1.docker.io on
# 2026-08-14, an OCI index covering linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
super-productivity:
image: johannesjo/super-productivity:v18.19.0@sha256:ae91fe9ac19561e0f3669d15a2c4c71d7a75c43a29eb44ddc010ae50d1f63c82
container_name: super-productivity
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
ports:
# Loopback only: no other device on the wifi can reach 8199.
- "127.0.0.1:8199:80"
EOF
cd ~/selfhost/super-productivity && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. One service, one published port, no volumes, no env file.
## 6. Nothing is public
No reverse proxy, no certificate, no firewall rule, and each is a decision. There is no hostname to
resolve, and a certificate attests a public name that nothing here has. Browsers treat
http://localhost as a secure context anyway, which matters more for this app than for most: it
registers a service worker and uses WebCrypto, and both need that secure context. Nothing is
published beyond loopback, so no port needs closing. Confirm the binding:
```bash
grep -c '"127.0.0.1:' ~/selfhost/super-productivity/compose.yml
```
Assert: that count is exactly `1`. The user's phone cannot reach 8199, nor can a laptop on the same
wifi. For most apps that is a fair trade. For a task list it is the trade, because being open on a
second device is what a subscription was buying.
## 7. Start and verify
```bash
cd ~/selfhost/super-productivity
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8199/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sSL http://localhost:8199/ | grep -c '<title>Super Productivity</title>'
curl -sS http://localhost:8199/assets/sync-config-default-override.json; echo
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8199/webdav/
docker compose ps
```
Assert all four and print what you received. The loop ends printing `200`. The title grep prints
`1`. The third command prints the served sync defaults, a single `_comment` key and nothing else,
which is the evidence that no server, account or credential was baked into the page. The fourth
prints `404`, the image's own proxy route answering with `WEBDAV_BACKEND` unset. If
`port is already allocated` came back, find what holds 8199 (`lsof -nP -iTCP:8199 -sTCP:LISTEN`,
`ss -ltnp | grep 8199` on Linux, `netstat -ano | findstr :8199` on Windows) and stop until the user
frees it. There is no sign-in step and no wizard. A running container is not success.
STOP: tell the user to open http://localhost:8199, add one throwaway task, and confirm the app loads with no login and the task appears. Do not continue until they confirm.
## 8. First backup and restore
Two backups, and they are not the same thing. Do both.
The first is this folder, and it is one file, because nothing else here is yours:
```bash
cd ~/selfhost/super-productivity
tar -C ~/selfhost/super-productivity -czf ~/selfhost/super-productivity/backups/super-productivity-$(date +%F).tar.gz compose.yml
ls -lh ~/selfhost/super-productivity/backups/
```
Assert: the archive exists and is non-empty. Print its size. To restore it: untar `compose.yml`
back into ~/selfhost/super-productivity and run `docker compose up -d`. That returns the same app
at the same address, and no tasks, because none were ever in this folder.
The second backup is the user's, and it is the one that matters. In the app: Settings, the
Sync & Backup tab, then Export data. That downloads one plaintext JSON file of tasks, projects,
tags, time tracking, notes, metrics and archives, which upstream describes as a restorable snapshot
of the application model. Import in the same place replaces current data with it, and that is the
restore. On the web build there is no automatic file backup to schedule, so this export is the
mechanism, and on a laptop the disk and the machine fail together.
STOP: tell the user to export that file now, then copy it somewhere that leaves this computer, a folder their sync service watches or a USB stick, and confirm the filename is listed there. Do not continue until they confirm.
## 9. Updating later
New versions are listed at https://github.com/super-productivity/super-productivity/releases. The
release tag and the image tag are the same string, and the image lives at
`johannesjo/super-productivity` even though the repository is now
`super-productivity/super-productivity`. Export first, then edit the image line in
~/selfhost/super-productivity/compose.yml to the new tag and its digest:
```bash
cd ~/selfhost/super-productivity
docker compose pull
docker compose up -d
docker compose logs --tail 20 super-productivity
```
Re-run step 7's checks. The app also updates itself through its service worker and asks the browser
to reload, so the user may be prompted minutes later. Their data is in that browser: reload, do not
clear.
## 10. What will probably go wrong
I rebooted, opened the bookmark out of habit, and got a connection error, which read like lost data
for about a minute. Docker Desktop had not started, so nothing was serving 8199; the tasks were
still in the browser, untouched, because they were never in the container. Turn on Docker Desktop's
start-at-login setting, and after any reboot run
`cd ~/selfhost/super-productivity && docker compose up -d` before believing anything is wrong. Two
more, both local-specific. The app refuses to run in two tabs at once and shows a blocker asking
you to close one, which reads like a crash until you notice the other tab. And a browser profile is
a fragile place to keep a working week: a cleaning extension, a privacy setting, or a denied
persistent-storage request can take the lot, which is why step 8 asks for a file off this machine.
## 11. Out of scope
- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not rebind 8199 to 0.0.0.0 so a phone on the wifi can load it. It would not help: the phone
would get its own empty list, because the data is in this browser and not in the container.
- Do not set `WEBDAV_BACKEND` or any `WEBDAV_` variable, and do not install SuperSync. Both are
sync work, and sync on this build is a decision to make after reading the project page, not a
variable to add here.compose.local.ymlthe services, pinned · local layout34 lines
# Super Productivity · the deterministic fallback for the local path. Authored
# by caniselfhostit from the upstream documentation, not copied from a
# repository:
# run with docker .... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docs/wiki/2.13-Run-with-Docker.md
# image build ........ https://github.com/super-productivity/super-productivity/blob/v18.19.0/Dockerfile
# entrypoint ......... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docker-entrypoint.sh
#
# One service on the computer you are sitting at. No volumes and no bind mounts,
# because there is nothing to mount: the image is nginx serving the built web
# app as static files, and every task you type is stored by the browser under
# http://localhost:8199, not by this container. That is also why there is no
# .env and no environment block. Browsers treat http://localhost as a secure
# context, so the parts of the app that need WebCrypto keep working without a
# certificate.
#
# Tag v18.19.0 was released 2026-08-07; digest read from registry-1.docker.io on
# 2026-08-14, an OCI index covering linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
super-productivity:
image: johannesjo/super-productivity:v18.19.0@sha256:ae91fe9ac19561e0f3669d15a2c4c71d7a75c43a29eb44ddc010ae50d1f63c82
container_name: super-productivity
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
ports:
# Loopback only: no other device on the wifi can reach 8199.
- "127.0.0.1:8199:80"agent-readable mirror: /self-host/ticktick.md
The files, if you'd rather do it yourself
The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.
compose.ymlthe services, pinned38 lines
# Super Productivity · the deterministic fallback. Authored by caniselfhostit
# from the upstream documentation, not copied from a repository:
# run with docker .... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docs/wiki/2.13-Run-with-Docker.md
# entrypoint ......... https://github.com/super-productivity/super-productivity/blob/v18.19.0/docker-entrypoint.sh
# nginx template ..... https://github.com/super-productivity/super-productivity/blob/v18.19.0/nginx/default.conf.template
#
# One service: nginx serving the built web app as static files. No database, no
# volume, no state on this box, because upstream states that data is stored in
# the browser and the container provides no persistent storage. No env_file and
# no environment block, on purpose: the entrypoint would write a world-readable
# sync-config-default-override.json under assets/ from the WEBDAV_ and
# SYNC_INTERVAL variables, and with WEBDAV_BACKEND unset the image's /webdav/
# proxy answers 404. Block 8 of the prompts says why. No
# `user:` line: nginx binds 80 in the container and the image names no
# unprivileged user; the healthcheck uses the curl the image installs.
#
# Image identity, checked 2026-08-14: the wiki's docker run line names a Docker
# Hub repository that does not resolve; the one that does, and that upstream's
# own compose file names, is johannesjo/super-productivity. Tag v18.19.0 was
# released 2026-08-07; digest read from registry-1.docker.io on 2026-08-14, an
# OCI index covering linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
super-productivity:
image: johannesjo/super-productivity:v18.19.0@sha256:ae91fe9ac19561e0f3669d15a2c4c71d7a75c43a29eb44ddc010ae50d1f63c82
container_name: super-productivity
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8199.
- "127.0.0.1:8199:80"Caddyfilethe hostname and TLS28 lines
# Super Productivity · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. What is served here is
# public by design: there are no accounts, because there is no server-side store
# to have an account in, and a stranger loading this URL gets an empty task list
# in their own browser rather than a view of yours. Caddy basic_auth is the
# opt-in. TLS is not decoration here: the app registers a service worker and
# uses WebCrypto, and browsers grant neither on a plain-http hostname.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8199 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8199
}install.shthe same install, no agent141 lines
#!/usr/bin/env bash
# Super Productivity · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
# DOMAIN_HOST=tasks.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
# https://github.com/super-productivity/super-productivity/blob/v18.19.0/docs/wiki/2.13-Run-with-Docker.md
#
# No secret is generated and no .env is written. There are no accounts, no
# registration form and no first-run wizard, because there is no server-side
# store to have an account in: upstream states that data is stored in the
# browser and the container provides no persistent storage. The page is public
# by design, and a stranger who loads it gets an empty task list in their own
# browser rather than a window into yours. Caddy basic_auth is the opt-in if
# you would rather the URL were not world-readable.
#
# There is no application data directory. This script's backup is compose.yml
# plus the live Caddyfile. YOUR backup is the app's own Export data file, and
# the summary at the end tells you where to click for it.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail
APP_DIR="${APP_DIR:-/srv/super-productivity}"
DOMAIN_HOST="${DOMAIN_HOST:-}"
die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }
# --- 1. Refuse to start on a machine that is not ready -----------------------
[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. tasks.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 256 ] || die "only ${avail_mb} MB of RAM available; this install wants 256 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 2 ] || die "only ${avail_gb} GB free on /srv; this install wants 2 GB"
resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}')" || resolved=""
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."
# --- 2. Lay the files out ----------------------------------------------------
#
# No data/ directory. The container mounts nothing and writes nothing you
# would want back.
sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"
cd "$APP_DIR"
docker compose config >/dev/null
# --- 3. Caddy site block, on the host ----------------------------------------
if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-super-productivity"
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# --- 4. Ports ----------------------------------------------------------------
if command -v ufw >/dev/null 2>&1; then
echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8199 stays closed"
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
fi
# --- 5. Start it -------------------------------------------------------------
docker compose pull
docker compose up -d
echo "==> waiting for https://${DOMAIN_HOST}/"
for _ in $(seq 1 24); do
code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/")" || code=""
[ "$code" = "200" ] && break
sleep 5
done
[ "${code:-}" = "200" ] || die "https://${DOMAIN_HOST}/ answered ${code:-nothing}. Check: docker compose logs --tail 40 super-productivity"
# --- 6. Asserts, with their evidence printed ---------------------------------
title_hits="$(curl -sSL "https://${DOMAIN_HOST}/" | grep -c '<title>Super Productivity</title>')" || title_hits=0
echo "==> title marker count: ${title_hits}"
[ "$title_hits" -ge 1 ] || die "the page at https://${DOMAIN_HOST}/ does not look like Super Productivity"
echo "==> served sync defaults:"
override="$(curl -sS "https://${DOMAIN_HOST}/assets/sync-config-default-override.json")"
printf '%s\n' "$override"
leaks="$(printf '%s' "$override" | grep -ciE 'password|userName|baseUrl|syncFolderPath')" || leaks=0
echo "==> credential-shaped keys in that file: ${leaks}"
[ "$leaks" -eq 0 ] || die "the served sync defaults name a server or an account. This install sets no WEBDAV_ variables, so that file should carry nothing but its comment."
dav_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/webdav/")" || dav_code=""
echo "==> /webdav/ answered: ${dav_code}"
[ "$dav_code" = "404" ] || die "/webdav/ answered ${dav_code:-nothing}; with WEBDAV_BACKEND unset the image's proxy route must answer 404"
# --- 7. Backup (compose + live Caddyfile; there is no app state on this box) --
STAMP="$(date +%Y%m%d-%H%M%S)"
sudo tar -czf "$APP_DIR/backups/super-productivity-${STAMP}.tar.gz" \
-C "$APP_DIR" compose.yml \
-C /etc/caddy Caddyfile
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/super-productivity-${STAMP}.tar.gz" ] || die "the backup archive is empty"
cat <<-DONE
Super Productivity is answering at https://${DOMAIN_HOST}
1. Open it and add one task. That task is now in this browser and nowhere
else. The server holds nothing, which is why the archive above is two
files of configuration and not a database.
2. Take your real backup now, in the app: Settings, the Sync & Backup tab,
then Export data. That downloads one plaintext JSON file holding tasks,
projects, tags, time tracking, notes and archives. Import in the same
place puts it back. Keep the file somewhere that is not this laptop,
and treat it as private: it is not encrypted and it can carry the API
credentials of any issue provider you connect later.
3. That page is public. There is no login, because there is no account.
Anyone who reaches the hostname gets their own empty list, not yours.
Caddy basic_auth is the opt-in if you want a password in front.
4. Cross-device sync is not configured and is not one setting away. Read
block 8 of prompt.md before you try: upstream says browser WebDAV sync
is likely to fail on CORS, and the image's own same-origin workaround
caps uploads at 1 MB and sends no TLS server name upstream.
5. Copy $APP_DIR/backups off this disk, and copy the exported JSON off it
too. NOT YET VERIFIED on a clean harness machine.
DONEWhat you're signing up for
The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel TickTick.
- The server you are about to set up holds none of your tasks. This is a local-first app and the container is a static file server, so everything you type lives in the browser profile that loaded the page. Clearing site data for the hostname deletes the lot, and the browser can also refuse the app's request for persistent storage, which the app warns about rather than fixes. Your backup is a file you export yourself, and the prompts make you do it before they finish.
- Cross-device sync is the part that does not come across cleanly, and it is the whole reason people pay for TickTick. Upstream says browser WebDAV sync is likely to fail on CORS. The image ships its own same-origin workaround, a /webdav/ proxy driven by a WEBDAV_BACKEND variable, and this install leaves it off for two measured reasons: the nginx inside the image caps request bodies at its 1 MB default, and it sends no TLS server name to the upstream. If sync across devices is the requirement, the desktop app is the build upstream points at, and Vikunja is the one-container answer with a real server behind it.
- The page is public by design and that is less alarming here than it sounds, because there is nothing on the server to read. Anyone who reaches the hostname gets an empty task list in their own browser, not a window into yours. What they do get is the use of your bandwidth and the knowledge that the hostname exists. Caddy basic_auth is the documented opt-in and this install does not enable it.
- The web build is the smaller build. Calendar integration is likely to fail in a browser, local-file sync is desktop only, automatic file backups are desktop and mobile only, only one tab may hold the app at a time, and time tracking runs only while that tab is open. Nothing here reaches out to a closed laptop, so a due date is something you see when you look.
- What a TickTick subscriber loses on day one: shared lists and anyone to share them with, reminders that arrive on a phone you are not looking at, and voice capture. The Android and iOS builds exist and are free, but they are separate installs that only agree with this one if you solve the sync question above first.
Where this came from
“Data is stored in the browser; the container does not provide persistent storage.”
- Upstream states that data is stored in the browser and that the container provides no persistent storage, which is why this install mounts no volume and backs up no application directory. source
- The production image stage is FROM nginx:1 with the built browser bundle copied to /usr/share/nginx/html, so the container is a static file server with no application process behind it. source
- The image entrypoint writes assets/sync-config-default-override.json from WEBDAV_BASE_URL, WEBDAV_USERNAME, WEBDAV_SYNC_FOLDER_PATH, SYNC_INTERVAL, IS_COMPRESSION_ENABLED and IS_ENCRYPTION_ENABLED, and has no variable for a password, so no credential can be baked into the served bundle. source
- The image's nginx template proxies /webdav/ to the WEBDAV_BACKEND value and returns 404 when that value is empty, which is what makes an unconfigured install's only proxy route inert. source
- Upstream states that WebDAV sync and calendar integration are likely to fail in the browser because of CORS, and that the desktop and mobile builds issue those requests through a native HTTP layer instead. source
- Export produces a plaintext JSON snapshot of the restorable application model and import replaces current state with it; on the web there is no automatic file-based backup. source
- The project is MIT licensed at the pinned tag. source
Questions people actually ask
Answered from this page's own data — the same numbers, in sentences.
Can I self-host TickTick?
Not TickTick itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Super Productivity. A task list with timeboxing, a Pomodoro timer and per-task time tracking, served from one container that keeps none of your data. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 8 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.
What replaces TickTick?
Super Productivity. A task list with timeboxing, a Pomodoro timer and per-task time tracking, served from one container that keeps none of your data. The closest match to what a TickTick subscriber actually does all day: a personal list with timeboxing, a Pomodoro timer and per-task time tracking, MIT licensed, served from one container. The trade is worth understanding before you install rather than after. The web build is local-first in the strictest sense, so your tasks live in the browser profile you opened it in and not on the server you set up. That makes the install short and the backup a file you export yourself, and it makes cross-device sync the part you have to build. Reminders only reach you while a tab is open. Also evaluated and ranked below it: Vikunja. The prompt on this page installs Super Productivity only. Super Productivity is MIT-licensed and free; nothing on this page is a hosted service we sell you.
What does self-hosting cost compared to TickTick?
256 MB of RAM and 2 GB of disk — the smallest tier most VPS hosts sell, about $5 a month. Super Productivity itself is free and MIT-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: TickTick Premium, $3/mo — $36 a year.
How hard is it really?
ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the Super Productivity install, not from anyone's impression of it, and the whole rubric is published on the methodology page.
Can I run Super Productivity on my own computer instead of a server?
Yes — that is the second path in the prompt box above. "On my computer" installs the same Super Productivity on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. The catch: Everything you type lives in this computer's browser profile at http://localhost:8199, so clearing site data for that address deletes your tasks and no phone or second laptop can open the list at all. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.
Content last checked 2026-08-14. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.